Webhook Events

This page describes the payloads Lydian delivers to your webhook endpoints and how to verify that each request genuinely came from Lydian. To register an endpoint, see Webhooks.

The Event Envelope

Every webhook — regardless of type — is delivered as the same JSON envelope over an HTTP POST with Content-Type: application/json. The envelope identifies the event and the account it belongs to, while the event-specific details live in the data object.

  • Name
    accountUUID
    Type
    string
    Description

    UUID of the account the event belongs to.

  • Name
    type
    Type
    string
    Description

    The event type — see Event Types below.

  • Name
    timestamp
    Type
    string
    Description

    RFC 3339 timestamp of when the event was emitted.

  • Name
    data
    Type
    object
    Description

    The event payload. Its structure depends on type.

envelope

{
    "accountUUID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
    "type": "transaction.success",
    "timestamp": "2025-08-15T10:15:44.762134-04:00",
    "data": { }
}

Event Types

A webhook only receives the event types it subscribed to when it was created (the events array — see Create Webhook). The following event types are available:

typeSent when
transaction.successA transaction is fully paid and confirmed on-chain.
transaction.failedA transaction fails or is cancelled before it completes.
refund.successA refund is confirmed on-chain and the funds have reached the customer.
refund.failedA refund cannot be completed.

transaction.success

Sent when a transaction has been fully paid and confirmed. The data object is the complete transaction record — identical in structure to the Get Transaction Response. A successful transaction has status 1 and no remaining balance.

transaction.success

{
    "accountUUID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
    "type": "transaction.success",
    "timestamp": "2025-08-15T10:15:44.762134-04:00",
    "data": {
        "transactionID": "236118b0-b43e-421e-998d-0d0f84b99e5a",
        "accountID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
        "merchantID": "7da1d36d-c995-4e99-bfb4-758fd2c94c99",
        "externalMID": "",
        "descriptor": "Order #1234",
        "referenceNumber": "d83fba40-87ea-4a62-bafa-a2601a673f51",
        "status": 1,
        "paymentStatus": 1,
        "amounts": {
            "currency": "USD",
            "totalLocal": 9.34,
            "totalUSD": 9.34,
            "receivedUSD": 9.34,
            "customerFeeUSD": 1.25,
            "fxRateToUSD": 1
        },
        "compliance": {
            "kycTriggered": false,
            "travelRuleTriggered": false
        },
        "payments": [
            {
                "paymentRequestID": "a8f2c1e0-1234-4abc-9def-0123456789ab",
                "cryptoAsset": "USDT",
                "cryptoNetwork": "ethereum",
                "address": "0xe095f9f7b75213d86767c0a6c5de4f2d93c9c4dd",
                "expectedCrypto": 9.34,
                "receivedCrypto": 9.34,
                "expectedUSD": 9.34,
                "receivedUSD": 9.34,
                "txnHash": "0x9f2c...c4dd",
                "confirmationCount": 12,
                "gasFee": 0.42,
                "status": 1,
                "qrData": "ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7/transfer?address=0xe095f9f7b75213d86767c0a6c5de4f2d93c9c4dd",
                "expiration": "2025-08-12T03:35:02.769Z"
            }
        ],
        "refunds": [],
        "remainingLocal": 0,
        "remainingUSD": 0,
        "cancellationReason": "",
        "createdAt": "2025-08-12T03:20:02.769Z",
        "updatedAt": "2025-08-12T03:21:44.762Z",
        "expiration": "2025-08-12T03:35:02.769Z"
    }
}

transaction.failed

Sent when a transaction fails or is cancelled before it completes. The data object has the same structure as transaction.success — the Get Transaction Response — with the transaction's status reflecting the failure (for example -2 cancelled or -4 forfeited) and cancellationReason set where applicable.

  • Name
    data
    Type
    object
    Description

    The full transaction object, with status reflecting the failure. See the Get Transaction Response for the complete field list and the full set of status values.

transaction.failed

{
    "accountUUID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
    "type": "transaction.failed",
    "timestamp": "2025-08-15T10:22:03.114902-04:00",
    "data": {
        "transactionID": "236118b0-b43e-421e-998d-0d0f84b99e5a",
        "accountID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
        "merchantID": "7da1d36d-c995-4e99-bfb4-758fd2c94c99",
        "descriptor": "Order #1234",
        "referenceNumber": "d83fba40-87ea-4a62-bafa-a2601a673f51",
        "status": -2,
        "paymentStatus": 0,
        "amounts": {
            "currency": "USD",
            "totalLocal": 9.34,
            "totalUSD": 9.34,
            "receivedUSD": 0,
            "customerFeeUSD": 1.25,
            "fxRateToUSD": 1
        },
        "remainingLocal": 9.34,
        "remainingUSD": 9.34,
        "cancellationReason": "Payment window expired",
        "createdAt": "2025-08-12T03:20:02.769Z",
        "updatedAt": "2025-08-12T03:35:02.769Z",
        "expiration": "2025-08-12T03:35:02.769Z"
    }
}

refund.success

Sent when a refund has been confirmed on-chain and the funds have reached the customer. The data object is the refund record — the same Refund Object that appears in a transaction's refunds array — plus transactionID and merchantID so the event can be tied back to the original payment. A successful refund has status 1.

  • Name
    data
    Type
    object
    Description

    The refund object with transactionID and merchantID added. See the Refund Object in the Get Transaction Response for the field list.

refund.success

{
    "accountUUID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
    "type": "refund.success",
    "timestamp": "2025-08-16T09:05:12.331901-04:00",
    "data": {
        "refundID": "acd09ac7-612f-4011-ac74-a5e1f4753648",
        "transactionID": "236118b0-b43e-421e-998d-0d0f84b99e5a",
        "merchantID": "7da1d36d-c995-4e99-bfb4-758fd2c94c99",
        "cryptoAsset": "USDT",
        "cryptoNetwork": "ethereum",
        "cryptoAmount": 9.34,
        "fiatAmount": 9.34,
        "fiatCurrency": "USD",
        "gasFee": 0.42,
        "gasFeePayer": "merchant",
        "receiverAddress": "0x1c9a3b7f2e5d4c6b8a0f9e8d7c6b5a4f3e2d1c0b",
        "status": 1,
        "reason": "Customer returned order #1234"
    }
}

refund.failed

Sent when a refund cannot be completed. The data object has the same structure as refund.success, with the refund's status reflecting the failure.

  • Name
    data
    Type
    object
    Description

    The refund object with transactionID and merchantID added, and status reflecting the failure.

refund.failed

{
    "accountUUID": "50e6dc19-8d39-4f33-a98c-8fbf4e12ed9d",
    "type": "refund.failed",
    "timestamp": "2025-08-16T09:12:47.902114-04:00",
    "data": {
        "refundID": "acd09ac7-612f-4011-ac74-a5e1f4753648",
        "transactionID": "236118b0-b43e-421e-998d-0d0f84b99e5a",
        "merchantID": "7da1d36d-c995-4e99-bfb4-758fd2c94c99",
        "cryptoAsset": "USDT",
        "cryptoNetwork": "ethereum",
        "cryptoAmount": 9.34,
        "fiatAmount": 9.34,
        "fiatCurrency": "USD",
        "gasFee": 0,
        "gasFeePayer": "merchant",
        "receiverAddress": "0x1c9a3b7f2e5d4c6b8a0f9e8d7c6b5a4f3e2d1c0b",
        "status": -1,
        "reason": "Customer returned order #1234"
    }
}

Verifying Signatures

Every webhook request includes an X-Lydianpay-Sig header so you can confirm it came from Lydian and was not tampered with in transit. Verify this signature on every request before acting on the payload.

  • Name
    X-Lydianpay-Sig
    Type
    string
    Description

    A hex-encoded Ed25519 signature of the raw request body, computed with your webhook's signing key.

The signature is produced with the signingKey returned when you created the webhook (see Create Webhook). To verify a request:

  1. Read the X-Lydianpay-Sig header from the incoming request.
  2. Take your webhook's signingKey, strip the wk_ prefix, and hex-decode the remainder to get the key bytes.
  3. Compute the Ed25519 signature of the raw request body with that key and hex-encode the result.
  4. Compare it to the header value using a constant-time comparison. Reject the request if they do not match.

Verify a webhook signature

import crypto from 'node:crypto'

// PKCS#8 DER prefix for an Ed25519 private key, followed by the 32-byte seed.
const PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex')

export function verifyWebhook(rawBody, signatureHeader, signingKey) {
  const keyBytes = Buffer.from(signingKey.replace(/^wk_/, ''), 'hex')
  const seed = keyBytes.subarray(0, 32) // 64-byte key = seed(32) + public(32)
  const privateKey = crypto.createPrivateKey({
    key: Buffer.concat([PKCS8_PREFIX, seed]),
    format: 'der',
    type: 'pkcs8',
  })

  const expected = crypto.sign(null, Buffer.from(rawBody), privateKey)
  const received = Buffer.from(signatureHeader, 'hex')
  return expected.length === received.length && crypto.timingSafeEqual(expected, received)
}