Webhook payloads (October 2026)
The webhook envelope, the events no longer sent, and every payload field that changed, with what your integration must update.
Release: webhook payloads · Date: 2026-10-14 · Environment: sandbox first, then production
Corrected on 2026-10-15
Some payloads described here were corrected afterwards: the resulting balances, network codes, treasury amounts, sweep_tx_hash, and the new user.funds.released. Read Webhook payload fixes (2026-10-15) for what changed since this page.
This release puts every webhook on one documented catalogue. Each event now has a fixed data shape, closed reason_code values and a resource it's ordered by. Fields that exposed internal routing or ids are gone. The full catalogue is in Webhooks.
These changes take effect as soon as the release reaches your environment. Webhook payloads have no deprecated aliases.
Before the release reaches your environment
Check these first. They break integrations that don't change:
- Read
event_type. The top-leveleventkey is gone. - Order events per resource with
sequence_number, not per account. - Stop waiting for
cosigner.rejected,withdrawal.queued,withdrawal.queue.processed,user.wallet.address_createdanduser.deposit.return_initiated. - Read withdrawal ids from
withdrawal_id.atx_idandwithdraw_idare gone. - Read
reason_code, notreason, ontreasury.withdrawal.failedandtreasury.batch.failed. - Don't read
emailfromuser.withdrawal.verification_code_issued.
Envelope
| Change | Before | After | What you must change |
|---|---|---|---|
| Event name | Sent twice, as event and event_type | Only event_type | Read event_type. |
sequence_number | One counter per customer account | One counter per resource (resource_type + resource_id), starting at 1 | Store the last number per resource, not per account. Compare numbers only between events of the same resource. |
| Body keys | event plus the envelope | Exactly event_id, event_type, occurred_at, resource_type, resource_id, sequence_number, data | Nothing else to read at the top level. |
Resources that already had events keep counting from their highest number. A new resource starts at 1.
Resource changes
Some events now use a different resource. Their sequence_number follows the new resource.
| Event | Before | After |
|---|---|---|
ledger.wallet.debited for a withdrawal | withdrawal, the withdrawal id | wallet, the wallet id |
ledger.wallet.debited and ledger.wallet.credited for a transfer | transfer, the transfer id | wallet, the wallet id |
ledger.wallet.credited with reason withdrawal_reversal | withdrawal, the withdrawal id | wallet, the wallet id |
custody.withdrawal.requested | withdrawal, an internal custody id | withdrawal, the public withdrawal id |
user.wallet.created | wallet, an internal custody wallet id | wallet, the wallet id the API returns |
gas.primary.threshold_breached, gas.backup.activated, gas.backup.threshold_breached | wallet, the gas wallet id | gas, primary or backup |
Events no longer sent
| Event | Listen to instead |
|---|---|
cosigner.rejected | The outcome of the refused transaction, with reason_code: policy_rejected: treasury.withdrawal.failed for a withdrawal, treasury.batch.failed for a batch, user.deposit.failed for a deposit. Other refusals send no event. |
withdrawal.queued | custody.withdrawal.requested, which is sent for the same withdrawal. |
withdrawal.queue.processed | The events of each withdrawal (custody.withdrawal.executed, treasury.withdrawal.failed). Read the withdrawal for its status. |
user.wallet.address_created | The addresses list in user.wallet.created. |
user.deposit.return_initiated | ledger.wallet.reversed when the return starts, and user.deposit.returned when it settles. |
Some cases of existing events are no longer sent either:
| Case | Before | After |
|---|---|---|
| High-risk deposit | user.deposit.screening_failed and user.deposit.failed with high_risk_kyt | Only user.deposit.screening_failed. |
| Blocked deposit | user.deposit.screening_failed and user.deposit.failed, both with blacklisted_address | user.deposit.screening_failed with blacklisted, and user.funds.frozen with screening_failed. |
| Deposit that can't be screened | user.deposit.screening_failed or user.deposit.failed with screening_unavailable | No event. The deposit stays held for manual review. |
| Treasury deposit without a screening result | treasury.deposit.screened with risk_result: "unknown" | No event. |
| Wallets that aren't your users' wallets | user.wallet.created for every new wallet, including treasury wallets | Only for your users' wallets. |
Field changes by event
"Removed" fields are no longer sent. Ignore them if your code still expects them.
Verification codes
| Event | Before | After | What you must update |
|---|---|---|---|
user.withdrawal.verification_code_issued | Carried email when the code arrived by email | No email. Adds attempts_remaining (3) when the treasury pays directly. | Don't read email. Read attempts_remaining when present. |
Deposits
| Event | Before | After | What you must update |
|---|---|---|---|
user.deposit.screening_failed | reason_code: high_risk or elevated_risk by risk band, blacklisted_address, screening_unavailable | reason_code: elevated_risk (medium risk), high_risk, severe_risk, blacklisted | Map the new values. Severe risk now has its own value. |
user.deposit.failed | Sent for screening failures, with wallet_id, deposit_id, reason_code, tx_hash | Sent only when a signing policy refuses the deposit's transaction: deposit_id and reason_code: policy_rejected | Handle it as a stuck deposit, not a screening result. |
user.deposit.swept_to_treasury (the event with batch_id) | deposit_id, batch_id, forward_tx_name, tx_hash, total_amount, treasury_share, treasury_amount, forward_amount, currency, destination_address, deposit_tx_hash, deposit | wallet_id, deposit_id, sweep_tx_hash, destination, batch_id | Read the transaction from sweep_tx_hash. Read amounts from the deposit. |
Wallets
| Event | Before | After | What you must update |
|---|---|---|---|
user.wallet.created | wallet_id was an internal custody wallet id; wallet_type always virtual; no addresses | wallet_id is the API wallet id; wallet_type is virtual or real; adds addresses with address_id, currency, network, address | Match wallets by the API id. Read the deposit addresses from addresses. |
Withdrawals
| Event | Before | After | What you must update |
|---|---|---|---|
treasury.withdrawal.requested | atx_id, routing, currency, amount, destination_address, user_id, pending_id (fiat: withdrawal_type, no address) | withdrawal_id, amount, currency, address (empty for fiat) | Read withdrawal_id and address. |
treasury.withdrawal.approved | atx_id, routing, user_id | withdrawal_id | Read withdrawal_id. |
treasury.withdrawal.executed | atx_id, routing, currency, amount, destination_address, user_id, tx_hash | withdrawal_id, amount, currency, address, tx_hash | Read withdrawal_id and address. |
treasury.withdrawal.failed | atx_id, user_id, reason (cancel also routing, currency, amount) | withdrawal_id, reason_code | Read withdrawal_id and reason_code. policy_rejected is a new value. |
custody.withdrawal.requested | atx_id (an internal custody id), safe_request_id, pending_id, currency, amount, treasury_wallet_address, final_destination_address, user_id | withdrawal_id (the public id), amount, currency | Read withdrawal_id. Read the destination from the withdrawal. |
custody.withdrawal.executed | withdraw_id, safe_request_id, pending_withdrawal_id, currency, amount, treasury_wallet_address, final_destination_address, user_id, tx_status, tx_hash | withdrawal_id, amount, currency, tx_hash | Read withdrawal_id. Read the status from the withdrawal. |
Ledger
| Event | Before | After | What you must update |
|---|---|---|---|
ledger.wallet.debited for a withdrawal | One event per withdrawal: withdraw_id, user_id, amount, asset, destination_address, reason, note, tx_hash, and the user's total balances | One event per wallet that paid: wallet_id, amount (that wallet's share), currency, reason: withdrawal, and that wallet's balances | Sum the events of one withdrawal per wallet. Read currency, not asset. Match the withdrawal through its debited_wallets. |
ledger.wallet.debited and ledger.wallet.credited for a transfer | available, held and balance as JSON numbers | Decimal strings | Parse balances as decimal strings. |
ledger.wallet.credited with reason withdrawal_reversal | withdraw_id, user_id, note, plus the common fields | withdrawal_id plus the common fields | Read withdrawal_id. |
Treasury
| Event | Before | After | What you must update |
|---|---|---|---|
treasury.batch.executed | batch_id, vault_id, asset, deposit_count, transfers, transfer_summaries, deposits, tx_hashes | batch_id, amount, currency, transactions (tx_hash, amount, currency) | Read the total from amount and the hashes from transactions. |
treasury.batch.failed | batch_id, vault_id, asset, deposit_count, reason, failed_count, sometimes succeeded_count, deposits, error, currency | batch_id, reason_code, succeeded_count, failed_count | Read reason_code. policy_rejected is a new value. |
treasury.deposit.screened | risk_result: a risk band or unknown | risk_result: low, medium, high or severe | Map the new values. |
treasury.wallet.flagged | reason_code: contamination_detected | reason_code: contamination | Map the new value. |
treasury.wallet.created | wallet_id, vault_id, previous_wallet_id, reason, triggered_by_transaction_id | wallet_id, addresses | Read the old wallet from treasury.wallet.rotated. |
treasury.funds.frozen | Also wallet_id and note | amount, currency, reason_code, scope, hold_id | Read the wallet from resource_id. |
treasury.funds.released | reason_code: wallet_rotated_after_contamination, plus wallet_id and new_wallet_id | reason_code: contamination, no wallet ids | Match it to the frozen event by hold_id. |
treasury.contamination.remediated | Also new_wallet_id, sweep_results, total_swept, total_failed; sweep_tx_hash absent when nothing moved | wallet_id, amount, currency, sweep_tx_hash (empty when nothing moved) | Read the new wallet from treasury.wallet.rotated. |
treasury.wallet.rotated | wallet_id, new_wallet_id, triggering_transaction_id, contaminated_asset, swept_assets | wallet_id, new_wallet_id, new_addresses | Read the new addresses from new_addresses. |
Gas
| Event | Before | After | What you must update |
|---|---|---|---|
gas.primary.threshold_breached | sponsor_wallet_id, vault_id, balance, threshold, context | source: primary, balance, threshold, currency, remaining_transactions (when estimated) | Read currency and remaining_transactions. |
gas.backup.threshold_breached | backup_gas_wallet_id, vault_id, balance, threshold (when set), context | source: backup, balance, threshold, currency, remaining_transactions (when estimated) | Same as above. |
gas.backup.activated | sponsor_wallet_id, vault_id, balance, context | reason: primary_depleted | Read reason. |
Unchanged
These events keep their payload: user.transfer.verification_code_issued, user.deposit.received, user.deposit.screening_passed, user.deposit.returned, user.deposit.suspended_address_received, custody.deposit.confirmed, ledger.wallet.added, ledger.wallet.credited with reason screening_cleared, ledger.wallet.reversed, ledger.transfer.failed, treasury.deposit.received, and user.deposit.swept_to_treasury without batch_id. Only the envelope changes for them.
Webhook payload fixes (2026-10-15)
Corrections to the October 2026 webhook payloads: one balance rule for every ledger and funds event, canonical network codes, real amounts, on-chain hashes only, the new user.funds.released event and the end of the legacy code body.
Contracts v1 (2026-10-12)
Every breaking change in the contracts v1 release of the Crypto as a Service API, what it replaces, and what your integration must change.