Skip to main content
Webhooks tell your server when an account becomes ready, when a withdrawal moves, when winnings are paid and when a trade ends. They are signed with your key’s webhook secret and retried until your endpoint accepts them.

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.
ZeroID sets the URL on the key and delivers its secret; you can rotate the secret yourself. See Keys.

Events

Envelope and headers

Every delivery is a POST with a JSON body in the same envelope:
Amounts are strings in USDC atomic units (6 decimals). Every account, withdrawal and payout event names the account by 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.
Re-sending an account’s current destination changes nothing and sends no webhook.

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

The X-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.
The payload schemas and types are also exported from @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

The scheme is Stripe’s, except that the header is 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, completedAt and 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 2xx within 10 seconds. Do slow work after you respond.
  • Retries. A timeout, network error, 5xx, 408, 429 or any other 4xx except those below is retried about every 2 minutes, up to 10 attempts. A 400, 404, 410, 422 or a redirect stops delivery of that event. See Retries & redelivery.
  • Final answers. Delivery stops at a 2xx, or at 400, 404, 410 or 422, 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:manage scope, GET /v1/webhooks/events lists the key’s events and their delivery state, and POST /v1/webhooks/events/{eventId}/redeliver sends one again.

Next

Errors

Every code a partner integration can meet.

Sandbox

Test the whole flow on staging.