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:
type | Sent when |
|---|---|
transaction.success | A transaction is fully paid and confirmed on-chain. |
transaction.failed | A transaction fails or is cancelled before it completes. |
refund.success | A refund is confirmed on-chain and the funds have reached the customer. |
refund.failed | A 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.
- Name
data- Type
- object
- Description
The full transaction object. See the Get Transaction Response for the complete field list.
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
statusreflecting 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
transactionIDandmerchantIDadded. 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
transactionIDandmerchantIDadded, andstatusreflecting 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:
- Read the
X-Lydianpay-Sigheader from the incoming request. - Take your webhook's
signingKey, strip thewk_prefix, and hex-decode the remainder to get the key bytes. - Compute the Ed25519 signature of the raw request body with that key and hex-encode the result.
- Compare it to the header value using a constant-time comparison. Reject the request if they do not match.
Verify against the exact bytes you received. Parsing and re-serializing the JSON can reorder keys or change formatting, which will change the signature and cause verification to fail. Capture the raw body before decoding it.
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)
}
