Idempotency
A network drops, a timeout fires, you never get the response. Do you
retry? With an Idempotency-Key you can, safely: the same
operation is never processed twice. No repeated operations, no double
sends, no duplicate resources. It is opt-in: omit the key and nothing changes.
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 same response
as the first time instead of running the action again.
Keys live 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 distinct sends carry two distinct keys.
How to use it
- Generate a unique identifier per operation. We recommend a UUID v4.
- Send it in the
Idempotency-Keyrequest header. - If the call fails on a network error or timeout, retry with the same key and the same body.
- For a different operation, use a new key.
The key accepts 1 to 255 characters from A-Z a-z 0-9 _ -. Scope is per organization: your key never collides with another account or another endpoint.
Examples
You only add a header. The body and the rest of the request stay the same.
cURL
curl -X POST https://api.hablame.co/api/v6/urlshortener/links \
-H "Authorization: Bearer hk_your_api_key" \
-H "Idempotency-Key: 5f3b2c10-9a7e-4b2d-8c1f-0a1b2c3d4e5f" \
-H "Content-Type: application/json" \
-d '{"url":"https://hablame.co/promo"}'
PHP
$key = bin2hex(random_bytes(16)); // one per operation; keep it for retries
$ch = curl_init('https://api.hablame.co/api/v6/urlshortener/links');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer hk_your_api_key',
'Idempotency-Key: ' . $key,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['url' => 'https://hablame.co/promo']),
]);
$response = curl_exec($ch);
JavaScript (Node)
import { randomUUID } from 'node:crypto';
const key = randomUUID(); // one per operation; reuse it on retries
const res = await fetch('https://api.hablame.co/api/v6/urlshortener/links', {
method: 'POST',
headers: {
'Authorization': 'Bearer hk_your_api_key',
'Idempotency-Key': key,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'https://hablame.co/promo' }),
});
Responses
The API tells you what happened to each key in the Idempotency-Status header.
New (executed)
The first time. The operation runs and the response is stored. Header: Idempotency-Status: created.
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 is not executed again: you get the same response. Headers: Idempotency-Status: replayed and Idempotency-Replayed: true.
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 one is still being processed. Wait and retry, respecting the Retry-After header.
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. This is almost always a client bug: a key identifies one operation, not several. Use a new key.
HTTP/1.1 422 Unprocessable Entity
{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_REUSED" } }
Invalid key (400)
The key is empty, too long, or has characters outside A-Z a-z 0-9 _ -.
HTTP/1.1 400 Bad Request
{ "success": false, "error": { "code": "IDEMPOTENCY_KEY_INVALID" } }
Recommendations for automatic retries
- Generate the key before the first attempt. Store it and reuse it on every retry of the same event. A new key per attempt loses the protection.
- Treat 409 as "retry later". Wait for
Retry-Afterand try again with the same key. - Treat 422 as a bug. It means you changed the body under the same key: fix your logic, do not loop.
- Exponential backoff with jitter for network retries, same as with rate limiting.
Two important details. Replay only applies to successful responses: if the first request failed with an error (for example an invalid field), a retry is attempted again, it does not return the stored error. And the guarantee is best effort: during an outage of our control infrastructure the request is processed anyway. For critical operations, pair the key with your own deduplication.