Idempotency
Nexa is designed to tolerate retries at every layer. This page lists the idempotency scopes and the keys that make each one safe.
Why it matters
Distributed systems retry. Airline feeds replay. Operators double-click. Background jobs re-enqueue. Without idempotency you end up with duplicate cases, double bookings, and double emails — every one of which is visible to passengers and expensive to unwind.
Nexa treats idempotency as the default, not an opt-in.
Scopes
| Scope | Key | Prevents |
|---|---|---|
| Case creation | externalEventId | Duplicate cases from retried airline webhooks |
| Demand request | (caseUrn, type, requestedAtBucket) | Double-approved surges |
| Allocation wave | (caseUrn, demandRequestUrn, waveSeq) | Re-running the same wave during a worker retry |
| Booking | (caseUrn, waveUrn, groupId) | Double bookings for the same group |
| Notification | (groupId, type, channel, reservationUrn) | Duplicate vouchers |
| Payment tokenization | (caseUrn, reservationUrn) | Double card charges |
| Agent run | (itemUrn, runSeq) | Duplicate AI Model invocations on the same review item |
Every scope is TTL-indexed so keys don't grow unbounded. The default TTL is 7 days, which is long enough for every expected retry path.
How keys are derived
Keys come from upstream, not from request IDs:
externalEventIdis the airline's ID. The airline is the source of truth for "did we already submit this?" — Nexa respects it.(caseUrn, waveUrn, groupId)is derived from platform state, not from the worker's job ID.(groupId, type, channel, reservationUrn)is derived from the thing being notified about, not from the send request.
This matters because a worker retry uses the same key, but a new operator action (resend, re-book) uses a different key — so retries are deduped while genuine new actions go through.
Behavior on duplicate
- POST /cases with an existing
externalEventIdreturns200with the existing case URN and aX-Nexa-Replayed: trueheader. No new case is created. - Booking worker sees an existing
CONFIRMEDreservation for(caseUrn, waveUrn, groupId)and returns it. No provider call is made. - Notification worker with an existing
SENTrecord skips the send and returns the existing record.
The replayed response is byte-identical to the original where possible, so downstream systems can't tell (and don't need to).
Operator-initiated duplication
Sometimes operators want a "duplicate":
- Resend a voucher — the operator wants a second email sent because the passenger never got the first.
- Rebook a cancelled reservation at a new hotel.
Both of these are new logical actions, not retries. They have their own endpoints with their own keys (resendSeq, rebookSeq) so they coexist with the idempotent retry behaviour.
Diagnostics
curl "https://us-central1.api.nexastudio.io/admin/idempotency/lookup?scope=BOOKING&caseUrn=…&waveUrn=…&groupId=…"
Returns whether the key exists, when it was first seen, and the URN of the result it's protecting. Useful when debugging "why didn't this send a second time?"