Custodial webhook events

Customer deposits, withdrawals, swaps, and gas-wallet alerts.

These events fire for Default (custodial) wallet activity. Payloads are a flat JSON object with an eventType field matching the Svix event type.

Configure endpoints under Dashboard → Custodial → Webhooks. See Webhooks for setup and Webhook events for signature verification.

Event catalog

Event typeWhen
CUSTOMER_DEPOSITA customer deposit is confirmed on-chain.
CUSTOMER_DEPOSIT_APPROVEDA flagged deposit was approved and credited.
CUSTOMER_DEPOSIT_REJECTEDA flagged deposit was returned to the sending address.
CUSTOMER_DEPOSIT_REDIRECTEDA flagged deposit was sent to a different address.
CUSTOMER_WITHDRAWA customer withdrawal is broadcast.
CUSTOMER_WITHDRAW_AWAITING_APPROVALA withdrawal needs manual approval (threshold / low liquidity).
CUSTOMER_FAILED_TO_WITHDRAWA withdrawal failed.
customer_swap.failedA customer swap failed after being created.
gas_wallet.low_balanceA gas wallet dropped below its configured threshold.

Test events

Send a sample of any event above from Dashboard → Webhooks → Send test event. Test payloads have the same shape as real ones, contain placeholder data (customerId: "test_customer", zeroed ids and hashes), and never touch balances. They carry a test flag, so your handler can skip them:

  • Flat payloads (every CUSTOMER_* event and customer_swap.failed) get "isTest": true.
  • gas_wallet.low_balance (a { type, data } envelope) gets "is_test": true at the top level.

Real events never include the flag, so a missing flag means the event is real.

To exercise the real flagged-deposit flow end to end on staging (CUSTOMER_DEPOSIT with isFlagged: true, then CUSTOMER_DEPOSIT_APPROVED / _REJECTED / _REDIRECTED), open a test customer under Dashboard → Custodial → Customers and turn on Require approval for every deposit. Every deposit to that customer is then held under Flagged Deposits for you to approve, reject or redirect, with flagReason.entityType set to MANUAL_REVIEW.


CUSTOMER_DEPOSIT

{
"eventType": "CUSTOMER_DEPOSIT",
"id": 12345,
"mint": { "id": 1, "symbol": "USDC", "chain": "ETHEREUM" },
"amount": "1000000",
"uiAmount": "1.00",
"usdAmount": "1.00",
"txHash": "0x…",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id",
"scannerUrl": "https://etherscan.io/tx/0x…",
"riskOfFunds": "VERY_LOW_RISK",
"isFlagged": false,
"swap": null
}

amount is atomic token units. riskOfFunds is a Scorechain severity string (for example HIGH_RISK, VERY_LOW_RISK) or NOT_ANALYZED when no scan ran.

When this deposit is the destination leg of a customer swap, swap is non-null instead of null:

"swap": {
"customerSwapId": "9f1e2b3a-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"idempotencyKey": "unique-per-swap",
"fromMint": { "id": 1, "symbol": "USDC", "chain": "SOLANA" },
"toMint": { "id": 4, "symbol": "ETH", "chain": "ETHEREUM" },
"amountIn": "5000000",
"estimatedAmountOut": "1950000000000000000",
"amountOut": "1948210000000000000",
"originTxHash": "5s2k…",
"destinationTxHash": "0x…",
"createdAt": "2026-01-01T00:00:00.000Z"
}

If you pay a deposit bonus on CUSTOMER_DEPOSIT, check swap first — swap arrivals are funded from the customer’s existing balance, not new outside money.


CUSTOMER_DEPOSIT_APPROVED

Emitted when a flagged deposit is approved. No on-chain transfer — there is no txHash or destinationAddress.

{
"eventType": "CUSTOMER_DEPOSIT_APPROVED",
"id": 12345,
"mint": { "id": 1, "symbol": "USDC", "chain": "ETHEREUM" },
"amount": "1000000",
"uiAmount": "1.00",
"usdAmount": "1.00",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id",
"flagReason": {
"score": 12,
"severity": "HIGH",
"entityType": "MIXER",
"entityName": "Example Mixer"
}
}

flagReason fields are nullable when the deposit was flagged before analysis was stored or the provider returned no entity. id matches the original CUSTOMER_DEPOSIT.


CUSTOMER_DEPOSIT_REJECTED

Emitted after the return transaction confirms on-chain.

{
"eventType": "CUSTOMER_DEPOSIT_REJECTED",
"id": 12345,
"mint": { "id": 1, "symbol": "USDC", "chain": "ETHEREUM" },
"amount": "1000000",
"uiAmount": "1.00",
"usdAmount": "1.00",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id",
"flagReason": {
"score": 12,
"severity": "HIGH",
"entityType": "MIXER",
"entityName": "Example Mixer"
},
"destinationAddress": "0xSender…",
"txHash": "0x…"
}

CUSTOMER_DEPOSIT_REDIRECTED

Same shape as rejected; destinationAddress is the operator-chosen address. Emitted after the transfer confirms on-chain. txHash may be omitted until broadcast completes.


CUSTOMER_WITHDRAW

{
"eventType": "CUSTOMER_WITHDRAW",
"id": 67890,
"mint": { "id": 1, "symbol": "USDC", "chain": "ETHEREUM" },
"amount": "1000000",
"uiAmount": "1.00",
"usdAmount": "1.00",
"txHash": "0x…",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id",
"scannerUrl": "https://etherscan.io/tx/0x…"
}

CUSTOMER_WITHDRAW_AWAITING_APPROVAL

{
"eventType": "CUSTOMER_WITHDRAW_AWAITING_APPROVAL",
"transactionId": 67890,
"status": "AWAITING_CONFIRMATION",
"reason": "MAIN_WITHDRAWAL_THRESHOLD",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id"
}

Approve the withdrawal in the dashboard (or via API) before it broadcasts. See Withdrawals & approvals.


CUSTOMER_FAILED_TO_WITHDRAW

{
"eventType": "CUSTOMER_FAILED_TO_WITHDRAW",
"id": 67890,
"mint": { "id": 1, "symbol": "USDC", "chain": "ETHEREUM" },
"amount": "1000000",
"txHash": "0x…",
"createdAt": "2026-01-01T00:00:00.000Z",
"customerId": "your-customer-id",
"reason": "Insufficient liquidity"
}

txHash is omitted when no broadcast occurred. This payload does not include uiAmount or usdAmount.


customer_swap.failed

Svix eventType is customer_swap.failed (not CUSTOMER_SWAP_FAILED). The JSON body repeats that value in eventType.

{
"eventType": "customer_swap.failed",
"customerId": "your-customer-id",
"swap": {
"customerSwapId": "9f1e2b3a-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"idempotencyKey": "unique-per-swap",
"fromMint": { "id": 1, "symbol": "USDC", "chain": "SOLANA" },
"toMint": { "id": 4, "symbol": "ETH", "chain": "ETHEREUM" },
"amountIn": "5000000",
"estimatedAmountOut": "1950000000000000000",
"amountOut": null,
"originTxHash": "5s2k…",
"destinationTxHash": null,
"createdAt": "2026-01-01T00:00:00.000Z"
},
"reason": "the swap provider could not route this pair",
"refundedAmount": "5000000",
"createdAt": "2026-01-01T00:05:00.000Z"
}

refundedAmount is atomic source-mint units, or null when nothing had been recognized at send time — poll GET /customer-swap/{swapId} to confirm a refund landed.


gas_wallet.low_balance

{
"type": "gas_wallet.low_balance",
"data": {
"chain": "ETHEREUM",
"address": "0x…",
"current_balance": "0.05",
"threshold": "0.10",
"warning": "Refunds and sweeps may fail if not topped up."
}
}

This event uses a type / data wrapper instead of top-level eventType.