Idempotency keys on requests
Network calls fail in ways that hide whether the server got the request. You send a
POST /v1/schedules, the connection drops before the response arrives, and now you don’t
know if a schedule was created. Retry blindly and you might create two.
Send an Idempotency-Key header on mutating requests and that ambiguity goes away. A
retry with the same key replays the original response instead of running the handler
again, so the retry is safe.
Send the header
Section titled “Send the header”Generate a unique key per logical operation (a UUID is the easy choice) and pass it on the request:
curl -sS https://api.schedstack.com/v1/schedules \ -H "Authorization: Bearer sk_test_…" \ -H "Idempotency-Key: 7e3a1f6c-2b9d-4a1e-8c5f-9d0b1a2c3d4e" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "https://example.com/hooks/billing", "method": "POST", "delay": "5m", "body": "{\"invoice\":\"inv_123\"}" }'The key is optional but recommended on every mutation. A request with no
Idempotency-Key header gets no idempotency protection — it runs every time.
Idempotency applies to the mutating endpoints:
POST /v1/schedules,PATCH /v1/schedules/{id},POST /v1/schedules/{id}/reschedulePOST /v1/endpoints,PATCH /v1/endpoints/{id}POST /v1/deliveries/{id}/replay
How a retry is resolved
Section titled “How a retry is resolved”SchedStack stores the key with a fingerprint of your request (method + path + body) and, once the request settles, the response status and body. A retry is matched against that record.
-
Same key, same body, original succeeded → the stored response replays. You get the original status code and body back, plus an
Idempotent-Replayed: trueresponse header. The handler does not run again, so nothing is double-created. -
Same key, different body →
409 Conflictwith codeidempotency_key_reuse. A key is bound to the exact request it first ran; reusing it for a different payload is a bug on the caller’s side, so SchedStack refuses rather than guess. -
Same key, original still in flight →
409 Conflictwith codeidempotency_in_progress. Two requests with the same key raced; one wins and runs, the other is rejected so the operation runs at most once.
Replay example
Section titled “Replay example”The first call creates the schedule and returns 201. Retry the exact same request and
you get the same 201 body back, marked as a replay:
HTTP/1.1 201 CreatedIdempotent-Replayed: trueContent-Type: application/json
{ "id": "sch_01J…", "state": "active", … }Because the body is byte-identical to the stored response, your code can treat the replay exactly like the original — same schedule ID, no duplicate.
Conflict example
Section titled “Conflict example”Reusing a key with a changed body returns the typed error envelope:
{ "error": { "type": "idempotency_error", "code": "idempotency_key_reuse", "message": "This Idempotency-Key was already used with a different request body.", "request_id": "req_…" }}A concurrent in-flight request returns the same envelope with code
idempotency_in_progress — retry it once the first request settles.
What gets stored, and for how long
Section titled “What gets stored, and for how long”- Window: a key is remembered for 24 hours from the request, then it expires and the same string can be reused for a fresh operation. Retry within that window for replay protection.
- Scope: keys are isolated per (project, mode, key). The same key string in
testmode andlivemode are independent and never collide. - Only settled outcomes are cached. If the original request returned a
4xxor5xx, the key is released instead of stored — a4xxis a client error you can fix and a5xxis transient, so a corrected retry with the same key is allowed to proceed rather than being locked to a failed result for 24 hours.
Generating keys
Section titled “Generating keys”IDEMPOTENCY_KEY=$(uuidgen)echo "$IDEMPOTENCY_KEY"const idempotencyKey = crypto.randomUUID();
await fetch("https://api.schedstack.com/v1/schedules", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SCHEDSTACK_API_KEY}`, "Idempotency-Key": idempotencyKey, "Content-Type": "application/json", }, body: JSON.stringify({ endpoint: "https://example.com/hooks/billing", method: "POST", delay: "5m", body: JSON.stringify({ invoice: "inv_123" }), }),});import os, json, uuid, httpx
idempotency_key = str(uuid.uuid4())
httpx.post( "https://api.schedstack.com/v1/schedules", headers={ "Authorization": f"Bearer {os.environ['SCHEDSTACK_API_KEY']}", "Idempotency-Key": idempotency_key, "Content-Type": "application/json", }, json={ "endpoint": "https://example.com/hooks/billing", "method": "POST", "delay": "5m", "body": json.dumps({"invoice": "inv_123"}), },)key := uuid.NewString()
req, _ := http.NewRequest("POST", "https://api.schedstack.com/v1/schedules", strings.NewReader(`{"endpoint":"https://example.com/hooks/billing","method":"POST","delay":"5m","body":"{\"invoice\":\"inv_123\"}"}`),)req.Header.Set("Authorization", "Bearer "+os.Getenv("SCHEDSTACK_API_KEY"))req.Header.Set("Idempotency-Key", key)req.Header.Set("Content-Type", "application/json")Related
Section titled “Related”- Idempotency — request idempotency vs delivery idempotency, and how SchedStack makes at-least-once delivery safe.
- Limits & constraints — the idempotency window, scope, and other bounds in one place.
- Quickstart — schedule your first durable delivery.