Idempotency
Every send endpoint accepts an Idempotency-Key header. It exists for one situation: your request timed out, or the connection dropped, and you do not know whether the message went out. Retrying blindly risks sending twice; not retrying risks not sending at all. An idempotency key removes the guess.
Sending the same request twice — for example because a network timeout made you retry — could deliver the same message twice. To make a retry safe, send an Idempotency-Key header whose value is unique to that request (for example order-1042-receipt):
- The first request with a given key is processed normally and its response is stored.
- Any later request with the same key (within 24 hours) is not reprocessed — it returns the original response unchanged, plus an
Idempotent-Replayed: trueresponse header. The side effect happens only once. - After 24 hours the key expires and may be reused.
The key can be any unique string up to 256 characters. A UUID works, but a value derived from the entity the request is about — like order-1042-receipt — is easier to regenerate identically on a retry. Keys are scoped per endpoint, so the same value can be reused independently across the send-email, send-SMS, and send-event endpoints without colliding.
Reuse a key with a different request body and the request is rejected with 409 invalid_idempotent_request, which protects you from receiving the wrong cached response. Use a fresh key for a genuinely different request.
Idempotency error responses
| Status | name | When |
|---|---|---|
400 | invalid_idempotency_key | The key is empty or longer than 256 characters. |
409 | invalid_idempotent_request | The key was already used with a different request body. |
409 | concurrent_idempotent_requests | An earlier request with the same key is still being processed — retry shortly. |
Which endpoints support it
| Endpoint | Suggested key |
|---|---|
POST /email/send | The thing the email is about — order-1042-receipt |
POST /sms/send | otp-{user_id}-{attempt} |
POST /whatsapp/send | booking-{booking_id}-confirmation |
POST /push/send | shipment-{shipment_id}-out-for-delivery |
POST /events/send | Your own event ID from the source system |
Keys are scoped per endpoint, so the same string can be used independently on /email/send and /sms/send without colliding.
Choosing a key
The best key is one your retry can regenerate identically without having stored anything. A key derived from the entity the request is about — an order ID, a booking reference, a shipment number — survives a process restart mid-retry. A random UUID only works if you persist it before the first attempt.
Avoid keys derived from the current time: a retry a second later produces a different key and defeats the mechanism entirely.
Idempotency is not deduplication
The 24-hour window protects retries of one logical request. It is not a general "never send this person the same thing twice" guard — after 24 hours the key expires and a request carrying it is processed as new. For genuine deduplication across longer spans, key on your own side.
Creation endpoints such as POST /contacts do not use idempotency keys; they are protected by uniqueness constraints instead, and a duplicate returns 409.