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.
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"
}
}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" }
Send a Test Event
POST /api/v1/webhooks/test (no body). Queues a webhook.test delivery to your URL.
List Deliveries
GET /api/v1/webhooks/deliveries — query page (default 1), limit (default 20, max 100).
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.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.paid | An invoice receives the exact expected payment |
deposit.wallet.credited | A subscribed wallet receives a deposit (not matched to an invoice) |
invoice.expired | An invoice passes its expiry time without payment |
invoice.failed | 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 |
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 |
transaction (on-chain transfer details)
| 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 | Raw amount in smallest unit (string) |
valueDecimals | number | Decimals used for value |
blockchainType | string | e.g. evm, tron, solana |
credit (ledger credit applied to your balance)
| Field | Type | Description |
|---|---|---|
tokenCode | string | Token credited |
amount | number | Human-readable amount |
referenceType | string | invoice or token_transfer |
referenceId | string | Invoice ID or token transfer ID |
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.paid
Sent when an incoming transfer exactly matches a pending invoice amount.
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 |
token | object | Token metadata |
tokenTransfer | object | Internal transfer record |
credit | object | 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
Sent when a deposit is credited to a listen wallet and is not matched to an invoice.
data fields
| Field | Type | Description |
|---|---|---|
matchType | string | Always wallet |
appId | string | Your app UUID |
wallet | object | Credited wallet |
transaction | object | On-chain transfer |
token | object | Token metadata |
tokenTransfer | object | Internal transfer record |
credit | object | 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.
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 balance |
withdrawGasFee | number | Gas fee component |
amountInDecimals | string | Amount in smallest unit |
recipientAddress | string | Destination address |
reference | string | null | Your reference |
withdrawWalletId | string | Source wallet UUID |
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_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_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_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.