Skip to content

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.

Generate a unique key per logical operation (a UUID is the easy choice) and pass it on the request:

Terminal window
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}/reschedule
  • POST /v1/endpoints, PATCH /v1/endpoints/{id}
  • POST /v1/deliveries/{id}/replay

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.

  1. Same key, same body, original succeeded → the stored response replays. You get the original status code and body back, plus an Idempotent-Replayed: true response header. The handler does not run again, so nothing is double-created.

  2. Same key, different body409 Conflict with code idempotency_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.

  3. Same key, original still in flight409 Conflict with code idempotency_in_progress. Two requests with the same key raced; one wins and runs, the other is rejected so the operation runs at most once.

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 Created
Idempotent-Replayed: true
Content-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.

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.

  • 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 test mode and live mode are independent and never collide.
  • Only settled outcomes are cached. If the original request returned a 4xx or 5xx, the key is released instead of stored — a 4xx is a client error you can fix and a 5xx is transient, so a corrected retry with the same key is allowed to proceed rather than being locked to a failed result for 24 hours.
Terminal window
IDEMPOTENCY_KEY=$(uuidgen)
echo "$IDEMPOTENCY_KEY"
  • 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.