Idempotency

A network drops, a timeout fires, you never get the response. Do you retry? With an Idempotency-Key, yes, safely: the same operation is not processed twice. It is optional; if you do not send the key, everything works as before.

5 min read

What it is

Idempotency guarantees that repeating a request has the same effect as making it once. You generate a unique identifier per operation and send it in the Idempotency-Key header. If a request with that key was already processed, the API returns the original response instead of running the action again.

The key lives for 24 hours. Within that window, any retry with the same key is safe.

Think of the key as "one per business intent", not "one per retry". Retrying the same send reuses the same key. Two different sends carry two different keys.

How to use it

  1. Generate a unique identifier per operation. We recommend a UUID v4.
  2. Send it in the request Idempotency-Key header.
  3. If the operation fails on the network or times out, retry it with the same key and the same body.
  4. For a different operation, use a new key.

The key accepts 1 to 255 characters from the set A-Z a-z 0-9 _ -. Scope is per organization: your key never collides with another account or another endpoint.

Examples

You only add one header. The body and the rest of the request stay the same.

curl -X POST https://developers.hablame.co/api/v6/urlshortener/links \
  -H "Authorization: Bearer hk_TU_API_KEY" \
  -H "Idempotency-Key: 5f3b2c10-9a7e-4b2d-8c1f-0a1b2c3d4e5f" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hablame.co/promo"}'

Responses

The API tells you what happened with each key in the Idempotency-Status header.

New: it ran

The first time. The operation runs and the response is stored.

HTTP
HTTP/1.1 201 Created
Idempotency-Status: created

{ "success": true, "data": { "code": "aZ3kPq1", "domain": "hbl.li" } }

Repeated: replay

A retry with the same key and the same body. It does not run again: you get the same response.

HTTP
HTTP/1.1 201 Created
Idempotency-Status:   replayed
Idempotency-Replayed: true

{ "success": true, "data": { "code": "aZ3kPq1", "domain": "hbl.li" } }

In progress: 409

You sent the same key while the first request is still being processed. Wait and retry, respecting the Retry-After header.

HTTP
HTTP/1.1 409 Conflict
Retry-After: 2

{ "success": false, "error": { "code": "IDEMPOTENCY_IN_PROGRESS" } }

Reused with a different body: 422

You used a key that already exists but with a different body. It is almost always a client bug: one key identifies one operation, not several. Use a new key.

HTTP
HTTP/1.1 422 Unprocessable Entity

{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_REUSED" } }

Invalid key: 400

The key is empty, too long, or contains characters outside A-Z a-z 0-9 _ -.

HTTP
HTTP/1.1 400 Bad Request

{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_INVALID" } }

Tips for automatic retries

  • Generate the key before the first attempt. Store it and reuse it on every retry of the same event. Generating a new key per attempt loses the protection.
  • Treat a 409 as "retry later". Wait for Retry-After and try again with the same key.
  • Treat a 422 as a bug on your side: you changed the body under the same key. Fix the logic, do not retry in a loop.
  • Use exponential backoff with random jitter for network retries, just like with usage limits.

Two important details. Replay only applies to successful responses: if the first request failed with an error (an invalid value, for example), a retry runs again instead of returning the stored error. And the guarantee is best-effort: if something on our side is unavailable, the request is processed anyway. For critical operations, combine the key with your own deduplication.

Where it applies today

The header is documented on every endpoint that supports it. Today it is accepted by:

  • POST /api/v6/urlshortener/links
  • GET /api/v6/numberinsight/{number}
  • POST /api/v6/numberinsight/batch
  • POST /api/v6/tts/synthesize
  • POST /api/v6/callblasting/calls
  • POST /api/v6/callblasting/calls/audio