# 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. URL: https://daybag.io/docs/holds ## States - held: The place is kept until `expires_at`, and counts against capacity until then. - confirmed: Booked. Without `hold_minutes`, `bookings_create` confirms at once. - cancelled: The 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`. ```bash 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" }' ``` Response 201: ```json { "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. ```bash curl -X POST https://daybag.io/api/v1/bookings/1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b/confirm \ -H "Authorization: Bearer daybag_live_…" ``` Response 200: ```json { "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 - too_many_holds (429), from bookings_create: The hold would pass a cap. Confirm or cancel one first, or book outright. - idempotency_mismatch (409), from bookings_create: The key was already used for a different booking. - sold_out (409), from bookings_create: Not enough left at that time. - hold_expired (409), from bookings_confirm: The hold ran past `expires_at` and its place was taken. - booking_cancelled (409), from bookings_confirm: The booking was cancelled, so it can't be confirmed. Errors come back as JSON with the HTTP status; MCP tools return them as `code: message`. ```json {"error":{"code":"hold_expired","message":"This hold expired and its place was taken"}} ```