Tone docs
Flows

Retrying safely after a timeout

A timeout tells you nothing about whether the work happened. This is how you find out without doing it twice.

Three operations spend money or dial a stranger, and for those a timeout is the dangerous case — not because it failed, but because you cannot tell whether it did.

OperationIdempotency-Key
POST /v1/numbers/purchaseRequired400 without it
POST /v1/campaigns/{id}/launchRequired400 without it
POST /v1/callsOptional, strongly recommended

The rule

Generate a key per logical operation, not per attempt. Retry with the same key.

const key = crypto.randomUUID();          // once, outside the retry loop

async function buy(e164) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${API}/v1/numbers/purchase`, {
      method: 'POST',
      headers: {
        authorization: `Bearer ${process.env.TONE_KEY}`,
        'content-type': 'application/json',
        'idempotency-key': key,           // ← the SAME key every attempt
      },
      body: JSON.stringify({ e164 }),
    });

    if (res.ok) return res.json();

    const { error } = await res.json();
    if (error.code === 'idempotency_key_in_use') {   // 409 — first one still running
      await sleep(1000 * 2 ** attempt);
      continue;
    }
    if (res.status >= 500 || error.code === 'provider_unavailable') {
      await sleep(1000 * 2 ** attempt);
      continue;
    }
    throw new Error(`${error.code}: ${error.message}`);
  }
}

Persist the key alongside whatever prompted the operation. A key held only in memory is gone exactly when the process crashed mid-request — the one case it existed for.

What each response means

2xx with Idempotent-Replayed: trueThe first attempt succeeded. This is its stored response — not new work
409 idempotency_key_in_useThe first attempt is still running. Wait and retry
422 idempotency_key_reused_with_different_paramsSame key, different body. Almost always a bug: a key reused across two genuinely different operations
503 provider_unavailable🔴 The carrier's answer left the outcome unknown — not "it failed". Retry with the same key. A fresh key risks buying two numbers

Keys are remembered for 24 hours. A handler that throws releases its key, so a genuine error can be retried after fixing the request.

Where it does not help

Idempotency protects the operation, not your bookkeeping. If you generate a new key because your own record of the first attempt was lost, you get two numbers and two rentals. The durable key is the whole mechanism.

Was this page helpful?

On this page