Skip to content

Idempotency and conflicts

Safely retry booking requests and build a deliberate recovery path for atomic inventory conflicts.

Updated View as Markdown

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

server/book-order.jsjs
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:

200 replay responsejson
{
  "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_progress with Retry-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.

409 responsejson
{
  "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

  1. Your platformCreates an order

    Assign one immutable booking reference before submitting the booking request.

  2. Payment providerAuthorizes payment

    When possible, authorize first so a real booking conflict can be voided instead of refunded.

  3. SeatLayerAttempts the atomic booking

    The request succeeds once, returns a genuine inventory conflict, or becomes an unknown result after a lost response.

  4. SuccessCapture and confirm

    After booking succeeds, capture payment and confirm your order.

  5. 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close