Skip to content
Apier
Apier.no
Guides

Idempotency and safe retries

Make write requests retry-safe with the Idempotency-Key header — at-most-once execution, a 24-hour replay window, and the four reservation outcomes an agent must handle.

[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-06-04]

Autonomous agents retry. A request times out, a worker restarts mid-call, or a network hop drops the response after the server already committed the write — and the agent, seeing no answer, sends it again. For a read that is harmless. For a write that files a VAT return or delegates a system user, a blind retry risks doing the work twice.

Apier closes that gap with the Idempotency-Key header. Send the same key on a retry and you get the original outcome back — the side effect happens at most once, no matter how many times the request lands.

The contract

Attach one header to a write request:

Idempotency-Key: 9f1c2a7e-4b6d-4e2a-8c10-5d7b3e9a1f04
  • Format. 1–255 printable ASCII characters. A UUID or ULID per logical operation is the safest choice — unique, collision-free, easy to log.
  • Scope. A key is scoped to your consumer account and the specific endpoint. The same string used on a different endpoint, or by a different consumer, never collides — so you can mint keys locally without coordinating globally.
  • Window. A reservation lives for 24 hours. After it expires the key is free to reuse; within the window the original result is replayed.
  • Body fingerprint. The server fingerprints the request body with canonical JSON before comparing. { "a": 1, "b": 2 } and { "b": 2, "a": 1 } are the same logical request, so reordered fields or reformatted whitespace on a retry still replay cleanly instead of being mistaken for a different request.

Which endpoints support it

The header is honoured on Apier's write (POST) endpoints. Read endpoints (GET) are naturally idempotent and ignore it.

EndpointIdempotency-Key
POST /api/v1/actions/executeRequired — a missing key returns IDEMPOTENCY_KEY_REQUIRED
POST /api/v1/altinn/list-acting-capacityOptional, recommended
POST /api/v1/auth/system-user/delegateOptional, recommended

The live filing path makes the key mandatory on purpose: it is the one call that produces an irreversible government submission, so it refuses to run without the double-submit guard in place. The MVA filing guide walks the full filing flow (approval token, scopes, signed receipt).

The four outcomes

Every keyed request resolves to exactly one of these. Branch on the HTTP status, never on response text.

SituationStatusWhat it meansKey headers
First time this key is seenthe handler's own status (e.g. 200)The request executed; its response is now cached for 24h.Idempotent-Replay: false
Same key, same body, againthe original statusReplay — the handler did not run again; you get the stored response byte-for-byte.Idempotent-Replay: true, Idempotent-Replay-Age-Ms: <ms>
Same key, still in flight409 IDEMPOTENCY_IN_PROGRESSA concurrent request with this key hasn't finished. Wait and retry.Retry-After: 2
Same key, different body422 IDEMPOTENCY_KEY_MISMATCHYou reused a key for a genuinely different request. Mint a new key.
Malformed key400 VALIDATION_FAILEDThe key isn't 1–255 printable ASCII characters.

Worked example

First attempt — the write executes and the response is cached:

POST /api/v1/actions/execute HTTP/1.1
Host: apier.no
Authorization: Bearer apr_test_<your_key_here>
Idempotency-Key: 9f1c2a7e-4b6d-4e2a-8c10-5d7b3e9a1f04
X-Approval-Token: <token-from-approval-step>
Content-Type: application/json

{
  "org_number": "999999999",
  "action_type": "mva_melding",
  "period": "2026-T1",
  "payload": {}
}
HTTP/1.1 200 OK
Idempotent-Replay: false
Content-Type: application/json

{ "success": true, "receipt": { "...": "..." } }

The connection drops before your client reads that response, so the agent retries — same key, same body:

HTTP/1.1 200 OK
Idempotent-Replay: true
Idempotent-Replay-Age-Ms: 1840
Content-Type: application/json

{ "success": true, "receipt": { "...": "..." } }

Identical body, no second filing. Idempotent-Replay-Age-Ms tells you how long ago the original ran — useful for deciding whether to trust a cached result or surface it to a human for review.

If instead the retry carries a changed body under the same key, Apier refuses it rather than guess which one you meant:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{ "success": false, "error_code": "IDEMPOTENCY_KEY_MISMATCH", "...": "..." }

The full Norwegian-language explanation envelope on that error is described in the error-handling guide.

What gets cached

Only responses worth replaying are stored:

  • Successes only. 2xx responses are cached; transient non-2xx codes are not frozen behind the key, so a later retry re-executes cleanly.
  • 5xx is never cached. A server error usually means the side effect did not commit (or is indeterminate). Freezing it for 24h would make every retry hit the same stale failure — instead, a retry with the same key re-runs the write.
  • Stateful headers are stripped. Set-Cookie and Authorization are never replayed; a stale session cookie is always wrong.

When the guard itself can't run

If Apier cannot reach the reservation store to check whether your key was already used, it fails closed: the write is rejected with 503 UPSTREAM_UNAVAILABLE rather than risk executing twice. A double-submitted filing cannot be un-sent; a 503 can simply be retried. Re-send with the same key once the service recovers.

Agent checklist

  • One key per logical operation. Generate it before the first attempt and reuse the same key on every retry of that operation. A fresh key per retry defeats the entire mechanism.
  • Never mutate the body between retries of the same operation — that earns a 422 IDEMPOTENCY_KEY_MISMATCH. If the request genuinely changed, it deserves a new key.
  • Honour Retry-After on a 409 IDEMPOTENCY_IN_PROGRESS instead of hammering the endpoint.
  • Mind the 24-hour window. Beyond it the key is forgotten and a re-send executes as a brand-new request.
  • Pair it with an approval token on the live filing path. Idempotency stops accidental duplicates; the approval token enforces the human-in-the-loop gate. Both headers are required on POST /api/v1/actions/execute.

Read next: MVA filing · Error handling · Authentication.