Skip to main content

Idempotency

A network error or a timeout leaves you not knowing whether a request went through. If you simply send it again, a customer may get the same email twice or be asked to pay twice. An idempotency key makes the retry safe: Ha Tang does the work once and answers every repeat with the same result.

Where it is supported​

Send the key in the Idempotency-Key header. It is any string you choose, up to 256 characters.

EndpointScope of the keyA repeat with the same bodyA repeat with a different body
POST /emailsThe business200 with the same id; nothing is sent again400 invalid_idempotent_request
POST /paymentsThe business and the Payments environment201 with the same payment400 invalid_idempotent_request
POST /chat/conversations/{id}/messagesThe conversation200 with the same message and "duplicate": true200 with the first message (the body is not compared)

POST /emails/batch and other endpoints do not take an idempotency key.

Send an idempotency key​

Use a key that identifies the business operation, not the HTTP attempt: your order id, welcome-<user id>, invoice-<number>-reminder-1. Generate it once, store it with the job, and reuse it on every retry.

curl -X POST https://api.hatang.dev/emails \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001-confirmation" \
-d '{
"from": "Shop <[email protected]>",
"subject": "Order #1001 confirmed",
"html": "<p>Thanks for your order!</p>"
}'

How keys are stored​

  • Email and payments. The key is stored with the email or payment it created, together with a hash of the request body. A repeat with the same key is compared against that hash. Keys do not expire: as long as the email or payment exists, its key keeps pointing to it. A replayed email does not count against the daily quota again.
  • Chat messages. The key becomes the message's client_id. A repeat in the same conversation returns the existing message with "duplicate": true. The widget and the mobile SDKs do the same thing with the client_id they send on each message, which is why a message sent twice over a flaky connection appears once in the inbox.
  • Payments environments. A payment key is scoped to its environment, so the same order id can be used once in development and once in production.

Conflicts​

nameStatusCauseWhat to do
invalid_idempotent_request400The key was already used with a different request bodyYou reused a key for a different operation. Generate a new key; never change the body of a retry.
concurrent_idempotent_requests409Email only: two requests with the same key arrived at the same momentWait a moment and retry with the same key; you get the result of the first one.
{
"statusCode": 400,
"name": "invalid_idempotent_request",
"message": "Same idempotency key used with a different request payload"
}

"Different body" means any changed value, such as a new timestamp or a regenerated random id inside the payload. Build the payload once, store it with the key, and resend the same payload on every retry.

Retry advice​

  1. Create the key before the first attempt and keep it with the job, so a crash between attempts does not lose it.
  2. Retry only what is temporary: network errors, 429 rate_limit_exceeded and 5xx. Use exponential backoff and honor retry-after (Rate limits).
  3. Do not retry 4xx errors other than 429 and 409 concurrent_idempotent_requests; fix the request instead.
  4. Treat a replay as success. A 200 for a repeated email or a "duplicate": true chat message means the first attempt worked.
  5. Make your webhook handlers idempotent too. Deliveries can repeat; see Webhooks.

Next steps​

View this page as Markdown