---
title: "Idempotency and conflicts"
description: "Safely retry booking requests and build a deliberate recovery path for atomic inventory conflicts."
---

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

```js title="server/book-order.js"
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:

```json title="200 replay response"
{
  "ok": true,
  "booked": []
}
```

<Aside type="caution" title="A timeout is an unknown result">
  Do not generate a new order or `bookingRef` after a timeout. Reconcile your existing order and retry with the same reference.
</Aside>

## 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`.

```json title="409 response"
{
  "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

<FlowDiagram
  label="Payment-safe booking recovery flow"
  steps={[
    {
      actor: "Your platform",
      title: "Creates an order",
      detail: "Assign one immutable booking reference before submitting the booking request.",
      tone: "platform",
    },
    {
      actor: "Payment provider",
      title: "Authorizes payment",
      detail: "When possible, authorize first so a real booking conflict can be voided instead of refunded.",
      tone: "payment",
    },
    {
      actor: "SeatLayer",
      title: "Attempts the atomic booking",
      detail: "The request succeeds once, returns a genuine inventory conflict, or becomes an unknown result after a lost response.",
      tone: "seatlayer",
    },
    {
      actor: "Success",
      title: "Capture and confirm",
      detail: "After booking succeeds, capture payment and confirm your order.",
      tone: "outcome",
    },
    {
      actor: "Conflict or timeout",
      title: "Recover deliberately",
      detail: "Void or refund a real conflict; for an unknown result, retry with the same booking reference until it resolves.",
      tone: "outcome",
    },
  ]}
/>

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

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](/server-api/booking) and the [complete checkout example](/examples/complete-checkout).