Idempotency
Safe retries for write operations via the Idempotency-Key header (§1.3). Eligible routes, TTL, storage, and retry semantics.
Network failures happen. Sending the same mutating request twice can create duplicate resources (two runs started, two keys generated, double-charged cost rates). CodeCourier's idempotency layer solves this: attach an Idempotency-Key header and the server guarantees a single side effect, even if the client retries.
How It Works
- Client generates a unique key per logical operation (UUIDv4 recommended) and sends it in the
Idempotency-Keyheader. - Server hashes the key + method + path + body; on first request it executes the handler and stores the full response.
- On any retry with the same key, the stored response is replayed byte-for-byte. No duplicate side effect.
- If a retry arrives with the same key but a different body, the server returns
409 idempotency_mismatch.
TTL
Idempotency records are retained for 24 hours. After that, the key is forgotten and replaying it executes a fresh request. Design retries to complete well within this window.
Eligible Routes
All POST mutation endpoints accept the header. Notable examples:
POST /api/v1/runs/startPOST /api/v1/workflows/createPOST /api/v1/issues/createPOST /api/v1/project/api-keys/generatePOST /api/v1/sandboxes/createPOST /api/v1/cost-rates/upsertPOST /api/v1/recurring-tasks/create
GET, HEAD, and DELETE are idempotent by definition; the header is ignored (never an error) for them.
PATCH endpoints
Only POST routes are formally documented as supporting the Idempotency-Key header. The single PATCH endpoint currently exposed is treated by the server as a write operation and will accept the header on a best-effort basis:
PATCH /api/v1/webhooks/endpoints/{id}- supported
For any future PATCH routes, assume idempotency is not guaranteed unless explicitly documented on that route. Clients integrating new PATCH endpoints should implement client-side deduplication (e.g. a request-in-flight cache keyed by resource id + body hash) until first-class support is confirmed.
Example
curl
IDEM=$(uuidgen)
curl -X POST https://<your-deployment>.convex.site/api/v1/runs/start \
-H "Authorization: Bearer cc_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d '{ "workflowId": "wf_abc", "input": { "topic": "hello" } }'TypeScript
import { randomUUID } from "node:crypto";
async function startRun(workflowId: string, input: unknown) {
const idempotencyKey = randomUUID();
const attempt = async () =>
fetch("https://<your-deployment>.convex.site/api/v1/runs/start", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CC_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ workflowId, input }),
});
// Retry up to 3x with exponential backoff - safe because of Idempotency-Key
for (let i = 0; i < 3; i++) {
const res = await attempt();
if (res.ok) return res.json();
if (res.status < 500) throw new Error(await res.text());
await new Promise((r) => setTimeout(r, 2 ** i * 500));
}
throw new Error("exhausted retries");
}Python
import os, uuid, time, requests
def start_run(workflow_id: str, input_payload: dict):
idem = str(uuid.uuid4())
headers = {
"Authorization": f"Bearer {os.environ['CC_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": idem,
}
body = {"workflowId": workflow_id, "input": input_payload}
for i in range(3):
r = requests.post(
"https://<your-deployment>.convex.site/api/v1/runs/start",
headers=headers, json=body, timeout=30,
)
if r.ok:
return r.json()
if r.status_code < 500:
r.raise_for_status()
time.sleep(2 ** i * 0.5)
raise RuntimeError("exhausted retries")Retry Semantics
- Same key + same body → replay stored response (200 OK if original succeeded, original error otherwise).
- Same key + different body →
409 idempotency_mismatch. Generate a new key for the new operation. - In-flight duplicate(two retries racing) → later request blocks briefly, then replays the first's response.
Best Practices
- Generate the key before the first attempt; reuse it for all retries of that logical operation.
- Use UUIDv4 or a ULID. Never derive the key from mutable request state.
- Do not reuse keys across logically different operations.
- Log the key alongside the response for trace correlation.