Skip to main content

Safe mutations

Marea honors Idempotency-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-Key are independent requests.
  • Cross-endpoint replays don’t work. POST /v1/storefronts and POST /v1/storefronts/:id/products with the same Idempotency-Key are independent.

Outcomes

Missing the header

Sending POST / 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

Wait 1s and retry with the same key. Two concurrent calls on the same key are serialized server-side — only one wins owned; the other waits or replays.

conflict — same key, different body

Generate a fresh key. Don’t reuse it. The most common cause is an agent retrying with edits (“same call, but change the email”) — that’s a new request and needs a new key.

invalid_idempotency_key — format error

Fix the header. UUID v4 is the safe default.

idempotency_snapshot_unavailable — oversize response

Response snapshots are capped at 100 KB (Firestore doc-size headroom). On the rare oversize response, the original is marked oversize and future replays return 410 — drop the header and reissue.

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 drives Idempotency-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.tsacquireOrReplay, complete, fail, snapshot cap (SNAPSHOT_MAX_BYTES).