Safe mutations
Marea honorsIdempotency-Key on every POST and PATCH. Replays of the same key + same body return the same response (status + body, including failures). This lets agents retry network errors without double-creating resources.
Header contract
Invalid headers (too short, too long, or containing non-printable bytes) return
400 invalid_idempotency_key. The regex is ^[\x20-\x7e]{1,255}$.
Match key
A stored idempotency record is keyed on the triple(keyId, method+path, Idempotency-Key). The body is hashed (canonical JSON, sorted keys, SHA-256 hex) and compared on every replay.
- Cross-key replays don’t work. Different API keys with the same
Idempotency-Keyare independent requests. - Cross-endpoint replays don’t work.
POST /v1/storefrontsandPOST /v1/storefronts/:id/productswith the sameIdempotency-Keyare independent.
Outcomes
Missing the header
SendingPOST / PATCH without Idempotency-Key is not an error. The request proceeds and Marea returns a Marea-Recommendation: include-idempotency-key response header to nudge you. Skipping the header is fine for one-shot calls; include it for any retry-prone integration.
The four 409 / 410 error shapes
in_flight — same key still processing
owned; the other waits or replays.
conflict — same key, different body
invalid_idempotency_key — format error
idempotency_snapshot_unavailable — oversize response
What is stored
The first successful response is persisted as(keyId, method+path, Idempotency-Key) → { status, body }. Failed responses are also stored — a replay of a failed call returns the same failure. The stored snapshot is held to back replays; there is no client-controlled TTL.
Convention
Stripe-style. If you already use a Stripe retry library that drivesIdempotency-Key, it works on Marea unchanged.
Verification in code
src/api/middleware/idempotency.ts— header parsing + replay vs. owned vs. conflict branching.src/api/services/apiIdempotency.service.ts—acquireOrReplay,complete,fail, snapshot cap (SNAPSHOT_MAX_BYTES).