Who receives what
- Account, withdrawal and payout events go to every active (not revoked, not expired) partner key of your organisation that has a webhook URL. While two keys have URLs, for example during a key rotation, each receives every event under a different event id, even when both keys share a URL.
- Trade events go only to the key that placed the trade, as for any API key.
Events
Envelope and headers
Every delivery is aPOST with a JSON body in the same envelope:
accountId and by your externalId.
Account events
account.active
The account is ready. Send USDC on Solana to depositAddress to fund it.
account.provisioning_failed
The account could not be created, for good. reason is signer_missing, no_solana_wallet, privy_refused, privy_identity_mismatch or privy_user_already_linked (see Accounts). Release the account and create it again, which gives it a new accountId, and contact ZeroID support if it recurs.
account.destination_changed
The destination was changed with PATCH /v1/partner/accounts/{accountId}. Withdrawals are held for 24 hours from changedAt. If you did not make the change, suspend the account and contact ZeroID support.
account.released
Your link to the account ended. No further webhooks arrive for it, apart from a destination change made before the release that is delivered after it.
Withdrawal events
Every withdrawal event carries these fields:amountAtomic is the amount requested. For what deliveredAmountAtomic and minimumDeliveredAtomic mean on each network, see Amounts delivered.
withdrawal.submitted
Adds "status": "pending" and submittedAt.
withdrawal.completed
Adds "status": "completed", transactionHash (the Solana transaction that sent the USDC, or null if not recorded), destinationTransactionHash (the transaction that delivered it: on Arc for an Arc withdrawal, otherwise the same Solana transaction) and completedAt.
withdrawal.failed
Adds status (failed or cancelled, as GET .../withdrawals/{withdrawalId} returns it), errorCode, errorMessage and failedAt.
Payout events
payout.completed
Winnings from a resolved market were paid into the account. amountAtomic is what arrived in the wallet. This is how you learn that an account’s balance rose after a resolution.
Trade events
trade.completed, trade.failed and trade.rejected have the same payloads as for any API key: tradeId, marketId, chainRef, outcomeIndex, amount (decimal USDC), side, status and your metadata, plus txHash and tokensOut on completion, or errorCode and errorMessage on failure or rejection. See Webhooks overview.
Trade payloads do not carry accountId or externalId. Put your own ids in the trade’s metadata when you create it.
A partner key has no high-value confirmation step, so it never receives trade.confirmation-required.
Verify every delivery
TheX-Kash-Signature header is t=<unix-ms>,v1=<hex>: an HMAC-SHA256 of <t>.<raw request body> under your webhook secret. Reject a delivery whose signature does not match or whose timestamp is more than five minutes from your clock. During a secret rotation the header carries two v1= values; accept the delivery if either matches.
Verify against the raw body. Parsed and re-serialised JSON does not produce the same bytes.
With the SDK
From@kashdao/sdk 0.4.0, kash.webhooks.constructEvent verifies the signature and parses every partner event type, as well as the trade events, into a typed event. An event type the SDK does not know yet does not throw: it comes back as an unknown event, which isUnknownWebhookEvent recognises, so acknowledge it and handle it after you upgrade. kash.webhooks.verifySignature returns { valid, reason } if you only need the check.
Versions before 0.4.0 parse only the trade events and refuse the partner events with
KashValidationError. With an earlier version, verify partner deliveries with kash.webhooks.verifySignature and parse the body yourself, as in By hand.@kashdao/sdk/webhooks (AccountActivePayloadSchema, WithdrawalFailedPayload and so on), for validating one payload on its own. The same schemas are in @kashdao/sdk/schemas.
If you are upgrading a 0.3 handler: in 0.4.0
constructEvent can return an unknown event, so check isUnknownWebhookEvent(event) before you read event.data, and WebhookEvent includes the eight partner event types, so a switch with an exhaustiveness check on the four trade types needs the partner cases or a default branch.By hand
X-Kash-Signature and the timestamp is in milliseconds. Python, Go and Rust versions: Verifying signatures.
Delivery
- At least once. An event can arrive more than once. Process each event id once, and store the ids you have processed.
- No ordering guarantee, even for one key. Up to 5 deliveries from one batch can be sent at once, events reach the delivery queue from several parts of the system with no common order, and an event sent again later (a redelivery you request, or one recovered after it failed to queue) arrives after newer ones. Compare the timestamps in the payloads (
activatedAt,changedAt,completedAtand so on) rather than the order of arrival, and treat each event as a prompt to read the current state from the API (GET /v1/partner/accounts/{accountId},GET .../withdrawals/{withdrawalId},GET /v1/balance). - Answer
2xxwithin 10 seconds. Do slow work after you respond. - Retries. A timeout, network error,
5xx,408,429or any other4xxexcept those below is retried about every 2 minutes, up to 10 attempts. A400,404,410,422or a redirect stops delivery of that event. See Retries & redelivery. - Final answers. Delivery stops at a
2xx, or at400,404,410or422, which the API treats as final and does not retry. Fix a failing endpoint quickly, and replay anything that ran out of attempts. - Replay. With the
webhooks:managescope,GET /v1/webhooks/eventslists the key’s events and their delivery state, andPOST /v1/webhooks/events/{eventId}/redeliversends one again.
Next
Errors
Every code a partner integration can meet.
Sandbox
Test the whole flow on staging.