Networks time out, payment webhooks repeat, and workers retry jobs. Use one immutable bookingRef for one business order so repeated booking calls cannot create a second sale.
Safe booking retries
async function bookOrder({ eventKey, holdId, orderId }) {
return fetch(
`https://api.seatlayer.io/v1/events/${eventKey}/book`,
{
method: "POST",
headers: {
authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
holdId,
bookingRef: orderId,
}),
},
);
}Calling this again with the same orderId is safe. If the first request already booked the seats, an idempotent replay succeeds without a second inventory or credit transition:
{
"ok": true,
"booked": []
}The Idempotency-Key header
Calls that create something or move money take an Idempotency-Key header.
Send a new key for each new action, and the same key when you retry it:
- Same key, same request: you get the first answer again, and nothing happens twice. Keys are kept for 30 days.
- Same key, different request:
409 idempotency_conflict. - Same key while the first call is still running:
409 idempotency_in_progresswithRetry-After. Wait and retry.
The operation support matrix lists which calls replay this way.
Season orders need a key
Three calls on a season order refuse to run without one, with 400 idempotency_key_required:
| Call | Route |
|---|---|
| Refund | POST /v1/seasons/:key/orders/:orderId/refund |
| Cancel without a refund | POST /v1/seasons/:key/orders/:orderId/cancel |
| Cancel one date | POST /v1/seasons/:key/orders/:orderId/dates/:eventKey/cancel |
Each moves money or takes tickets away, so a lost response must never turn
into a second refund. The dashboard runs the same actions under the same
names. A refund’s id (rfo_…) comes from your account and the key, so the
same key sent from the dashboard and from your server reaches one refund. A
refund or date cancel that is still settling answers 202; retry with the
same key to read the result. See the Seasons API.
What counts as a conflict
A booking under a different reference, an expired or invalid hold, missing inventory, or an invalid capacity allocation returns 409.
{
"error": "conflict",
"conflicts": [
{
"label": "A-12",
"status": "booked"
}
]
}SeatLayer applies inventory mutations atomically. If any required item conflicts, nothing in that request is newly booked.
Recovery by operation
| Operation | Likely reason | Correct response |
|---|---|---|
| Buyer hold | Someone else acquired inventory after render | Refresh and let the buyer choose again |
| Booking held items | Hold expired or inventory no longer belongs to it | Void/refund as required, then reselect |
| Platform direct booking | Item is not free | Surface the conflicting labels to the operator |
| Blocking | Item is already held, booked, or blocked | Skip intentionally or ask the operator |
Do not retry an actual 409 in a loop. Idempotency protects unknown/repeated delivery; it does not make unavailable inventory available.
Payment-safe state machine
Your platformCreates an order
Assign one immutable booking reference before submitting the booking request.
Payment providerAuthorizes payment
When possible, authorize first so a real booking conflict can be voided instead of refunded.
SeatLayerAttempts the atomic booking
The request succeeds once, returns a genuine inventory conflict, or becomes an unknown result after a lost response.
SuccessCapture and confirm
After booking succeeds, capture payment and confirm your order.
Conflict or timeoutRecover deliberately
Void or refund a real conflict; for an unknown result, retry with the same booking reference until it resolves.
If your provider captures before booking, replace the void with the appropriate refund or reversal. Persist enough state for a background worker or webhook reconciliation job to finish an uncertain request.
Why concurrency is safe
For hold, book, block, and release, exactly one competing request wins. Two requests cannot both move the same seat out of free; one succeeds and the other sees the new state.
Your application still needs idempotency for its own order creation and payment operations. SeatLayer’s bookingRef does not automatically deduplicate your checkout route.
Test cases
- Send the same booking request twice; only one sale is recorded.
- Simulate a lost first response, then retry with the same reference.
- Try a different reference against the already booked seats; receive
409. - Expire the hold before booking; no partial inventory changes.
- Compete for the same seat from two browser sessions.
- Ensure payment compensation runs only for a real conflict.
- Ensure background retries preserve the original reference.
Read the booking endpoint and the complete checkout example.