Network failures happen mid-flight. Idempotency lets you retry a write without risking a duplicate trade. The API supports two idempotency mechanisms — the Stripe-styleDocumentation Index
Fetch the complete documentation index at: https://docs.kash.bot/llms.txt
Use this file to discover all available pages before exploring further.
Idempotency-Key header (HTTP-level) and the clientRequestId body field (domain-level). They serve slightly different purposes; you can use either or both.
Idempotency-Key header (recommended)
- First request: processed normally. The response is cached server-side under the key.
- Same key, same body: the cached response is returned with
_meta.idempotent = true. No new trade is created. - Same key, different body:
409 IDEMPOTENCY_KEY_CONFLICT. The first request “owns” the key for 24 hours. - TTL: 24 hours. After that, the key can be reused (but you should generate fresh ones).
metadata field — see Body comparison rules below.
When to use it
- Network timeouts on
POST /v1/trades— retry with the same key, get the same trade back. - Distributed retry layers (queues, schedulers) — pin the key to the logical job id.
- “Was this trade already sent?” — replay returns the same response, regardless of how many times you ask.
clientRequestId body field
clientRequestId is stored alongside the trade and used for trade-level idempotency:
- First request with a given
clientRequestId: trade is created. - Second request, same
clientRequestId, same trade body: the existing trade row is returned. No new trade. - Second request, same
clientRequestId, different trade body:409 IDEMPOTENCY_KEY_CONFLICT.
- 1 to 128 characters
- Allowed:
A-Z a-z 0-9 _ - : .
When to use clientRequestId over Idempotency-Key?
Pick clientRequestId when:
- Your idempotency key is meaningful and not random — e.g.
trade-{userId}-{marketId}-{tradeBatchId}. The key persists with the trade row, so you can look it up later. - You want the idempotency to survive longer than 24 hours (e.g. nightly batch jobs that must never double-create).
- You’re not in HTTP-retry land — you’re at the domain layer, deciding whether to send a trade in the first place.
Idempotency-Key (or both) when:
- You want the protection of “if this exact request was just made, give me the cached response” without thinking about it.
- You have transient HTTP retries between a client and the API (proxies, retry middleware, etc.).
Body comparison rules
For both mechanisms, the body comparison treats these fields as identity-defining:marketIdoutcomeIndexamountsidemetadata(compared as a sorted, shallow-equalityRecord<string, string>)
409 IDEMPOTENCY_KEY_CONFLICT.
These fields are NOT part of identity:
clientRequestId(it IS the identity for the second mechanism, not part of the body comparison)- Server-derived fields (the response always reflects the original trade, including its server-assigned
id,correlationId,createdAt, etc.)
Detecting an idempotent replay
Every response fromPOST /v1/trades includes _meta.idempotent:
idempotent: true, the trade was created on a previous request — your retry simply rediscovered it.
High-value confirmation interaction
If your trade hits the high-value threshold:- First request:
202 Acceptedwith a one-time confirmation token. - Same
Idempotency-Key, same body, before confirm:202again with the same trade and the same token. - Same
Idempotency-Key, same body, after confirm:200withidempotent: true. The trade is already past the gate.
GET /v1/trades/{id}) — its status will be pending (or further along).
Error envelope on conflict
- Replay the original body — you’ll get the original response back.
- Generate a new key — you’ll create a new trade.
What’s not idempotent
POST /v1/trades/{id}/confirm— the confirmation token is single-use by design. A second confirm with the same token returns409 CONFIRMATION_TOKEN_USED. (The server-side flip frompending_confirmation → pendingis itself atomic — a race between two concurrent confirms can’t both succeed.)POST /v1/webhooks/events/{id}/redeliver— explicitly not idempotent at the HTTP layer. Each call enqueues another delivery attempt (capped at 5 per event to prevent amplification). The customer’s webhook endpoint dedupes onX-Kash-Event-Id.POST /v1/auth/api-keys/me/webhook-secret/rotate— each call rotates the secret. Don’t retry blindly; check the rotated-at timestamp first.
Next
Pagination
Cursor-based pagination on every list endpoint.
ETags
If-None-Match short-circuits — don’t pay quota for unchanged data.