Skip to main content

Idempotency

If a write times out or the connection drops, you cannot tell whether the request never arrived or arrived and succeeded before the answer was lost. Retrying the first is right; retrying the second creates a duplicate checklist, a second comment or a second task.

An Idempotency-Key removes that doubt. Send the same key with the retry, and if the original request succeeded you get its original answer back instead of the work being done again.

Sending a Key​

Add an Idempotency-Key header to any POST, PUT, PATCH or DELETE request. Use a value that is unique to the operation you mean — a new GUID per operation, or an id from your own system such as an order number.

POST https://api.checkflow.io/v3/checklists
X-API-KEY: your-api-key-here
Content-Type: application/json
Idempotency-Key: 5f0c9a2e-8b41-4d3a-9e76-1c2b3a4d5e6f

{
"templateKey": "0e7ad584-7788-4ab1-95a6-ca0a5b444cbb",
"name": "Invoice Review — INV-2041"
}

If the answer does not arrive, send exactly the same request with the same key again.

The header is optional. A request without it is processed normally, and nothing is stored.

What Happens on a Retry​

SituationResponse
First use of the keyThe request is processed. If the response status is below 500, it is stored against the key for 24 hours.
Same key, same request, original finishedThe stored response is returned — same status, same body — with the header Idempotency-Replayed: true. Nothing is done again.
Same key, same request, original still running409 CONFLICT — "A request with Idempotency-Key '…' is still in progress. Retry shortly." Wait a moment and retry.
Same key, different request409 CONFLICT with field set to Idempotency-Key — "… was already used for a different request." Use a new key.
Same key after 24 hoursThe key has expired and the request is processed as new.

A replayed response carries its own new X-Request-Id, and it still counts against your rate limit.

What Counts as the Same Request​

Two requests are the same when they have the same method, the same path and the same body, byte for byte. A retry that adds an optional property, reorders the JSON or changes whitespace is a different request and is refused with 409. Build the body once and resend the stored bytes.

The query string, headers other than the key, and the API key itself are not part of the comparison — but keys are scoped to your workspace, so another workspace can never collide with yours.

Failures Are Not Stored​

A 5xx response, a timeout on our side or a request that fails before producing a response is not stored. The key is released and the next request with it is processed as a first attempt. A 4xx refusal is stored, because retrying an invalid request unchanged would be refused again.

Rules​

  • Keys can be up to 255 characters. A longer key is refused with 400 VALIDATION_ERROR.
  • Keys are scoped to the workspace. The same key used by two of your integrations for different requests conflicts, so make keys unique — GUIDs are the simplest way.
  • On GET requests the header is accepted and ignored. Reads are already safe to repeat.
  • Committing a template draft is asynchronous: an idempotent retry of the commit returns the original 202 Accepted, and you then poll the commit status as usual.

A Safe Retry Loop​

import { randomUUID } from 'node:crypto';

async function createChecklist(body) {
const key = randomUUID(); // one key for this operation, reused on every retry
const payload = JSON.stringify(body); // one body, resent byte for byte

for (let attempt = 1; attempt <= 4; attempt++) {
try {
const response = await fetch('https://api.checkflow.io/v3/checklists', {
method: 'POST',
headers: {
'X-API-KEY': process.env.CHECKFLOW_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: payload,
signal: AbortSignal.timeout(30_000),
});

if (response.status === 409 || response.status >= 500) {
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
continue; // still in progress, or failed on our side: retry
}

return await response.json(); // success, a replay, or a 4xx to report
} catch {
await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); // timeout or network error
}
}

throw new Error('Could not create the checklist.');
}

A 409 whose message says the key was already used for a different request will not succeed on retry — in production code, check the message or the field and stop rather than looping.

In the MCP Server​

MCP clients cannot set per-call headers, so the MCP server's write tools take an idempotencyKey argument instead. It works the same way, against the same store, with the same 24-hour lifetime. See How the MCP Server Works.

  • Errors — the 409 CONFLICT code and the others.
  • Checklists — the create call that most needs a key.
  • Template Drafts — the one asynchronous write.
  • Rate Limits — replays are counted like any other request.