Knowledge Base
Idempotency & retries
The metering layer is built so you can retry safely — you’re charged exactly once per successful attempt, and never for a failure. Here’s how that works and how to lean on it.
The billing guarantees
Every metered call inherits four guarantees (covered in Credits & billing):
- Rate-limit first — a throttled request is rejected before any charge.
- Debit before work — credits are reserved up front; an insufficient balance fails before processing.
- Exactly once per attempt — the debit and usage record are idempotent on a server-generated request ID.
- Refund on failure — if the work raises, the reservation is refunded and the usage row records 0 net credits.
Together these mean a failed call — a 4xx, 5xx, or a dropped connection mid-work — costs you nothing.
Retrying a tool call
Because you’re only charged on success, retrying a failed call is safe: a call that errored wasn’t billed, so a retry is a fresh, independently-billed attempt. Back off and retry on transient conditions — 429(respect Retry-After), 503, network timeouts — and don’t retry on deterministic errors like 400, 401, 403, 415 or 422, which will just fail again.
async function callWithRetry(url, options, tries = 3) {
for (let attempt = 0; attempt < tries; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
continue; // transient — retry
}
if (res.status >= 500) { await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); continue; }
return res; // 4xx (except 429) — don't retry
}
throw new Error("exhausted retries");
}One caveat on double-billing
Idempotency is per server-generated request ID, so a network retry of a call that actually succeeded on the server (you just never saw the response) is a new attempt and will be billed again. For expensive calls, prefer catching the timeout and checking your own records before blindly re-sending.The request ID
Each request gets a server-generated UUID, returned in the request_id error field and the X-Request-ID response header. The API deliberately ignores any inbound X-Request-ID you send — the ID is always minted server-side so a client can’t replay one to get free work. Log it: it’s the fastest way for support to trace a specific call. See Errors.
Webhook idempotency
On the receiving side, webhook deliveries are at-least-once, so you can get the same event twice (a retry, or a manual redelivery). Dedupe on event_id (also sent as the Webhook-Id header) — it’s stable across retries and redeliveries of the same event. Process each event_id once and acknowledge the rest.
