Idempotency
Three endpoints require an Idempotency-Key header: POST /otp/send, POST /messages/send, and
POST /devices/{id}/messages. Without it the request is refused with 400 invalid_input.
The Google Business writes (PATCH /gmb/locations/{id}, PUT /gmb/locations/{id}/reviews/{reviewId}/reply and
POST /gmb/locations/{id}/refresh) take an optional Idempotency-Key of 8–128 characters, which is stored for
24 hours rather than forever; the same key with a different request is 409 gmb_idempotency_conflict. See
Google reviews.
Idempotency-Key: order-8821-shippedThe value is 1–128 characters, and it is yours to choose. Derive it from the business event, not from
the attempt: order-8821-shipped is right, retry-3 is not.
What it guarantees
Section titled “What it guarantees”| You send | MauKirim does |
|---|---|
| A key it has never seen | Sends, and remembers the outcome |
| The same key with the same normalized request | Returns the original result. No second message, no second charge. On devices/{id}/messages the response carries "created": false. |
| The same key with a different request | Refuses with 409 batch_idempotency_conflict |
“Same request” is decided by a SHA-256 fingerprint of the normalized fields, not by the raw bytes:
| Endpoint | Fingerprint covers |
|---|---|
POST /otp/send |
purpose, phone |
POST /messages/send |
template id, phone, variable pairs sorted by name; item arrays retain item order while each item’s field pairs are sorted by name |
POST /devices/{id}/messages |
gateway, device, recipient, kind, trimmed text, media key |
So changing whitespace or adding a variable that the template does not use still counts as a different request, and changing the recipient definitely does.
Retry the same key, always
Section titled “Retry the same key, always”This is the whole point. A send can fail after MauKirim accepted it — a timeout, a dropped connection, a worker that restarts mid-dispatch. If you retry with a new key you may send the message twice; if you retry with the same key you either get the original result or an explicit conflict.
# the request that timed outcurl -X POST "$BASE/messages/send" -H "Idempotency-Key: order-8821-shipped" …
# the safe retry: identical body, identical keycurl -X POST "$BASE/messages/send" -H "Idempotency-Key: order-8821-shipped" …Two consequences worth knowing before you design around it
Section titled “Two consequences worth knowing before you design around it”1. Keys are reserved for the life of the account. The batch row that holds the outcome is never deleted,
and the uniqueness rule is a database index on (account, key). So a key you used a year ago is still
reserved: reusing it with a different message is a 409, forever. Use keys that are naturally unique per
business event (an order id, an OTP challenge id, a ticket id) rather than a counter you might reset.
2. The message body is purged, the record is not. Send text and media object keys are cleared seven days after an attempt becomes terminal, and queued media expires seven days after upload. The id, the states and the timestamps remain — which is what makes the delivery log and the dashboard useful without keeping your customers’ messages around.
What is not idempotent
Section titled “What is not idempotent”POST /otp/verify, POST /devices/{id}/presence, POST /devices/{id}/chat-presence,
PATCH /devices/{id}/profile and the webhook routes do not take an Idempotency-Key. Verification is
naturally repeatable (a wrong code simply returns verified: false), presence changes are immediate
gateway actions rather than queued messages, and the webhook routes are declarative: setting the same
destination twice leaves the same destination.