Idempotency and safe retries

Send an Idempotency-Key on any write: a retry replays the first response instead of repeating the action.

Embed
Preview of the card
<a href="https://hermeticor.com/guides/idempotency"><img src="https://hermeticor.com/guides/_api/public/orgs/5fcd8b3c-a27c-4688-972f-de710125592f/spaces/9a3da731-ded5-4536-b84f-8c9a61420ff4/pages/fc00a87b-0b9f-4973-a4b2-99ab54b5aa0b/social-card.svg?v=3e7ae33eec8b" alt="Idempotency and safe retries" width="600" height="315"></a>

Networks fail. A retry must never create a record twice or send a message twice. Hermeticor works the way Stripe does: put an Idempotency-Key header on the call, and a retry with the same key gets the first answer back.

1. Idempotency-Key on every write

Any POST, PATCH or DELETE takes the header:

curl -s -X POST "$BASE/v1/business-goals" \
  -H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: goal-q4-paid-1" \
  -d '{"name":"Q4 2026: first 25 paying workspaces","metric":"conversions","targetValue":25,"startsOn":"2026-10-01","endsOn":"2026-12-31"}'
  • Same key, same request, within 24 hours: you get the first call's status and body back, with the header Idempotent-Replayed: true. Nothing is created or sent again.

  • Same key, different request (another body, URL or method): 422 with code idempotency_key_reused. Use a new key for a new request.

  • Two calls with the same key at the same moment: the second waits for the first and then gets its answer.

  • A call that failed (any 4xx or 5xx) is not stored, because nothing was changed. Retrying it with the same key runs it again.

  • A response that carries a secret, such as a new site key or a webhook signing secret, is never stored, so a retry of that call runs again.

  • Keys are 1-255 characters and belong to the API key (or signed-in user) that sent them. After 24 hours a key can be used again.

Use a key that names the thing you are doing, such as receipt-order-1042, not a random value made at retry time. Then a retry after a crash still sends the same key.

2. Sends keep their own permanent key

Send routes (transactional sends, messages, in-app, push, WhatsApp, social) require a key, as the header or as idempotencyKey in the body:

curl -s -X POST "$BASE/v1/transactional/send" \
  -H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: receipt-order-1042" \
  -d '{"to":[{"email":"ada@example.com"}],"subject":"Your receipt","html":"<p>Thanks!</p>","from":{"email":"hello@example.com"}}'
  • No key: 400. Header and body key that differ: 409.

  • Within 24 hours a retry replays the first response, as above.

  • After 24 hours the send's own key still holds: a repeat answers 202 with status: duplicate and the original job id. Nothing new is sent, ever.

One-off cold email (POST /v1/outbound/send) takes the key in the body as idempotencyKey.

3. dedupeKey on events

curl -s -X POST "$BASE/v1/events" \
  -H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Signed up","profile":{"externalId":"usr_ada"},"dedupeKey":"signup-usr_ada"}'

A repeat counts in duplicates and is not stored again, with no time limit.

4. Without a key

A retried create without a key can make a second record. If you cannot send a key, use upsert routes where they exist (POST /v1/profiles/upsert, imports with "onExisting":"update"), or list or search for the record before retrying.

Updated