Idempotency
Network failures, container restarts, client-side timeouts — anything
that leaves a POST in an ambiguous “did it land?” state is fixed
with the Idempotency-Key header.
When to use
Section titled “When to use”Use an idempotency key on every POST that has side effects you can’t
afford to double:
POST /v1/messages— duplicate sends spam your customer.POST /v1/webhook_subscriptions/{id}/secrets— duplicate rotations invalidate the wrong secrets.POST /v1/contacts/bulk_label— duplicate attaches are no-ops but duplicate detaches followed by an attach create a confusing audit trail.
GET requests are naturally idempotent — no key needed.
How it works
Section titled “How it works”Pass a client-generated key in the header:
POST /v1/messages HTTP/1.1Authorization: Bearer kdv_live_…Idempotency-Key: 8e1a2c30-f0a4-4c70-9c2d-7b5e3aef9201Content-Type: application/json
{ "to": "+628…", "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 the same organisation.
Server semantics
Section titled “Server semantics”The key is scoped to your organisation. Kirim 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. Add Idempotent-Replayed: false to the response headers. |
| Replay with same key + same body | Return the cached response verbatim, including status code and body. Add Idempotent-Replayed: true. |
| Replay with same key + different body | Return 422 idempotency_key_reuse. Don’t lie about the side effect outcome. |
| In-flight (lock held by an earlier call) | Wait up to 30 s for the result. On timeout, return 409 idempotency_in_progress. Retry later with the same key. |
Worked example
Section titled “Worked example”import { randomUUID } from 'node:crypto'
async function sendOnce(payload: SendMessageBody): Promise<MessageResource> { // Generate ONE idempotency key per logical "send attempt". // Reuse it across network-level retries; generate fresh for a new send. const key = randomUUID()
for (let attempt = 0; attempt < 3; attempt++) { const res = await fetch('https://api-kckit.kirim.chat/v1/messages', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': key, // SAME key across retries }, body: JSON.stringify(payload), })
if (res.ok) return (await res.json()).data if (res.status === 409) { // still processing — back off await sleep(1000 * (attempt + 1)) continue } if (res.status === 422) { // key reuse with different body throw new Error('Idempotency key collision — caller bug') } if (res.status === 429) { // rate-limited const delay = Number(res.headers.get('retry-after') ?? 1) await sleep(delay * 1000) continue } // 4xx other than 409/422/429 — don't retry, surface to caller. throw await res.json() } throw new Error('Idempotency retry budget exhausted')}TTL behaviour
Section titled “TTL behaviour”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.
Anti-patterns
Section titled “Anti-patterns”- Don’t reuse a key across logically different sends. A single key represents a single intent. If you’re sending two distinct messages, generate two keys.
- Don’t generate a fresh key on every retry. The whole point of the key is to dedupe — a new key per attempt sends a new message per attempt.
- Don’t use predictable keys (sequential integers, user id + timestamp). Even within your own org, predictable keys risk collisions across services.
- Don’t truncate or hash the key client-side. Pass the full string you generated.
Endpoint coverage
Section titled “Endpoint coverage”Idempotency-Key is honoured on every POST in the v1 surface.
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.
What’s next
Section titled “What’s next”- Errors — the full error envelope including
idempotency_key_reuseandidempotency_in_progresssemantics. - API Reference → POST /v1/messages — the most common idempotency target.