Custodian
Webhook

Webhook

Register an HTTPS endpoint to receive real-time events such as account creation, paid invoices, wallet deposits, withdrawals, and invoice lifecycle changes. Settings endpoints require authentication.

Balance types in webhooks: Confirmed deposit events (deposit.invoice.paid, deposit.wallet.credited) include both an on-chain transaction and an off-chain credit. Pending events (*.pending_confirmation) include the on-chain transaction only — the ledger is not credited yet. Withdrawal events describe amounts deducted from off-chain balance. See Understanding Balances.

Manage Webhook Settings

Get Settings

GET /api/v1/webhooks/settings

{
  "data": {
    "url": "https://your-app.com/webhook",
    "enabled": true,
    "updatedAt": "2026-01-01T00:00:00Z"
  }
}
FieldDescription
urlHTTPS endpoint that receives POST event payloads
enabledtrue when settings exist and deliveries are allowed
updatedAtLast time the URL was saved
CodeMeaning
404No webhook settings for this app
503Outbound webhooks not configured on the server

Set / Update Settings

PUT /api/v1/webhooks/settings

FieldTypeRequiredDescription
urlstringYesA valid https URL
{ "url": "https://your-app.com/webhook" }

Delete Settings

DELETE /api/v1/webhooks/settings → { "status": "ok" }

CodeMeaning
404Settings not found

Send a Test Event

POST /api/v1/webhooks/test (no body). Queues a webhook.test delivery to your URL.

Response 202:

{
  "data": {
    "id": "delivery-uuid",
    "eventId": "webhook.test:…",
    "eventType": "webhook.test",
    "status": "pending",
    "attemptCount": 0,
    "maxAttempts": 5,
    "createdAt": "2026-01-01T00:00:00Z",
    "updatedAt": "2026-01-01T00:00:00Z"
  }
}

List Deliveries

GET /api/v1/webhooks/deliveries

QueryDefaultDescription
page1Page number
limit20Page size

Delivery object

FieldTypeDescription
idstringDelivery attempt record UUID
eventIdstringSame id as X-Xellar-Event-Id / payload id
eventTypestringEvent type string (e.g. deposit.invoice.paid)
statusstringDelivery state (e.g. pending, success, failed)
attemptCountintHTTP delivery attempts so far
maxAttemptsintMaximum retries
lastHttpStatusint | nullLast HTTP status from your server
lastErrorstring | nullLast transport or response error
deliveredAtstring | nullWhen your endpoint returned success
createdAtstringRFC3339
updatedAtstringRFC3339

Receiving Events

Xellar sends a POST request to your configured URL with these headers:

HeaderDescription
Content-Typeapplication/json
X-Xellar-Event-IdUnique event ID (same value as the JSON id field)
X-Xellar-TimestampUnix time in seconds
X-Xellar-SignatureBase64 ED25519 signature

Payload Envelope

Every event uses the same top-level shape:

{
  "apiVersion": "2025-06-22",
  "id": "<event-id>",
  "type": "<event-type>",
  "createdAt": "2026-07-08T06:21:44.051787576Z",
  "data": { }
}
FieldDescription
apiVersionWebhook schema version. Currently 2025-06-22.
idStable event identifier used for idempotency and signature verification.
typeEvent type string (see below).
createdAtUTC timestamp when the event was created.
dataEvent-specific payload.

Event ID Format

The id field (and X-Xellar-Event-Id header) follow a predictable pattern per event type:

TypeEvent ID pattern
account.createdaccount.created:<accountId>
deposit.invoice.pending_confirmationdeposit.invoice.pending_confirmation:<invoiceId>:<txHash>
deposit.wallet.pending_confirmationdeposit.wallet.pending_confirmation:<walletId>:<txHash>
deposit.invoice.paiddeposit.invoice.paid:<invoiceId>:<txHash>
deposit.wallet.crediteddeposit.wallet.credited:<tokenTransferId>
invoice.expiredinvoice.expired:<invoiceId>
invoice.failedinvoice.failed:<invoiceId>:<txHash>
withdraw.createdwithdraw.created:<withdrawId>
withdraw.successwithdraw.success:<withdrawId>
withdraw.failedwithdraw.failed:<withdrawId>
webhook.testRandom UUID

Event Types Overview

TypeWhen it fires
account.createdA custodial account is provisioned and wallet addresses are ready
deposit.invoice.pending_confirmationAn invoice-matching on-chain transfer is detected (first webhook). Not final. No credit.
deposit.wallet.pending_confirmationA listen-wallet on-chain transfer is detected (first webhook). Not final. No credit.
deposit.invoice.paidAfter required block confirmations: invoice marked paid and off-chain balance credited (second webhook)
deposit.wallet.creditedAfter required block confirmations: listen-wallet deposit credited — not matched to an invoice (second webhook)
invoice.expiredAn invoice passes its expiry time without payment
invoice.failedAfter confirmations: a payment is detected for an invoice address but the amount does not match
withdraw.createdA withdrawal request is accepted and queued
withdraw.successA withdrawal completes on-chain
withdraw.failedA withdrawal fails
webhook.testYou call POST /api/v1/webhooks/test

Deposit webhooks (two steps)

Every matched deposit sends up to two outbound webhooks for the same transaction hash:

  1. Detected (pending confirmation) — deposit.invoice.pending_confirmation or deposit.wallet.pending_confirmation
    Fired as soon as Xellar matches the on-chain transfer to an invoice or listen wallet.
    data.confirmationStatus is always "pending". There is no credit object and your off-chain balance is unchanged.

  2. Confirmed (credited) — deposit.invoice.paid or deposit.wallet.credited
    Fired after the transaction reaches the required number of block confirmations for that network (network-specific; often more than one block).
    The invoice is updated (if applicable) and your off-chain balance is credited via credit.

On-chain transfer detected
        │
        ▼
deposit.*.pending_confirmation   ← show “processing” in your UI
        │
        │  wait for block confirmations
        ▼
deposit.invoice.paid  OR  deposit.wallet.credited   ← treat as final / credit balance

Use paid / credited for ledger and balance logic. Use pending_confirmation only for early UX (e.g. “payment detected, waiting for confirmations”). You may receive both events for the same transaction.txHash; use X-Xellar-Event-Id (or JSON id) for idempotency.

Shared Objects

Several fields appear across multiple event types:

token

FieldTypeDescription
idstringToken UUID
tokenCodestringToken code (e.g. usdc-polygon-pos)
symbolstringDisplay symbol (e.g. USDT)
decimalsnumberToken decimal places

credit (ledger credit applied to your off-chain balance)

FieldTypeDescription
tokenCodestringToken credited
amountnumberHuman-readable amount added to off-chain activeBalance
referenceTypestringinvoice or token_transfer
referenceIdstringInvoice ID or token transfer ID

transaction (on-chain transfer details — the blockchain payment that triggered the event)

FieldTypeDescription
txHashstringTransaction hash
chainIdnumberChain ID
networkCodestringNetwork code (e.g. polygon-pos)
fromstringSender address
tostringRecipient address
tokenAddressstringContract address (native coin may be empty)
tokenSymbolstringToken symbol from the chain event
valuestringOn-chain raw amount in smallest unit (string)
valueDecimalsnumberDecimals used for value
blockchainTypestringe.g. evm, tron, solana

account.created

Sent when MPC account creation completes and wallet addresses are available for all configured networks.

data fields

FieldTypeDescription
appIdstringYour app UUID
accountobjectCreated account details
walletsarrayOne entry per network wallet

account object

FieldTypeDescription
idstringAccount UUID
subIdstringYour user identifier
mpcSubIdstringMPC sub-account ID
statusstringactive
poolTypestringdedicated or organization_owned

wallets[] object

FieldTypeDescription
walletIdstringWallet UUID
networkstringNetwork code
addressstringDeposit address on that network
addressTypestringe.g. legacy, native-segwit, or empty for EVM/Solana/Tron
{
  "apiVersion": "2025-06-22",
  "id": "account.created:a9e73399-00ce-4c1f-bd25-316c140345a6",
  "type": "account.created",
  "createdAt": "2026-07-08T06:10:26.462722174Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "account": {
      "id": "a9e73399-00ce-4c1f-bd25-316c140345a6",
      "subId": "baru1",
      "status": "active",
      "mpcSubId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45-baru1",
      "poolType": "dedicated"
    },
    "wallets": [
      {
        "walletId": "97736085-85be-405e-9b50-be3d1feec524",
        "network": "ethereum",
        "address": "0xb22139675b58b1e5ae1541530aada62afa309077",
        "addressType": ""
      },
      {
        "walletId": "57e1151f-26ef-41e9-ae12-caa0748be294",
        "network": "polygon-pos",
        "address": "0xb22139675b58b1e5ae1541530aada62afa309077",
        "addressType": ""
      },
      {
        "walletId": "61b86a18-ec2e-4179-ad32-c39877860eb9",
        "network": "bitcoin",
        "address": "1MYp8aU3qNwhtU5dHrrSPkZYNAqug9bq15",
        "addressType": "legacy"
      }
    ]
  }
}

The wallets array includes every network configured for your app. EVM networks typically share the same address; UTXO chains may include multiple address types per network.


deposit.invoice.pending_confirmation

Step 1 of 2 for invoice deposits. Sent as soon as Xellar detects an on-chain transfer that exactly matches a pending invoice amount and address.

This happens before the required block confirmations complete. The invoice status stays pending. There is no credit and no tokenTransfer. When confirmations finish, you receive deposit.invoice.paid for the same transaction.txHash (separate event id).

data fields

FieldTypeDescription
matchTypestringAlways invoice
confirmationStatusstringAlways pending
appIdstringYour app UUID
userIdstringEnd-user UUID linked to the invoice
invoiceobjectInvoice details (status is still pending)
transactionobjectOn-chain transfer that was seen
tokenobjectToken metadata
{
  "apiVersion": "2025-06-22",
  "id": "deposit.invoice.pending_confirmation:9544b2ae-c84a-4bc5-a7ed-599b20d0f25d:0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05",
  "type": "deposit.invoice.pending_confirmation",
  "createdAt": "2026-07-08T06:21:40.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "matchType": "invoice",
    "confirmationStatus": "pending",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "invoice": {
      "id": "9544b2ae-c84a-4bc5-a7ed-599b20d0f25d",
      "status": "pending",
      "amount": 0.00123,
      "amountInDecimals": "1230",
      "reference": "INV-001990",
      "walletId": "5326036c-50c3-4d63-8150-69cb253542e3",
      "depositAddress": "0x37937c09c7454b1d4ff8fb7657cfd84418168036",
      "transactionHash": "0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05"
    },
    "transaction": {
      "txHash": "0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05",
      "chainId": 137,
      "networkCode": "polygon-pos",
      "from": "0x0f6c4A746C303a2ad7E008302d61f615D97dfe3e",
      "to": "0x37937c09C7454B1d4Ff8fB7657cFd84418168036",
      "tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
      "value": "1230",
      "valueDecimals": 6,
      "blockchainType": "evm"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    }
  }
}

deposit.wallet.pending_confirmation

Step 1 of 2 for listen-wallet deposits. Sent as soon as Xellar detects a transfer to a listen wallet that is not matched to an invoice.

Same timing as the invoice pending event: before block confirmations, no credit. When confirmations finish, you receive deposit.wallet.credited for the same transaction.txHash (separate event id).

data fields

FieldTypeDescription
matchTypestringAlways wallet
confirmationStatusstringAlways pending
appIdstringYour app UUID
walletobjectDeposit wallet
transactionobjectOn-chain transfer that was seen
tokenobjectToken metadata
amountnumberDetected human-readable amount (not yet credited)
{
  "apiVersion": "2025-06-22",
  "id": "deposit.wallet.pending_confirmation:57e1151f-26ef-41e9-ae12-caa0748be294:0x79ed73ec782a3a01050885b37a7261873369448e6d863be23664f96a40f305fb",
  "type": "deposit.wallet.pending_confirmation",
  "createdAt": "2026-07-08T06:24:30.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "matchType": "wallet",
    "confirmationStatus": "pending",
    "wallet": {
      "walletId": "57e1151f-26ef-41e9-ae12-caa0748be294",
      "address": "0xb22139675b58b1e5ae1541530aada62afa309077",
      "networkCode": "polygon-pos",
      "chainId": 137
    },
    "transaction": {
      "txHash": "0x79ed73ec782a3a01050885b37a7261873369448e6d863be23664f96a40f305fb",
      "chainId": 137,
      "networkCode": "polygon-pos",
      "from": "0x0f6c4A746C303a2ad7E008302d61f615D97dfe3e",
      "to": "0xB22139675b58B1E5aE1541530AAdA62AFA309077",
      "tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
      "value": "1340",
      "valueDecimals": 6,
      "blockchainType": "evm"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    },
    "amount": 0.00134
  }
}

deposit.invoice.paid

Step 2 of 2 for invoice deposits. Sent after the matching transfer reaches the network’s required block confirmation depth.

The invoice is marked paid and your off-chain balance is credited via the credit object. If you already handled deposit.invoice.pending_confirmation, this is the event that confirms the payment for accounting and balance updates.

data fields

FieldTypeDescription
matchTypestringAlways invoice
appIdstringYour app UUID
userIdstringEnd-user UUID linked to the invoice
invoiceobjectPaid invoice details
transactionobjectOn-chain transfer that triggered the event
tokenobjectToken metadata
tokenTransferobjectInternal transfer record
creditobjectOff-chain balance credit applied

invoice object

FieldTypeDescription
idstringInvoice UUID
statusstringpaid
amountnumberExpected amount
amountInDecimalsstringAmount in smallest unit
referencestringYour invoice reference
walletIdstringDeposit wallet UUID
depositAddressstringAddress that received payment
paidAtstringUTC timestamp
transactionHashstringMatching transaction hash
{
  "apiVersion": "2025-06-22",
  "id": "deposit.invoice.paid:9544b2ae-c84a-4bc5-a7ed-599b20d0f25d:0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05",
  "type": "deposit.invoice.paid",
  "createdAt": "2026-07-08T06:21:44.051787576Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "matchType": "invoice",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "invoice": {
      "id": "9544b2ae-c84a-4bc5-a7ed-599b20d0f25d",
      "status": "paid",
      "amount": 0.00123,
      "amountInDecimals": "1230",
      "reference": "INV-001990",
      "walletId": "5326036c-50c3-4d63-8150-69cb253542e3",
      "depositAddress": "0x37937c09c7454b1d4ff8fb7657cfd84418168036",
      "paidAt": "2026-07-08T06:21:44.041555Z",
      "transactionHash": "0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05"
    },
    "transaction": {
      "txHash": "0x005c514230821be3b7f0a588b9012f76cf1b84a90a28235cf772128ec1775b05",
      "chainId": 137,
      "networkCode": "polygon-pos",
      "from": "0x0f6c4A746C303a2ad7E008302d61f615D97dfe3e",
      "to": "0x37937c09C7454B1d4Ff8fB7657cFd84418168036",
      "tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
      "value": "1230",
      "valueDecimals": 6,
      "blockchainType": "evm"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    },
    "tokenTransfer": {
      "id": "9633bb86-aeec-4b83-97c3-9d9c1ff48de1",
      "amountInDecimals": "1230"
    },
    "credit": {
      "tokenCode": "usdc-polygon-pos",
      "amount": 0.00123,
      "referenceType": "invoice",
      "referenceId": "9544b2ae-c84a-4bc5-a7ed-599b20d0f25d"
    }
  }
}

deposit.wallet.credited

Step 2 of 2 for listen-wallet deposits. Sent after the transfer reaches the network’s required block confirmation depth and is not matched to an invoice.

The on-chain transaction is recorded and your off-chain balance is updated via credit. If you already handled deposit.wallet.pending_confirmation, this is the event that confirms the deposit for accounting and balance updates.

data fields

FieldTypeDescription
matchTypestringAlways wallet
appIdstringYour app UUID
walletobjectCredited wallet
transactionobjectOn-chain transfer that triggered the event
tokenobjectToken metadata
tokenTransferobjectInternal transfer record
creditobjectOff-chain balance credit applied

wallet object

FieldTypeDescription
walletIdstringWallet UUID
addressstringWallet address
networkCodestringNetwork code
chainIdnumberChain ID
{
  "apiVersion": "2025-06-22",
  "id": "deposit.wallet.credited:44f5465c-e1b4-4128-aa88-728290a9dea6",
  "type": "deposit.wallet.credited",
  "createdAt": "2026-07-08T06:24:38.863991778Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "matchType": "wallet",
    "wallet": {
      "walletId": "57e1151f-26ef-41e9-ae12-caa0748be294",
      "address": "0xb22139675b58b1e5ae1541530aada62afa309077",
      "networkCode": "polygon-pos",
      "chainId": 137
    },
    "transaction": {
      "txHash": "0x79ed73ec782a3a01050885b37a7261873369448e6d863be23664f96a40f305fb",
      "chainId": 137,
      "networkCode": "polygon-pos",
      "from": "0x0f6c4A746C303a2ad7E008302d61f615D97dfe3e",
      "to": "0xB22139675b58B1E5aE1541530AAdA62AFA309077",
      "tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
      "value": "1340",
      "valueDecimals": 6,
      "blockchainType": "evm"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    },
    "tokenTransfer": {
      "id": "44f5465c-e1b4-4128-aa88-728290a9dea6",
      "amountInDecimals": "1340"
    },
    "credit": {
      "tokenCode": "usdc-polygon-pos",
      "amount": 0.00134,
      "referenceType": "token_transfer",
      "referenceId": "44f5465c-e1b4-4128-aa88-728290a9dea6"
    }
  }
}

invoice.expired

Sent when a pending invoice passes its expiry window without receiving payment.

data fields

FieldTypeDescription
appIdstringYour app UUID
userIdstringEnd-user UUID
invoiceobjectExpired invoice details
tokenobjectToken metadata

invoice object

FieldTypeDescription
idstringInvoice UUID
statusstringexpired
amountnumberExpected amount
amountInDecimalsstringAmount in smallest unit
referencestringYour invoice reference
walletIdstringDeposit wallet UUID
depositAddressstringAddress that was assigned
expiresAtstringOriginal expiry timestamp
bufferInSecondsnumberGrace buffer after expiry
expiredAtstringWhen the expiry was processed
{
  "apiVersion": "2025-06-22",
  "id": "invoice.expired:9544b2ae-c84a-4bc5-a7ed-599b20d0f25d",
  "type": "invoice.expired",
  "createdAt": "2026-07-08T07:00:00.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "invoice": {
      "id": "9544b2ae-c84a-4bc5-a7ed-599b20d0f25d",
      "status": "expired",
      "amount": 0.00123,
      "amountInDecimals": "1230",
      "reference": "INV-001990",
      "walletId": "5326036c-50c3-4d63-8150-69cb253542e3",
      "depositAddress": "0x37937c09c7454b1d4ff8fb7657cfd84418168036",
      "expiresAt": "2026-07-08T06:30:00.000000000Z",
      "bufferInSeconds": 300,
      "expiredAt": "2026-07-08T07:00:00.000000000Z"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    }
  }
}

invoice.failed

Sent when a transfer arrives at an invoice deposit address but the amount does not match the expected invoice amount.

data fields

FieldTypeDescription
userIdstringEnd-user UUID
reasonstringFailure reason (currently wrong_amount)
invoiceobjectInvoice that was targeted
transactionobjectOn-chain transfer that failed to match
tokenobjectToken metadata
{
  "apiVersion": "2025-06-22",
  "id": "invoice.failed:9544b2ae-c84a-4bc5-a7ed-599b20d0f25d:0xabc123...",
  "type": "invoice.failed",
  "createdAt": "2026-07-08T06:25:00.000000000Z",
  "data": {
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "reason": "wrong_amount",
    "invoice": {
      "id": "9544b2ae-c84a-4bc5-a7ed-599b20d0f25d",
      "status": "pending",
      "amount": 0.00123,
      "amountInDecimals": "1230",
      "reference": "INV-001990",
      "walletId": "5326036c-50c3-4d63-8150-69cb253542e3",
      "depositAddress": "0x37937c09c7454b1d4ff8fb7657cfd84418168036"
    },
    "transaction": {
      "txHash": "0xabc123...",
      "chainId": 137,
      "networkCode": "polygon-pos",
      "from": "0x0f6c4A746C303a2ad7E008302d61f615D97dfe3e",
      "to": "0x37937c09C7454B1d4Ff8fB7657cFd84418168036",
      "tokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
      "value": "500",
      "valueDecimals": 6,
      "blockchainType": "evm"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdc-polygon-pos",
      "symbol": "USDT",
      "decimals": 6
    }
  }
}

withdraw.created

Sent when a withdrawal request is accepted and enters the processing pipeline. totalDeducted is removed from your off-chain activeBalance (moved to withholdBalance while pending).

data fields

FieldTypeDescription
appIdstringYour app UUID
userIdstringEnd-user UUID
withdrawobjectWithdrawal details
tokenobjectToken metadata
networkobjectTarget network

withdraw object

FieldTypeDescription
idstringWithdrawal UUID
statusstringpending
amountnumberRequested amount
netAmountnumberAmount sent to recipient after fees
totalDeductednumberTotal deducted from off-chain balance
withdrawGasFeenumberGas fee component
amountInDecimalsstringAmount in smallest unit
recipientAddressstringDestination address
referencestring | nullYour reference
withdrawWalletIdstringHot wallet that broadcasts the transfer
walletIdstring | nullLedger / deposit wallet deducted on per_wallet apps (omitted for per_app)
withdrawTransactionFeestringFee mode (e.g. exclusive)
txHashstring | nullOn-chain hash (null until broadcast)
createdAtstringUTC timestamp
updatedAtstringUTC timestamp

network object

FieldTypeDescription
codestringNetwork code
chainIdnumberChain ID
typestringBlockchain type
{
  "apiVersion": "2025-06-22",
  "id": "withdraw.created:wd_abc123",
  "type": "withdraw.created",
  "createdAt": "2026-07-08T08:00:00.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "withdraw": {
      "id": "wd_abc123",
      "status": "pending",
      "amount": 50,
      "netAmount": 49.5,
      "totalDeducted": 50.5,
      "withdrawGasFee": 0.5,
      "amountInDecimals": "50000000",
      "recipientAddress": "0xdef...",
      "reference": "WD-001",
      "withdrawWalletId": "wal_hot",
      "walletId": "wal_1",
      "withdrawTransactionFee": "exclusive",
      "txHash": null,
      "createdAt": "2026-07-08T08:00:00.000000000Z",
      "updatedAt": "2026-07-08T08:00:00.000000000Z"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdt-ethereum",
      "symbol": "USDT",
      "decimals": 6
    },
    "network": {
      "code": "ethereum",
      "chainId": 1,
      "type": "evm"
    }
  }
}

withdraw.success

Sent when a withdrawal completes successfully on-chain. Same data shape as withdraw.created, with status: "success" and txHash populated.

{
  "apiVersion": "2025-06-22",
  "id": "withdraw.success:wd_abc123",
  "type": "withdraw.success",
  "createdAt": "2026-07-08T08:05:00.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "withdraw": {
      "id": "wd_abc123",
      "status": "success",
      "amount": 50,
      "netAmount": 49.5,
      "totalDeducted": 50.5,
      "withdrawGasFee": 0.5,
      "amountInDecimals": "50000000",
      "recipientAddress": "0xdef...",
      "reference": "WD-001",
      "withdrawWalletId": "wal_hot",
      "walletId": "wal_1",
      "withdrawTransactionFee": "exclusive",
      "txHash": "0x789...",
      "createdAt": "2026-07-08T08:00:00.000000000Z",
      "updatedAt": "2026-07-08T08:05:00.000000000Z"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdt-ethereum",
      "symbol": "USDT",
      "decimals": 6
    },
    "network": {
      "code": "ethereum",
      "chainId": 1,
      "type": "evm"
    }
  }
}

withdraw.failed

Sent when a withdrawal fails. Same data shape as withdraw.created, with status: "failed".

{
  "apiVersion": "2025-06-22",
  "id": "withdraw.failed:wd_abc123",
  "type": "withdraw.failed",
  "createdAt": "2026-07-08T08:05:00.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "userId": "de4865aa-f81b-4fe0-9e62-6e5a8f674280",
    "withdraw": {
      "id": "wd_abc123",
      "status": "failed",
      "amount": 50,
      "netAmount": 49.5,
      "totalDeducted": 50.5,
      "withdrawGasFee": 0.5,
      "amountInDecimals": "50000000",
      "recipientAddress": "0xdef...",
      "reference": "WD-001",
      "withdrawWalletId": "wal_hot",
      "walletId": "wal_1",
      "withdrawTransactionFee": "exclusive",
      "txHash": null,
      "createdAt": "2026-07-08T08:00:00.000000000Z",
      "updatedAt": "2026-07-08T08:05:00.000000000Z"
    },
    "token": {
      "id": "53c22951-9740-47dc-82f0-db0179e617a7",
      "tokenCode": "usdt-ethereum",
      "symbol": "USDT",
      "decimals": 6
    },
    "network": {
      "code": "ethereum",
      "chainId": 1,
      "type": "evm"
    }
  }
}

webhook.test

Sent when you call POST /api/v1/webhooks/test. Use it to verify your endpoint and signature verification.

{
  "apiVersion": "2025-06-22",
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "type": "webhook.test",
  "createdAt": "2026-07-08T09:00:00.000000000Z",
  "data": {
    "appId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
    "message": "This is a test webhook from Xellar Custodian API."
  }
}

Respond with HTTP 200 to acknowledge. Non-2xx responses are retried with exponential backoff (up to 5 attempts).

Verifying the Signature

The signature is computed over event_id + "\n" + timestamp + "\n" + raw_body. Fetch the public key from the public endpoint:

GET /v1/webhooks/public-key →

{ "algorithm": "ed25519", "publicKey": "<base64>" }

Example (Node.js)

const crypto = require('crypto');
 
function verifyWebhook(publicKeyB64, headers, rawBody) {
  const message = `${headers['x-xellar-event-id']}\n${headers['x-xellar-timestamp']}\n${rawBody}`;
 
  const keyObject = crypto.createPublicKey({
    key: Buffer.from(publicKeyB64, 'base64'),
    format: 'der',
    type: 'spki',
  });
 
  return crypto.verify(
    null,
    Buffer.from(message),
    keyObject,
    Buffer.from(headers['x-xellar-signature'], 'base64'),
  );
}
 
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyWebhook(process.env.XELLAR_WEBHOOK_PUBKEY, req.headers, req.body.toString());
  if (!ok) return res.status(401).send('Invalid signature');
  res.status(200).send('OK');
});

Best Practices

  • Always verify the signature before trusting an event.
  • Return 200 quickly and process asynchronously.
  • Use X-Xellar-Event-Id (or the JSON id field) for idempotency to avoid double-processing retries.
  • Branch on the type field to handle each event explicitly.