Skip to content
MyEscapeRoomDocs
Esc

    Type to search. Use the arrow keys and Enter to open a result.

    Booking

    Transactional booking and slots

    MethodPathWhat it does
    POST/api/bookingsCreate a booking (transactional, row-level locking)
    POST/api/bookings/quotePrice a booking without creating one
    POST/api/bookings/{bookingId}/releaseRelease an abandoned checkout
    POST/api/bookings/payment-returnThe booking behind a PaymentIntent, for the return from a redirect payment
    POST/api/slots/previewPreview bulk slot creation (no commit)
    POST/api/slotsCommit bulk slot creation

    Create a booking (transactional, row-level locking)#

    POST/api/bookingsPublic (session optional)

    Totals are always recomputed server-side from the room's pricing; a client-supplied amount is ignored. Ownership comes from the session, never from the body. The payment mode is validated against the venue's policy — a mode the venue does not offer is refused. Every booking holds its slot for 15 minutes and returns a clientSecret: the player's service fee (the amount is the quote's platformFeeCents) is charged online in all cases, plus the room price when the mode is online. The booking is only confirmed once the webhook reports the payment succeeded. Discount and gift voucher codes are redeemed inside the booking transaction and returned if the payment never completes. A signed-in booker may also say who is playing, as either a team booking (teamId — one of their own teams — and memberPlayerIds, the player.ids of the members coming, the booker left out) or a player booking (players, other players' public player IDs, and guests, names). The two are mutually exclusive. The booker is always a participant, taken from the session; team members picked are accepted at once, while a player named by ID is only invited and is credited with the outcome once they accept. Everyone named, the booker included, must fit in teamSize. An anonymous body that names anyone is refused. The venue's bookerPhone setting (on the room detail's venue) decides the phone: required refuses a booking without one that looks like a phone number, hidden drops any phone sent, and optional keeps whatever was sent, trimmed, with a blank phone stored as none. The response carries the quote the booking was actually written with — the same breakdown the PaymentIntent was built from — so the payment step never shows an amount taken from an earlier quote. An optional invoice object ("Need an invoice?": buyerKind business or person, name, vatNumber for an EU business, address, postalCode, city, country, email) is stored with the booking, in its transaction, for a Belgian venue only; an EU VAT number is checked with VIES first, and a VIES that does not answer never fails the booking. MTEC Games VOF's service fee invoice is emailed once the booking is confirmed.

    Example request

    POST /api/bookings
    curl -X POST https://myescaperoom.eu/api/bookings \
      -H "Content-Type: application/json" \
      -d '{"slotId":"slot-123","guestName":"Sofie V.","guestEmail":"sofie@example.com","teamSize":4,"paymentMode":"online"}'

    Example request body

    json
    {
      "slotId": "slot-123",
      "guestName": "Sofie V.",
      "guestEmail": "sofie@example.com",
      "phone": "+32 470 00 00 00",
      "teamSize": 4,
      "paymentMode": "online"
    }

    Responses

    StatusMeaning
    201{ bookingId, ref, clientSecret, quote }
    400Invalid body; or who is playing was refused: PARTICIPANTS_REQUIRE_PLAYER (no signed-in player), NOT_A_TEAM_MEMBER, PLAYER_NOT_FOUND, DUPLICATE_PARTICIPANT, TOO_MANY_PARTICIPANTS; or the venue requires a phone: PHONE_REQUIRED (none sent), PHONE_INVALID (not a phone number)
    409Slot unavailable, already full, or already started
    422Invalid team size or contact-priced room; or INVOICE_NOT_AVAILABLE (an invoice for a venue outside Belgium)
    502Stripe did not respond; the held seats were released
    503The venue has no connected Stripe account

    Price a booking without creating one#

    POST/api/bookings/quotePublic (session optional)

    Read-only: a discount or voucher code checked here is only reported as valid, never spent. The booking endpoint recomputes everything itself from the room's own pricing, so nothing returned from this endpoint is trusted back. This endpoint is unauthenticated, so a voucher code that exists but has no balance left is reported as VOUCHER_INVALID rather than VOUCHER_EMPTY — the distinction would confirm which guessed codes are real. POST /api/bookings keeps it.

    Example request

    POST /api/bookings/quote
    curl -X POST https://myescaperoom.eu/api/bookings/quote \
      -H "Content-Type: application/json" \
      -d '{"slotId":"slot-123","teamSize":4,"paymentMode":"online","discountCode":"ESCAPE10"}'

    Example request body

    json
    {
      "slotId": "slot-123",
      "teamSize": 4,
      "paymentMode": "online",
      "discountCode": "ESCAPE10"
    }

    Responses

    StatusMeaning
    200{ quote: { roomSubtotalCents, discountCents, voucherCents, platformFeeCents, totalCents, onlineChargeCents, dueAtVenueCents }, allowedPaymentModes }
    404Slot or room not found
    409The venue is not live, the slot has started, or the slot is taken
    422Invalid team size, disallowed payment mode, invalid code, or contact-priced room

    Release an abandoned checkout#

    POST/api/bookings/{bookingId}/releasePublic (session optional)

    Gives back the hold of a pending booking whose player left checkout, so the slot is free at once rather than when the 15-minute hold runs out, and cancels its PaymentIntent. A guest proves the booking is theirs with the PaymentIntent's clientSecret (from POST /api/bookings): its id must be the booking's PaymentIntent, and the secret must be the one Stripe holds. The signed-in booker may send {} instead. Anything else is a 404, never a 403, so a booking id cannot be probed. Whether the booking is unpaid is asked of Stripe, not read from its status: a payment that went through before its webhook landed counts as paid. Releasing a booking that is already cancelled answers 200 with it.

    Parameters

    NameInTypeDescription
    bookingIdrequiredpathstring

    Request body

    FieldTypeDescription
    clientSecretoptionalstring

    Example request body

    json
    {
      "clientSecret": "pi_123_secret_456"
    }

    Responses

    StatusMeaning
    200{ booking } — cancelled, now or already
    400INVALID_JSON; INVALID_INPUT
    404BOOKING_NOT_FOUND — no such booking, or neither the client secret nor the session proves it
    409BOOKING_PAID — the payment went through (the booking stands); PAYMENT_PROCESSING — Stripe is still processing it, try again later
    429RATE_LIMITED — too many attempts from this address (20 per 10 minutes, shared with POST /api/bookings/payment-return); see Retry-After
    502STRIPE_ERROR — Stripe could not be asked whether the booking is paid; nothing was released, the hold runs out by itself

    The booking behind a PaymentIntent, for the return from a redirect payment#

    POST/api/bookings/payment-returnPublic (session optional)

    Where Bancontact, iDEAL and 3-D Secure redirects send the player back to. The PaymentIntent's clientSecret (from POST /api/bookings) is the proof, so it works without a session and from another device: its id must be a booking's PaymentIntent, and the secret must be the one Stripe holds. Anything else is a 404, never a 403. The secret is sent in the body so it stays out of the API's own access logs. intentStatus is Stripe's status for the payment, which can be succeeded while the booking is still pending until the webhook lands. No guest details are returned. room and venue carry only what the ticket prints and are read without the live filter, so a venue paused since still shows its ticket. cancellation is released for an unpaid hold given back and cancelled for a paid booking cancelled later, null otherwise. roomLive is whether the venue is live, so whether the room page still exists for players. party (the team name, the other players with a kind of member, invited or guest, and the registered players who could be invited to a team of the group) is sent only when the caller's player session is the booking's booker, else null.

    Request body

    FieldTypeDescription
    clientSecretrequiredstring

    Example request body

    json
    {
      "clientSecret": "pi_123_secret_456"
    }

    Responses

    StatusMeaning
    200{ bookingId, ref, bookingStatus, cancellation, intentStatus, room, roomLive, venue, slot, teamSize, totalCents, onlineChargeCents, dueAtVenueCents, paymentMode, party }
    400INVALID_JSON; INVALID_INPUT
    404BOOKING_NOT_FOUND — no booking, or the client secret does not prove it
    429RATE_LIMITED — too many attempts from this address (20 per 10 minutes, shared with POST /api/bookings/{bookingId}/release); see Retry-After
    502STRIPE_ERROR — Stripe could not be asked about the payment

    Preview bulk slot creation (no commit)#

    POST/api/slots/previewSession

    Requires the caller to hold at least the admin venue role.

    Responses

    StatusMeaning
    200Candidate slots + conflicts

    Commit bulk slot creation#

    POST/api/slotsSession

    Requires the caller to hold at least the admin venue role.

    Responses

    StatusMeaning
    200Created + skipped slots
    MyEscapeRoom Docs