Holds and idempotency

A hold keeps a place while a guest decides or pays. Pass hold_minutes to bookings_create and the booking comes back held with an expires_at; bookings_confirm makes it confirmed and bookings_cancel frees it. Pass an idempotency_key, a UUID you generate, so a retry returns the same booking instead of booking twice.

Plain text

States

heldThe place is kept until expires_at, and counts against capacity until then.
confirmedBooked. Without hold_minutes, bookings_create confirms at once.
cancelledThe place is free again. A payment taken online is refunded in full.

A hold past expires_at stays held but no longer takes a place: there's no expired status. Each change writes booking.held, booking.confirmed, booking.moved or booking.cancelled; a hold keeps its expires_at when it moves.

Hold a place

Pass hold_minutes, 5 to 1440. The booking comes back held, with its expires_at.

curl
curl -X POST https://daybag.io/api/v1/bookings \
  -H "Authorization: Bearer daybag_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "offering_id": "8f1c2e4a-0b7d-4c55-9a7e-3f2d1c0b9a88",
    "starts_at": "2026-12-05T15:00:00Z",
    "quantity": 2,
    "customer": {
      "email": "pat@example.com",
      "name": "Pat"
    },
    "hold_minutes": 30,
    "idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
  }'
201response
{
  "id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
  "reference": "0GWYRZ2V",
  "status": "held",
  "expires_at": "2026-12-01T17:34:11+00:00",
  "starts_at": "2026-12-05T15:00:00+00:00",
  "quantity": 2,
  "total_cents": 7000,
  "idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
}

The widget holds a place for 35 minutes while a guest pays on Stripe Checkout, and the payment confirms it. That guest has passed bot checks and is paying, so the hold counts against capacity only.

Confirm or cancel

Confirm before expires_at. Confirming twice returns the booking unchanged; past expires_at, a hold confirms only if its place is still free.

curl
curl -X POST https://daybag.io/api/v1/bookings/1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b/confirm \
  -H "Authorization: Bearer daybag_live_…"
200response
{
  "id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
  "reference": "0GWYRZ2V",
  "status": "confirmed",
  "expires_at": "2026-12-01T17:34:11+00:00",
  "starts_at": "2026-12-05T15:00:00+00:00",
  "quantity": 2,
  "total_cents": 7000,
  "idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
}

bookings_cancel frees the place of a hold or a booking. Cancelling twice changes nothing.

Idempotency keys

  • Generate a UUID per booking and send it as idempotency_key.
  • A retry with the same key and the same request returns the original booking, even if the slot has filled since, and writes no second event.
  • The same key with a different request is refused with idempotency_mismatch.
  • The request is the offering, session, start, quantity, periods, customer and hold_minutes; names, phones and notes don't count.

Hold limits

Open holds made over the API and MCP are capped, so no agent or script can sit on your inventory. Past a cap, a hold is refused with too_many_holds:

  • 3 open holds per guest email
  • 20 open holds per API key or OAuth client
  • Half of any one slot's capacity, rounded up

Errors

Errors come back as JSON with the HTTP status; MCP tools return them as code: message.

too_many_holds429From bookings_create. The hold would pass a cap. Confirm or cancel one first, or book outright.
idempotency_mismatch409From bookings_create. The key was already used for a different booking.
sold_out409From bookings_create. Not enough left at that time.
hold_expired409From bookings_confirm. The hold ran past expires_at and its place was taken.
booking_cancelled409From bookings_confirm. The booking was cancelled, so it can't be confirmed.
409response
{
  "error": {
    "code": "hold_expired",
    "message": "This hold expired and its place was taken"
  }
}