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": []
}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 |
| Direct/box-office 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
created
└─> payment_authorized
├─> seatlayer_booked ─> payment_captured ─> confirmed
├─> inventory_conflict ─> payment_voided ─> needs_reselection
└─> booking_unknown ─> retry same bookingRefIf 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
Each event processes hold, book, block, and release transitions through one authoritative serialized inventory writer. Two competing requests cannot both transition the same seat from free; one commits first and the other observes 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.