done.developers

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

  1. 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.
  2. Retry with the same key and the same body after a timeout, a dropped connection, a 5xx, a 429 or an idempotency_key_in_progress.
  3. 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.
  4. Do not retry other 4xx errors 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.

On this page