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-chaintransactionand an off-chaincredit. Pending events (*.pending_confirmation) include the on-chaintransactiononly — 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"
}
}| Field | Description |
|---|---|
url | HTTPS endpoint that receives POST event payloads |
enabled | true when settings exist and deliveries are allowed |
updatedAt | Last time the URL was saved |
| Code | Meaning |
|---|---|
| 404 | No webhook settings for this app |
| 503 | Outbound webhooks not configured on the server |
Set / Update Settings
PUT /api/v1/webhooks/settings
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | A valid https URL |
{ "url": "https://your-app.com/webhook" }Delete Settings
DELETE /api/v1/webhooks/settings → { "status": "ok" }
| Code | Meaning |
|---|---|
| 404 | Settings 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
| Query | Default | Description |
|---|---|---|
page | 1 | Page number |
limit | 20 | Page size |
Delivery object
| Field | Type | Description |
|---|---|---|
id | string | Delivery attempt record UUID |
eventId | string | Same id as X-Xellar-Event-Id / payload id |
eventType | string | Event type string (e.g. deposit.invoice.paid) |
status | string | Delivery state (e.g. pending, success, failed) |
attemptCount | int | HTTP delivery attempts so far |
maxAttempts | int | Maximum retries |
lastHttpStatus | int | null | Last HTTP status from your server |
lastError | string | null | Last transport or response error |
deliveredAt | string | null | When your endpoint returned success |
createdAt | string | RFC3339 |
updatedAt | string | RFC3339 |
Receiving Events
Xellar sends a POST request to your configured URL with these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Xellar-Event-Id | Unique event ID (same value as the JSON id field) |
X-Xellar-Timestamp | Unix time in seconds |
X-Xellar-Signature | Base64 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": { }
}| Field | Description |
|---|---|
apiVersion | Webhook schema version. Currently 2025-06-22. |
id | Stable event identifier used for idempotency and signature verification. |
type | Event type string (see below). |
createdAt | UTC timestamp when the event was created. |
data | Event-specific payload. |
Event ID Format
The id field (and X-Xellar-Event-Id header) follow a predictable pattern per event type:
| Type | Event ID pattern |
|---|---|
account.created | account.created:<accountId> |
deposit.invoice.pending_confirmation | deposit.invoice.pending_confirmation:<invoiceId>:<txHash> |
deposit.wallet.pending_confirmation | deposit.wallet.pending_confirmation:<walletId>:<txHash> |
deposit.invoice.paid | deposit.invoice.paid:<invoiceId>:<txHash> |
deposit.wallet.credited | deposit.wallet.credited:<tokenTransferId> |
invoice.expired | invoice.expired:<invoiceId> |
invoice.failed | invoice.failed:<invoiceId>:<txHash> |
withdraw.created | withdraw.created:<withdrawId> |
withdraw.success | withdraw.success:<withdrawId> |
withdraw.failed | withdraw.failed:<withdrawId> |
webhook.test | Random UUID |
Event Types Overview
| Type | When it fires |
|---|---|
account.created | A custodial account is provisioned and wallet addresses are ready |
deposit.invoice.pending_confirmation | An invoice-matching on-chain transfer is detected (first webhook). Not final. No credit. |
deposit.wallet.pending_confirmation | A listen-wallet on-chain transfer is detected (first webhook). Not final. No credit. |
deposit.invoice.paid | After required block confirmations: invoice marked paid and off-chain balance credited (second webhook) |
deposit.wallet.credited | After required block confirmations: listen-wallet deposit credited — not matched to an invoice (second webhook) |
invoice.expired | An invoice passes its expiry time without payment |
invoice.failed | After confirmations: a payment is detected for an invoice address but the amount does not match |
withdraw.created | A withdrawal request is accepted and queued |
withdraw.success | A withdrawal completes on-chain |
withdraw.failed | A withdrawal fails |
webhook.test | You call POST /api/v1/webhooks/test |
Deposit webhooks (two steps)
Every matched deposit sends up to two outbound webhooks for the same transaction hash:
-
Detected (pending confirmation) —
deposit.invoice.pending_confirmationordeposit.wallet.pending_confirmation
Fired as soon as Xellar matches the on-chain transfer to an invoice or listen wallet.
data.confirmationStatusis always"pending". There is nocreditobject and your off-chain balance is unchanged. -
Confirmed (credited) —
deposit.invoice.paidordeposit.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 viacredit.
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 balanceUse 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
| Field | Type | Description |
|---|---|---|
id | string | Token UUID |
tokenCode | string | Token code (e.g. usdc-polygon-pos) |
symbol | string | Display symbol (e.g. USDT) |
decimals | number | Token decimal places |
credit (ledger credit applied to your off-chain balance)
| Field | Type | Description |
|---|---|---|
tokenCode | string | Token credited |
amount | number | Human-readable amount added to off-chain activeBalance |
referenceType | string | invoice or token_transfer |
referenceId | string | Invoice ID or token transfer ID |
transaction (on-chain transfer details — the blockchain payment that triggered the event)
| Field | Type | Description |
|---|---|---|
txHash | string | Transaction hash |
chainId | number | Chain ID |
networkCode | string | Network code (e.g. polygon-pos) |
from | string | Sender address |
to | string | Recipient address |
tokenAddress | string | Contract address (native coin may be empty) |
tokenSymbol | string | Token symbol from the chain event |
value | string | On-chain raw amount in smallest unit (string) |
valueDecimals | number | Decimals used for value |
blockchainType | string | e.g. evm, tron, solana |
account.created
Sent when MPC account creation completes and wallet addresses are available for all configured networks.
data fields
| Field | Type | Description |
|---|---|---|
appId | string | Your app UUID |
account | object | Created account details |
wallets | array | One entry per network wallet |
account object
| Field | Type | Description |
|---|---|---|
id | string | Account UUID |
subId | string | Your user identifier |
mpcSubId | string | MPC sub-account ID |
status | string | active |
poolType | string | dedicated or organization_owned |
wallets[] object
| Field | Type | Description |
|---|---|---|
walletId | string | Wallet UUID |
network | string | Network code |
address | string | Deposit address on that network |
addressType | string | e.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
walletsarray 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
| Field | Type | Description |
|---|---|---|
matchType | string | Always invoice |
confirmationStatus | string | Always pending |
appId | string | Your app UUID |
userId | string | End-user UUID linked to the invoice |
invoice | object | Invoice details (status is still pending) |
transaction | object | On-chain transfer that was seen |
token | object | Token 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
| Field | Type | Description |
|---|---|---|
matchType | string | Always wallet |
confirmationStatus | string | Always pending |
appId | string | Your app UUID |
wallet | object | Deposit wallet |
transaction | object | On-chain transfer that was seen |
token | object | Token metadata |
amount | number | Detected 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
| Field | Type | Description |
|---|---|---|
matchType | string | Always invoice |
appId | string | Your app UUID |
userId | string | End-user UUID linked to the invoice |
invoice | object | Paid invoice details |
transaction | object | On-chain transfer that triggered the event |
token | object | Token metadata |
tokenTransfer | object | Internal transfer record |
credit | object | Off-chain balance credit applied |
invoice object
| Field | Type | Description |
|---|---|---|
id | string | Invoice UUID |
status | string | paid |
amount | number | Expected amount |
amountInDecimals | string | Amount in smallest unit |
reference | string | Your invoice reference |
walletId | string | Deposit wallet UUID |
depositAddress | string | Address that received payment |
paidAt | string | UTC timestamp |
transactionHash | string | Matching 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
| Field | Type | Description |
|---|---|---|
matchType | string | Always wallet |
appId | string | Your app UUID |
wallet | object | Credited wallet |
transaction | object | On-chain transfer that triggered the event |
token | object | Token metadata |
tokenTransfer | object | Internal transfer record |
credit | object | Off-chain balance credit applied |
wallet object
| Field | Type | Description |
|---|---|---|
walletId | string | Wallet UUID |
address | string | Wallet address |
networkCode | string | Network code |
chainId | number | Chain 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
| Field | Type | Description |
|---|---|---|
appId | string | Your app UUID |
userId | string | End-user UUID |
invoice | object | Expired invoice details |
token | object | Token metadata |
invoice object
| Field | Type | Description |
|---|---|---|
id | string | Invoice UUID |
status | string | expired |
amount | number | Expected amount |
amountInDecimals | string | Amount in smallest unit |
reference | string | Your invoice reference |
walletId | string | Deposit wallet UUID |
depositAddress | string | Address that was assigned |
expiresAt | string | Original expiry timestamp |
bufferInSeconds | number | Grace buffer after expiry |
expiredAt | string | When 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
| Field | Type | Description |
|---|---|---|
userId | string | End-user UUID |
reason | string | Failure reason (currently wrong_amount) |
invoice | object | Invoice that was targeted |
transaction | object | On-chain transfer that failed to match |
token | object | Token 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
| Field | Type | Description |
|---|---|---|
appId | string | Your app UUID |
userId | string | End-user UUID |
withdraw | object | Withdrawal details |
token | object | Token metadata |
network | object | Target network |
withdraw object
| Field | Type | Description |
|---|---|---|
id | string | Withdrawal UUID |
status | string | pending |
amount | number | Requested amount |
netAmount | number | Amount sent to recipient after fees |
totalDeducted | number | Total deducted from off-chain balance |
withdrawGasFee | number | Gas fee component |
amountInDecimals | string | Amount in smallest unit |
recipientAddress | string | Destination address |
reference | string | null | Your reference |
withdrawWalletId | string | Hot wallet that broadcasts the transfer |
walletId | string | null | Ledger / deposit wallet deducted on per_wallet apps (omitted for per_app) |
withdrawTransactionFee | string | Fee mode (e.g. exclusive) |
txHash | string | null | On-chain hash (null until broadcast) |
createdAt | string | UTC timestamp |
updatedAt | string | UTC timestamp |
network object
| Field | Type | Description |
|---|---|---|
code | string | Network code |
chainId | number | Chain ID |
type | string | Blockchain 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
200quickly and process asynchronously. - Use
X-Xellar-Event-Id(or the JSONidfield) for idempotency to avoid double-processing retries. - Branch on the
typefield to handle each event explicitly.