FurniOpsDocs

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 POST requires Idempotency-Key, except POST /v1/sources/{id}/test, which changes nothing. The API reference marks it on each operation. A missing or malformed key answers 400 idempotency_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, PUT and DELETE do not take a key: applying them twice has the same effect as once. Changes to sources and webhook endpoints are also guarded by If-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:

run.ts
// 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

SituationAnswer
First request with the keyThe work is done; the answer is stored with the key.
Same key, same request, within 24 hoursThe stored answer, byte for byte, with Idempotent-Replayed: true. Nothing runs again.
Same key, a different request422 idempotency_key_reused. Use a new key for a new request.
Same key while the first is still running409 idempotency_request_in_progress with Retry-After: 1. Wait and retry: you then get the stored answer.
The first request failedNothing is stored: the work and the key roll back together, so a retry with the same key runs again.
More than 24 hours laterThe 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.

On this page