Skip to main content

Idempotency

Network failures are inevitable. When you send WhatsApp messages and templates, or reply in a conversation, the Mojeeb Public API uses an Idempotency-Key header to make retries safe — the same key plus the same body returns the original response, never sends twice.

How do I make my retries safe?​

Send any client-chosen string up to 255 characters in the Idempotency-Key header when you send a WhatsApp message or template (POST /v1/whatsapp/messages, POST /v1/whatsapp/templates) or reply in a conversation (POST /v1/agents/{agentId}/conversations/{conversationId}/messages):

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-confirmation" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "order_shipped",
"language": "en",
"parameters": {
"customer_name": "Sara",
"tracking_url": "https://example.com/track/A123"
}
}
}'

The header is optional but strongly recommended for every send. Without it, a network retry can produce a duplicate message. Other endpoints ignore it: a retried POST that creates a lead, note, knowledge base or custom API tool creates it again, so check whether it exists before you retry.

What happens on retry?​

Only a successful answer is stored: an accepted send (202), or a reply in a conversation (201). The key is claimed only after the request passes authentication, scope, rate-limit and format checks, and on a reply the agent and conversation checks. A request refused there (401, 403, 413, 429, a format 422 such as invalid_request_body, invalid_agent_id, invalid_phone_number or template_header_media_url_invalid, or on a reply a 404 for an agent or conversation the key can't reach, or a 503) never touches the key: fix it and retry with the same key straight away.

Once the key is claimed, the behavior depends on how the original request ended and whether the body matches:

ScenarioResponseHTTP
First send with this keyNormal response202 (201 for a reply)
Same key with any body, while a request with it is still running or less than 60 seconds after the start of one that was refused or failed after claiming it (for example a WhatsApp send's agent_not_found, from_phone_not_found_for_agent or template_not_found, a reply's messaging_window_closed or channel_not_supported, or a 5xx)idempotency_key_in_progress (with Retry-After)409
Same key + same body, original acceptedReplays original response, Idempotent-Replayed: true header202 (201 for a reply)
Same key + different body, endpoint or conversation, original acceptedidempotency_key_in_use_with_different_params422
Same key, 60 or more seconds after the start of that refused or failed request, or a different keyProcessed as a new request—

How long are responses cached?​

At least 24 hours: a retry with the same key and body in that time always replays the stored answer. Don't count on a key becoming new again after that. Give every new operation a new key, and pick stable keys tied to your business operation, not random UUIDs that change between attempts.

How do I pick good idempotency keys?​

A good key is stable, unique, and tied to a single business operation.

Good:

  • order-12345-confirmation
  • signup-456-welcome
  • appointment-reminder-2026-04-30-user-789

Bad:

  • Timestamps (each retry gets a different one — defeats the point)
  • Random UUIDs generated at request time (same problem)
  • Generic strings like retry-1 (collisions across operations)
  • Keys reused across different message bodies or conversations (triggers 422 mismatch)

If you don't have a natural stable identifier, generate one once at the start of your operation and reuse it for every retry of that specific send.

What does a mismatch look like?​

When the same key is reused with a different body, or on another endpoint or conversation, the API rejects the request and gives you the correlation id of the original send so you can find it in your own logs:

{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_in_use_with_different_params",
"message": "The idempotency key was used previously with a different request body. Use a different key for this operation.",
"correlation_id": "req_01KQE...",
"original_correlation_id": "req_01KQE..."
}
}

original_correlation_id is the trace id of the request that originally claimed the key. Search your application logs for that id to see what was sent.

What does an in-progress retry look like?​

If you retry, with any body, while the original send is still being processed (rare, usually only inside a few seconds), or within 60 seconds of the start of a request with the key that was refused or failed after claiming it:

{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_in_progress",
"message": "A request with this idempotency key is currently being processed. Retry shortly.",
"correlation_id": "req_01KQE..."
}
}

The response also carries a Retry-After header in seconds. Wait that long, then retry the same key + same body — you'll get a replay of the original (202, or 201 for a reply) if it was accepted, a new attempt once 60 seconds have passed since the refused or failed request started, or another 409 until then or while it's still in flight.

Common questions​

Does the API hash my body to detect mismatches?​

Yes. The first request with a given key stores a SHA-256 hash of the request: its method, its path and its canonicalized body. Subsequent requests recompute the hash and compare. Field order doesn't matter; we sort keys before hashing — {"a":1,"b":2} and {"b":2,"a":1} are the same logical body. The path counts too: once the original was accepted, the same key with the same body on another endpoint, or in a reply to another conversation, is a mismatch (422), not a replay.

What about idempotency on GET requests?​

GET endpoints are inherently idempotent — calling them multiple times has no side effect. The Idempotency-Key header is ignored on GET; you don't need to send it.

Do I need idempotency keys for status lookups?​

No. GET /v1/whatsapp/messages/{id} is read-only.

What if the cached response was an error?​

It isn't cached. Only successful answers are stored. After a request that was refused or failed after claiming the key (a 4xx such as template_not_found or a reply's messaging_window_closed, or a 5xx), the same key answers 409 idempotency_key_in_progress, whatever the body, until 60 seconds after that request started, and then runs the request again. To retry a corrected request straight away, use a different key. A request refused before the key is claimed, such as a format 422, doesn't hold the key.

Is a replay the same response as the original?​

Yes. A replay returns the original status code and the same body: the same fields, null ones included, with the same values. Only the Idempotent-Replayed: true header is added, so you can tell a replay from the original. The fields may come in a different order, so compare the parsed JSON, not the raw bytes.