Skip to content

Idempotency keys on the write API

A network failure does not say whether the write happened. The request may have reached the server, committed, and lost only its response. Repeating it is how a caller finds out — and without a key, repeating it is how a second invoice gets created and a second document number gets burnt.

An Idempotency-Key request header makes the repeat safe: the first request’s outcome is remembered under that key, and a later request carrying it replays that outcome instead of running again.

Idempotency-Key is optional. A request without one behaves exactly as it always has, and is not protected.

  • The key is caller-chosen, 1–255 printable ASCII characters. Anything longer or outside that range is rejected with 400 core.request_rejected.
  • A key is scoped to the workspace. Two workspaces using the same string never collide.
  • A key is remembered for 24 hours from the moment the write commits.
  • A key is minted fresh per call, so two deliberate calls remain two writes. Reuse one only to repeat a call that may already have happened.

The second request answers with the response body the first request produced — byte for byte, including the document number it drew. The status is the route’s own, so a replayed create still answers 201.

The body is a snapshot of that first answer, not a fresh read. A record edited between the two requests still replays as it was, which is what a caller retrying a lost response asked for.

A request that arrives while the first one with its key is still running gets 409 core.request_in_flight. The outcome is not settled yet, so this is the one error worth retrying after a short pause.

A write that fails releases its key. The next request with that key is a real attempt, not a replay of a failure.

Route Honours the key
POST/PATCH/DELETE on /api/business-object/... Yes
POST .../archive, POST .../restore Yes
POST /api/business-object/.../actions/:action Yes
POST /api/actions/:namespace/:name Yes
Every route under /api/drafts Yes
POST /api/functions/:namespace/:name No — a function is a read over POST
File upload, roles, role assignments No

@onerp/client sends a key on every write it makes, so the browser app, the warehouse app, Node-RED flows and any integration built on the client are covered without doing anything.

In Redis, under the workspace’s key space, with a 24-hour expiry. The reservation is taken before the handler runs and confirmed after the unit of work commits, which is what lets two concurrent duplicates be told apart from one.

A stored answer is the size of the record it carries: a sales order serializes to about 900 bytes at one line and 2 KB at five, growing roughly 310 bytes per line. Ten thousand writes a day hold about 20 MB for the 24 hours they live.

Redis cannot join the SQL transaction, so the guarantee is not absolute: a process that dies between the commit and the confirm leaves the key unwritten, and a repeat would write again. The window is milliseconds.

An idempotency key deduplicates a repeat of one request. It does not make an operation idempotent in itself: two calls with two keys are two writes, which is what partial invoicing and partial fulfilment need them to be.

It also does not reach anything that never goes over HTTP. A workflow, a job processor or the seeder calls the manager in process, with no request to carry a header, so a replayed event or a re-run job writes again.