Skip to content
Core Concepts

Idempotency

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.1
Authorization: Bearer kdv_live_…
Idempotency-Key: 8e1a2c30-f0a4-4c70-9c2d-7b5e3aef9201
Content-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:

ScenarioBehaviour
First callProcess normally, cache the response. Response includes header Idempotent-Replayed: false.
Replay, same key + same bodyReturn the cached response verbatim — status code, body, headers. Header Idempotent-Replayed: true.
Replay, same key + different bodyReturn 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 })

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.

  • Don’t reuse a key across logically different sends. A key represents a single intent. Two distinct messages → two keys.
  • Don’t generate a fresh key on every retry. The whole point of the header is to dedupe — a new key per attempt sends a new message per attempt.
  • Don’t use predictable keys (sequential integers, user_id + timestamp). Predictable keys risk collisions across services in the same org.
  • Don’t truncate or hash the key client-side. Pass the full string you generated.

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.

Errors

idempotency_key_reuse and idempotency_in_progress envelopes. Read →

Rate limits

Combine idempotent retries with 429 backoff. Read →

Send a message

The most common idempotency target. API →

Handle failed sends

When and how to retry async failures. Read →