Errors
idempotency_key_reuse and idempotency_in_progress envelopes.
Read →
Network blips, container restarts, client-side timeouts — anything
that leaves a POST in an ambiguous “did it land?” state is fixed
with the Idempotency-Key header.
Use an idempotency key on every POST whose side effect you can’t
afford to double:
POST /v1/{phone_number_id}/messages — duplicate sends spam your customer.POST /v1/webhook_subscriptions/{id}/secrets — duplicate rotations invalidate the wrong secrets.POST /v1/{phone_number_id}/contacts/bulk_label — duplicate detach + attach creates a confusing audit trail.POST /v1/webhook_deliveries/{id}/replay — duplicate replays hammer your endpoint.GET requests are naturally idempotent. PATCH and DELETE operate
by primary key — re-applying them lands on the same final state. The
header is ignored on those methods.
Pass a client-generated key in the header:
POST /v1/106540352242922/messages HTTP/1.1Authorization: Bearer kdv_live_…Idempotency-Key: 8e1a2c30-f0a4-4c70-9c2d-7b5e3aef9201Content-Type: application/json
{ "messaging_product": "whatsapp", "to": "+628111222333", "type": "text", "text": { "body": "Halo!" }}The key is a free-form string. Recommended format: UUIDv4. Maximum length 255 characters. Keep it cryptographically random to avoid collisions across consumers within your organisation.
The key is scoped to your organisation. kirim.dev hashes the request
body (sha256(method + path + canonical_json(body))) and stores the
pair in Redis for 24 hours:
| Scenario | Behaviour |
|---|---|
| First call | Process normally, cache the response. Response includes header Idempotent-Replayed: false. |
| Replay, same key + same body | Return the cached response verbatim — status code, body, headers. Header Idempotent-Replayed: true. |
| Replay, same key + different body | Return 422 idempotency_key_reuse. Refusing to lie about which side effect ran. |
| In-flight (lock held by earlier call) | Wait up to 30 s for the original to finish. On timeout, return 409 idempotency_in_progress. Retry with the same key. |
import { Kirim } from '@kirimdev/sdk'import { randomUUID } from 'node:crypto'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })const phone = kirim.phoneNumbers('106540352242922')
// The SDK auto-generates an Idempotency-Key for every POST.// Network-level retries inside ONE call reuse it automatically.await phone.messages.send({ messaging_product: 'whatsapp', to: '+628111222333', type: 'text', text: { body: 'Halo!' },})
// For application-level retries that survive a process restart,// pin the key yourself so it persists in your job record:const job = await jobs.claim() // { id, payload, idempotencyKey }const key = job.idempotencyKey ?? randomUUID()await jobs.update(job.id, { idempotencyKey: key })
await phone.messages.send(job.payload, { idempotencyKey: key })KEY=$(uuidgen)
send() { curl -sS -w "\n%{http_code}" \ -X POST "https://api.kirim.dev/v1/106540352242922/messages" \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "messaging_product": "whatsapp", "to": "+628111222333", "type": "text", "text": { "body": "Halo!" } }'}
for attempt in 1 2 3; do RESP=$(send) STATUS=$(echo "$RESP" | tail -n1) case "$STATUS" in 2*) echo "$RESP" | sed '$d'; break ;; 409) sleep $((attempt * 1)); continue ;; # in-flight 422) echo "key reuse — caller bug" >&2; exit 1 ;; # body mismatch *) echo "$RESP" | sed '$d'; exit 1 ;; esacdoneimport uuid, time, httpx
def send_once(client: httpx.Client, payload: dict, key: str | None = None): key = key or str(uuid.uuid4()) # one key per logical send attempt for attempt in range(3): resp = client.post( "https://api.kirim.dev/v1/106540352242922/messages", json=payload, headers={"Idempotency-Key": key}, ) if resp.is_success: return resp.json() if resp.status_code == 409: # still processing time.sleep(attempt + 1) continue if resp.status_code == 422: # body mismatch raise RuntimeError("Idempotency-Key reuse with different body") resp.raise_for_status() raise RuntimeError("Idempotency retry budget exhausted")Cached responses expire 24 hours after the first call. After that, the same key + same body executes a fresh side effect (a new message gets sent). Pick keys that won’t be replayed beyond a day.
For longer dedup windows, persist your own “have I sent message X yet?” record (keyed by your own request id) and consult it before calling kirim.dev.
user_id + timestamp).
Predictable keys risk collisions across services in the same org.Idempotency-Key is honoured on every POST in the v1 surface
— path-scoped (/v1/{phone_number_id}/messages,
/v1/{phone_number_id}/contacts/bulk_label, …) and root-level
(/v1/labels, /v1/webhook_subscriptions, …) alike.
It is ignored on GET, PATCH, and DELETE — those are either
naturally idempotent or operate by primary key, so re-applying them
produces the same final state without the cache layer.