Skip to main content

Idempotency

POST /v1/tasks accepts an optional idempotency_key in the request body. Reusing the same key returns the existing task instead of creating a duplicate. Several other writes accept the same body field, with stricter semantics — see Beyond POST /v1/tasks.

When to use

  • Scheduled jobs. Cron fires at 9am Monday. Use idempotency_key: "weekly-deck:2026-W17". If your worker crashes after the POST succeeds but before writing the response, the next retry returns the same task.
  • Network-timeout retries. Your HTTP client times out mid-POST. Retry with the same key — safe.
  • Upstream-triggered flows. A Slack command fires two webhooks on the same action. Use the slack message ID as the key: idempotency_key: "slack:T01234:123456.789".
  • Bulk fan-out. 50 prospects × one task each. idempotency_key: "prospect:{id}:2026-04-weekly" keeps re-runs safe.

How it works

  • First call: creates the task, returns the envelope.
  • Second call with the same key: returns the original task’s envelope. The task lane matches on the key alone — it does not compare request bodies, so a reused key with a different prompt silently replays the first task rather than starting the new one.
No dedicated “I’m reusing” flag on the response — the id matches the original, that’s how you can tell. The key is the intent. Because there is no body comparison and no conflict error on this endpoint, a stale key is a silent no-op, not a loud failure: derive the key from everything that makes the request distinct (week, prospect, revision), and mint a fresh one whenever you want fresh output.

Key design

  • Stable and unique per logical operation. “weekly-deck” alone is bad (reused every week); "weekly-deck:2026-W17" is good.
  • Hash complex inputs if the key would otherwise be long: sha256(prompt + brand_kit_id + format). Deterministic + short.
  • Namespace per integration — keys are not isolated per API key. The task lane looks a key up globally, and the hashed lanes scope records to (team, user, operation, key). Two of your integrations sharing a team can collide on a bare "weekly-deck" and silently replay each other’s result. Prefix them: "myapp:weekly-deck:2026-W17".
  • TTL. Idempotency records are retained long enough to cover sensible retry windows (24h on the hashed lanes) — don’t build around a specific duration. If you’re retrying a week later, use a different key.

Beyond POST /v1/tasks

idempotency_key is not task-only, and the other endpoints that take it are stricter than the task lane: they ride a shared pipeline that hashes the request payload alongside the key. Same key + same payload replays the stored result; same key + a different payload is a real 409 idempotency_conflict; a same-key duplicate that lands while the first call is still running gets a retryable 409 instead of a second side effect. Records expire after 24h. POST /v1/brand-kits is the one to know — a retried create replays the stored kit instead of minting a second one (if the kit was deleted in the meantime, the stale record is invalidated and the create re-runs fresh). The metered verbs take it too. When in doubt, check the endpoint’s request schema for an idempotency_key field rather than assuming it has none. Records are scoped to (team, user, operation, key) — not to the API key — so namespace your keys per integration. Replay protection is best-effort, not a transaction. The store fails open: if Redis is unavailable or the record was evicted, the call executes without replay protection rather than 5xx-ing. A retry in that window can produce a second side effect. Treat idempotency_key as strong protection against the common failure (a timed-out retry), not as a guarantee of exactly-once. POST /v1/uploads has no idempotency_key, and doesn’t need one: it dedupes by content hash and reports was_duplicate: true.

Conflict handling (hashed lanes)

Fix one of:
  • Use a different idempotency_key for the new body.
  • Change your code so the body is byte-identical (e.g. canonicalize array order, don’t include a timestamp in the payload).

Not wired (yet)

  • Idempotency-Key HTTP header (Stripe-style). Today, idempotency_key is a body field. A header-based variant is on the roadmap; when it lands, existing body-field usage continues to work.

Webhook idempotency (separate concern)

Webhook receivers should use the event envelope’s id (evt_…) as the dedupe key. That’s about guarding your handler against duplicate deliveries — a different concern from idempotency_key on task creation. See webhooks.md.

Worked example — weekly cron

Common wrong guesses

  • Using an Idempotency-Key HTTP header. Not supported today. Use the body field idempotency_key.
  • Reusing the same key across different operations. “default” as the key for every task. You’ll just keep getting back the first task you ever created with that key.
  • Changing the body and reusing the key on POST /v1/tasks. You get the old task back, not an error and not a new task — the task lane never compares bodies. Use a fresh key when the intent changed. (On the hashed lanes below, the same mistake is a loud 409 idempotency_conflict.)
  • Assuming idempotency_key is task-only. POST /v1/brand-kits and the metered verbs take it too, with the same replay / 409 semantics. Check the endpoint’s request schema.

Upstream