Idempotent requests
Retry any POST safely with an Idempotency-Key. What is stored, for how long, and what each answer means.
A network can fail after FurniOps did the work but before you read the answer. Retrying blindly could start a second run or create a second endpoint. An idempotency key makes the retry safe: the first request with a key does the work, and every retry with that key gets the first answer back without doing anything again.
Send a key with every POST
# One key per logical request: generate it once, and send the same one on every retry.
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://api.furniops.com/v1/sources/src_034XqBaL2mRLTZPXyRbaUc/runs \
-H "Authorization: Bearer $FURNIOPS_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{}'- Every
POSTrequiresIdempotency-Key, exceptPOST /v1/sources/{id}/test, which changes nothing. The API reference marks it on each operation. A missing or malformed key answers400idempotency_key_invalid. - A key is 1 to 255 visible ASCII characters (no spaces). A UUID is ideal. The quoted form of the
IETF draft (
"5d0b…") is accepted too. - Keys belong to your organisation: another organisation's identical key never collides with yours.
PATCH,PUTandDELETEdo not take a key: applying them twice has the same effect as once. Changes to sources and webhook endpoints are also guarded byIf-Match.
With the SDK
The SDK sends a fresh key with every POST and reuses it on its own retries, so a call that
times out and is retried is never applied twice. You do nothing.
To make a retry safe across processes (a job that crashed and runs again tomorrow), choose the key yourself, from something stable, and keep it:
// The SDK sends a fresh key with every POST and reuses it on its own retries. To make a retry
// across processes (a job that crashed) safe too, choose the key yourself and keep it.
const key = `run-${randomUUID()}`;
const run = await furniops.sources.createRun(
"src_034XqBaL2mRLTZPXyRbaUc",
{},
{ idempotencyKey: key },
);
// Sending the same key and body again within 24 h answers this same run, byte for byte,
// with Idempotent-Replayed: true. Nothing runs twice.
console.log(run.id, run.lastResponse.headers.get("idempotent-replayed"));What each answer means
| Situation | Answer |
|---|---|
| First request with the key | The work is done; the answer is stored with the key. |
| Same key, same request, within 24 hours | The stored answer, byte for byte, with Idempotent-Replayed: true. Nothing runs again. |
| Same key, a different request | 422 idempotency_key_reused. Use a new key for a new request. |
| Same key while the first is still running | 409 idempotency_request_in_progress with Retry-After: 1. Wait and retry: you then get the stored answer. |
| The first request failed | Nothing is stored: the work and the key roll back together, so a retry with the same key runs again. |
| More than 24 hours later | The key is forgotten; the request runs as new. |
"The same request" means the same method, path and query, and the same JSON body. Key order and
whitespace do not matter: {"a":1,"b":2} and { "b": 2, "a": 1 } are the same request.
Because a failure stores nothing, there is no stuck state to clean up: a request that crashed half way did nothing, and the retry starts fresh.
Answers that carry a secret
Creating a webhook endpoint or rotating its secret answers the secret once. A retry with the same key still answers it, so a lost response does not lose the secret; FurniOps stores such answers encrypted.
Choosing keys
- One key per intended action, not per HTTP attempt. Generate it when you decide to act, store it with the job, and reuse it on every retry.
- Do not derive keys from the time or a counter that resets.
- A key stable across a whole job (
sync-2026-10-04-src_…) makes the job safe to rerun the same day, and new the next.