Skip to main content
Webhooks are how the API tells your code that something happened — a high-value trade needs confirmation, a trade completed on-chain, a trade failed terminally. They’re the push-based alternative to polling GET /v1/trades/{id}.

When you’ll get webhooks

Each API key can carry an optional webhook_url. When set, the API POSTs trade.* events to that URL throughout the trade lifecycle: Set webhook_url in the webapp’s Settings → API Keys page when issuing or editing a key, or programmatically via the admin CLI.

Wire shape

The HTTP body is JSON:

data for trade.confirmation-required

The token itself is NOT in the webhook — it’s only returned in the synchronous 202 response from POST /v1/trades. This is intentional: a leaked webhook log can’t be used to confirm a trade.

data for trade.completed

tokensOut is in WAD (18 decimals) as a decimal string.

data for trade.failed

errorCode is the same machine-readable code surface as the REST API error codes. errorMessage is sanitised — no stack traces, no internal URLs.

data for trade.rejected

Same shape as trade.failed, but the trade never reached the chain — the risk engine rejected it pre-execution. errorCode carries the risk evaluation’s rejection outcome; errorMessage is the sanitised rejection reason. Rejection codes come from trade risk evaluation (lowercase snake_case, e.g. trade_too_large, daily_limit_exceeded) — they are a separate namespace from the REST API error codes.

The metadata field

Every trade.* payload carries the metadata bag from the trade’s create call. Use it to route handlers without an extra GET /v1/trades/{id} round-trip:
Constraints (set at trade-create time): max 10 keys, key chars [a-zA-Z0-9_\-.] (1-64), value max 500 chars, strings only.

Headers

  • X-Kash-Signature — HMAC of the body. Always verify. See Verifying signatures.
  • X-Kash-Event-Id — same as the body’s id. Dedupe on this.
  • X-Kash-Api-Version — same as the body’s apiVersion.

Delivery semantics

  • At-least-once. Network failures and 5xx responses trigger automatic retries with exponential backoff over ~24 hours. Your handler MUST be idempotent — dedupe on X-Kash-Event-Id.
  • FIFO per key. Events for the same API key are delivered in order, never overlapping. Cross-key concurrency is unlimited.
  • Distributed circuit breaker. If your endpoint is consistently failing, our breaker opens and pauses delivery for that endpoint specifically — other customers’ webhooks are unaffected.
  • Terminal failure after 24h. If retries can’t get through (your endpoint is permanently broken), the event is marked terminally failed. You can manually replay via POST /v1/webhooks/events/{id}/redeliver once you’ve fixed the receiver.
Customer endpoint requirements:
  • Respond fast. Aim for sub-2s response times. We hard-timeout at 10s.
  • Respond 2xx to acknowledge. Anything else is a failure that triggers retry.
  • Drain the request body. Even if you decide not to process it; otherwise the connection may not close cleanly.

Discovery & inspection

Use GET /v1/webhooks/events to list deliveries and inspect their state — handy for debugging “did the webhook fire?” without instrumenting your receiver:
The response carries the per-event delivery state: attempts, lastAttemptedAt, lastStatusCode, lastFailureCode, terminalFailureAt, lastErrorMessage. The webapp’s Settings → Webhook Inspector page renders the same data graphically.

Building a webhook receiver

Minimal Node.js example (Express):
The TypeScript SDK provides a one-line verifier and parser:

Next

Verifying Signatures

Stripe-compatible verification recipe. One-line constant change from any Stripe library.

Retries & Redelivery

Backoff schedule, terminal failure, manual replay.

Secret Rotation

Rotate without dropping deliveries — the 7-day overlap window.