Safe retries
How the Idempotency-Key header lets you retry a create, a paid call or a send without doing it twice.
This page explains the Idempotency-Key header and how to use it so a retry never charges twice or sends a second message.
Networks fail. A request can time out after done. has already done the work. Without a safeguard, a retry would score a person twice, charge twice, or send a second message. The Idempotency-Key header is that safeguard.
When it is required
Every create, spend or send needs an Idempotency-Key. A call without one is refused with 400 idempotency_key_missing. The reference marks the header as required wherever it applies.
Examples: POST /scores, POST /scoring_runs, POST /customer_profiles, POST /people, POST /messages/draft, POST /webhooks, POST /campaigns/{id}/launch and POST /conversations/{id}/reply.
Reads need no key, and neither does POST /scoring_runs/preview, which is a read. Most plain changes and deletes need none either, but not all: PATCH /scouts/{id} requires the header, because setting a scout active can lead to messages. Check each operation in the reference.
How to use it
Choose a unique value for each new request, such as a UUID. It must be 8 to 128 visible ASCII characters, with no spaces.
curl https://api.done.com/v1/scores \
-H "Authorization: Bearer $DONE_API_KEY" \
-H "Idempotency-Key: 6f1d2c0e-4b8a-4c1e-9d55-2a7c1f0b9e31" \
-H "Content-Type: application/json" \
-d '{ "linkedin_url": "https://www.linkedin.com/in/ana-lima" }'Keys are stored for 24 hours, per API key, workspace, method and path. Within that time:
- The same key with the same body returns the first answer. Nothing is done a second time.
- The same key with a different body is refused with 409
idempotency_key_reused. - The same key while the first request is still running is refused with 409
idempotency_key_in_progress. Wait a few seconds, then send the same request again with the same key to get its answer.
Because the key is stored per method and path, the same value on a different endpoint is a different request.
Retry rules
- Make one key per intended action, not one per attempt. Make it before the first attempt and keep it for every retry of that action.
- Retry with the same key and the same body after a timeout, a dropped connection, a
5xx, a429or anidempotency_key_in_progress. - Use a new key for a new action, even if the body looks the same. Two people you really mean to score twice need two keys.
- Do not retry other
4xxerrors unchanged. They will fail the same way.
import { randomUUID } from 'node:crypto';
async function post(path, body) {
const key = randomUUID(); // one key for this action, reused by every attempt
for (let attempt = 1; attempt <= 4; attempt++) {
try {
const response = await fetch(`https://api.done.com/v1${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DONE_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(body),
});
const retryable =
response.status >= 500 ||
response.status === 429 ||
(response.status === 409 && (await response.clone().json()).code === 'idempotency_key_in_progress');
if (!retryable) return response;
const wait = Number(response.headers.get('Retry-After')) || 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
} catch {
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); // network error
}
}
throw new Error('Gave up after 4 attempts');
}If every attempt fails, you can still check what happened before you try again. For example, list recent runs with GET /scoring_runs.