API reference contents
API reference
Documentation
Idempotency
A write that gets retried must not happen twice. One header, chosen by the caller, makes every write in this API safe to send again.
Why it exists
A request that times out has an unknown outcome. It may never have arrived, or it may have arrived and its response was lost on the way back. From the client's side those two look identical, and the ordinary response to an identical situation is to try again.
For a read that is free. For a write it is not. The concrete case is
POST /work/{id}/complete on a row whose action is approve: the
first call sends the message, and a retry sends it again.
A retried approve is two emails to the same counterparty. That is a person on the other side reading the same request twice and drawing a conclusion about the business that sent it. It costs a relationship rather than a request, and it is not visible in any log the client keeps, because from the client's side nothing failed twice.
Some writes are harmless when repeated. Completing a row that is already complete does nothing further. Setting a snooze of seven days twice sets seven days. Writing the same note twice leaves two notes. The point of the header is that the caller does not have to know which is which: it makes all of them safe.
The header
Idempotency-Key: complete-47-2026-10-13
Any string, up to 200 characters. Declared as a parameter on every write:
/work/{id}/complete, /work/{id}/snooze,
/work/{id}/draft, /work/{id}/run and
/arrangements/{id}/notes. The OpenAPI description lists it on
POST /chat as well.
The key is scoped to the desk as well as the value, so the same string used against two desks is two separate records and neither interferes with the other.
What happens
First call with a given key: the write is performed, and the response is remembered
against that key. A later call with the same key returns the first response, unchanged,
with 200, and the write is not performed again.
# First call - the work is done
curl -s -X POST "https://outcomedev.com/api/v1/work/47/complete" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: complete-47-2026-10-13" \
-d '{"note":"Approved by phone, reference 88421."}'
# Same call again - the same bytes come back, and nothing else happens
curl -s -X POST "https://outcomedev.com/api/v1/work/47/complete" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: complete-47-2026-10-13" \
-d '{"note":"Approved by phone, reference 88421."}'
The replay is the whole stored response, so delivered,
delivery_note and status are the values from the first call
rather than recomputed. A client can treat a replay as the answer it never received.
One warning about replaying with a different body
The key is the only thing that is matched, not the request. A second call carrying the same key and a different note returns the first response and writes nothing. From the caller's side that looks like the write succeeded with the new note, and it did not. This is why the key has to identify the intent rather than the attempt: the same key means "the same thing", and the service takes the caller at their word.
Choosing a key
A key has to be the same across every retry of one intent, and different between two intents. Deriving it from what the write is about satisfies both:
complete-47-2026-10-13
snooze-47-7d-2026-10-13
note-18-2026-10-13
run-58-2026-10-13
| Do | Because |
|---|---|
| Derive the key from the action plus the row | The same intent retried always produces the same string |
| Keep it constant for the whole retry loop | A fresh key per attempt is the same as sending no key at all |
| Use a new key for a genuine second action | A note written twice on purpose should be two notes |
| Keep it under 200 characters | Only the first 200 are used, so a longer value can collide with another |
| Keep it to plain ASCII | It travels as a header, and non-ASCII header values are where obscure bugs live |
A random UUID works too, provided the client generates it once and stores it before the first attempt. The failure mode of a random key is generating it inside the retry: each attempt gets a new one and the protection is gone while looking present in the code.
The honest limitations
The record is held in memory by one process, and it does not survive a restart. That is the limit that matters. In detail:
- It is not durable. A restart of the service clears every remembered response. A retry after a restart is a new write.
- It is per process. If the API is ever served by more than one process, a retry routed to a different one is a new write. This is a known consequence of the current design rather than a property to rely on.
- It expires. A record is honoured for 24 hours. A retry after that is treated as a first call. This is generous for a retry loop and deliberately not permanent.
- It is bounded. The store holds a fixed maximum number of entries, and older ones are dropped when it fills. A key from a very busy period can be evicted before its 24 hours are up.
- It truncates the key at 200 characters. Two keys sharing a first 200 characters are the same key.
- It does not compare bodies. The key alone decides, as described above.
What follows from all of that, in practice: use the header on every write, retry within
minutes rather than hours, and do not treat it as a database of what has already happened.
For a durable record of what was written, read
GET /activity, which is where
the desk keeps its own account of what it sent.
There is no endpoint that lists idempotency records, and no way to inspect one. The record exists to make a retry safe and for nothing else.
When not to send one
Omitting the header is allowed on every write. Do that only when a repeat is genuinely wanted: a second note on an arrangement recording a second call, or a deliberate second attempt after a run returned a failure that has since been fixed.
The header is not a lock. It does not stop two different callers from writing at the same
time, and it does not queue a second call behind a first that is still running. Sending a
second run with a new key while the first is in flight is two attempts at the
same work.
Related
- Actions — the writes the header applies to.
- Errors — and the one code worth retrying with the same key.
- Code examples — working clients that set the header.