تخطَّ إلى المحتوى

Idempotency

هذا المحتوى غير متوفر بلغتك بعد.

Networks time out after the work has already happened. Your job queue sees no response, retries, and a second WhatsApp message arrives at a real customer: they see a duplicate, and you are billed for a second conversation.

Send an Idempotency-Key and that cannot happen.

Terminal window
curl https://k-message.kerneltics.com/v1/messages \
-H "Authorization: Bearer km_live_YOUR_KEY" \
-H "Idempotency-Key: order-1001-shipped" \
-H "Content-Type: application/json" \
-d '{"to":"+966500000000","type":"text","text":{"body":"Your order has shipped."}}'

For a given organization and key, the first request executes and every later one replays its stored response. The replay carries Idempotency-Replayed: true and the original status code and body, so your code does not need a special case for it.

Results are kept for 24 hours, which covers any sane retry policy including a queue that backs off overnight.

Derive it from the thing that happened, not from the attempt:

// Good: the same key on every retry of the same event.
const key = `order-${order.id}-shipped`
// Useless: a fresh key each attempt, so every retry sends again.
const key = crypto.randomUUID()

A UUID is fine if you generate it once, store it with the job, and reuse it across retries. What must not happen is a new value per attempt.

Keys may be up to 255 characters.

Rejected, with 409:

{
"error": {
"type": "invalid_request_error",
"code": "idempotency_key_reused",
"message": "This Idempotency-Key was already used with a different request body. Use a new key for a new request."
}
}

Executing it would return two different results under one key, which defeats the entire point. It almost always means the key is derived from something too coarse, such as an order id where the same order sends several different messages. Include what the message is: order-1001-shipped, not order-1001.

If a second request arrives while the first is still running, it gets 409 with Retry-After: 2 rather than being allowed to race it. Exactly one executes. Wait and retry, and you will get the first one’s result.

A 5xx is a failure, not a decision, so it is not stored. Retrying after one attempts the request again, which is what you want: caching a transient outage would make it permanent for that key for a day.

A 4xx is stored. It is a decision about that exact payload and will not change on retry.

  • POST /v1/messages
  • POST /v1/contacts
  • POST /v1/campaigns
  • POST /v1/campaigns/{id}/recipients
  • POST /v1/campaigns/{id}/start
  • POST /v1/webhooks

Requests without the header run normally. The header is offered, not imposed.