Embed
A venue's own booking flow (API-key auth + CORS)
| Method | Path | What it does |
|---|---|---|
| GET | /api/embed/rooms | The key's venue and its active rooms |
| GET | /api/embed/rooms/{slug} | White-label room snapshot for the booking widget |
| GET | /api/embed/rooms/{slug}/availability | White-label availability for the widget |
| GET | /api/embed/bookings | The venue's bookings by slot start |
| POST | /api/embed/bookings | Create a booking through the white-label widget (shares the booking engine) |
| GET | /api/embed/bookings/{ref} | One booking by its ref (e.g. BK-ABCD1234) |
| POST | /api/embed/bookings/{ref}/invoice-request | Ask for a booking's invoices after checkout |
| PUT | /api/embed/bookings/{ref}/invoice-request | Correct a booking's invoice request before it is issued |
| GET | /api/embed/bookings/{ref}/invoices | Where a booking's invoices stand |
| POST | /api/embed/bookings/{ref}/cancel | Cancel a booking, or release a pending hold |
The key's venue and its active rooms#
/api/embed/roomsAPI key (embed)Each room carries allowedPaymentModes: the paymentModes a booking of it accepts, online (the room price is paid now) and/or at_location (the room price is paid at the venue; the service fee is still paid online). It is the room's own policy, or the venue's when the room has none. A booking with any other mode is refused with 422 PAYMENT_MODE_NOT_ALLOWED. pricing says how the room is priced: per-person (price × teamSize), per-game (one price), tiered (the price for exactly that group size; a size with no tier cannot be priced) or contact (not bookable through the API).
Example request
curl https://myescaperoom.eu/api/embed/rooms -H "Authorization: Bearer $MER_API_KEY"Responses
| Status | Meaning |
|---|---|
200 | { venue, rooms: [{ …room, allowedPaymentModes }] } |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
White-label room snapshot for the booking widget#
/api/embed/rooms/{slug}API key (embed)Parameters
| Name | In | Type | Description |
|---|---|---|---|
slugrequired | path | string |
Responses
| Status | Meaning |
|---|---|
200 | { room: { …room, allowedPaymentModes }, venue } |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | ROOM_NOT_FOUND — no such room at the key's venue |
White-label availability for the widget#
/api/embed/rooms/{slug}/availabilityAPI key (embed)The room's available slots (never a started, held or booked one), soonest first. Days are YYYY-MM-DD in the venue's time zone: date is one day, from (inclusive) and to (exclusive) a window. A value that is not a real day (an ISO instant, for one) is a 400 INVALID_INPUT, never ignored. The group size filters nothing: a slot is booked whole. A page is at most limit slots (200 unless asked, 500 at most); total counts every slot the filters match and hasMore says whether offset plus this page falls short of it, so page with offset until it is false. With to, hasLater says whether any slot lies on or after that day. A venue may only offer the sessions a host has said they can work for its first few days ahead (its staffing window, set by the venue): inside it, a session with no host yet is left out of every availability answer, and a booking for one is refused with 409 SLOT_UNAVAILABLE. Further ahead every free session is listed.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
slugrequired | path | string | |
date | query | string | One venue-local day. |
from | query | string | First venue-local day of a window, inclusive. Plain YYYY-MM-DD only, no time or offset. |
to | query | string | Day the window ends, exclusive: that day is not included. from=2026-11-02&to=2026-11-09 is the seven days 2 to 8 November. Plain YYYY-MM-DD only, no time or offset. |
limit | query | integer | |
offset | query | integer |
Responses
| Status | Meaning |
|---|---|
200 | { slots, total, hasMore, hasLater? } |
400 | INVALID_INPUT — a day that is not YYYY-MM-DD, or a bad limit or offset |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | ROOM_NOT_FOUND — no such room at the key's venue |
The venue's bookings by slot start#
/api/embed/bookingsAPI key (embed)Parameters
| Name | In | Type | Description |
|---|---|---|---|
from | query | string | ISO 8601 with offset; slot start, from inclusive, to exclusive. A + in the offset may be sent raw or as %2B. |
to | query | string | ISO 8601 with offset; slot start, from inclusive, to exclusive. A + in the offset may be sent raw or as %2B. |
status | query | string | pending, confirmed or cancelled |
limit | query | integer | At most 200 |
offset | query | integer |
Responses
| Status | Meaning |
|---|---|
200 | { bookings: [{ booking, room, slot }], total }. booking.guestName, booking.guestEmail, booking.phone and booking.cancellationReason are null unless the key was granted "May read guest details" on the dashboard's API page (grant it only to a key kept on your own server). |
400 | INVALID_INPUT |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
Create a booking through the white-label widget (shares the booking engine)#
/api/embed/bookingsAPI key (embed)The same rules as POST /api/bookings, the venue's bookerPhone setting included: a venue that requires the booker's phone refuses a widget booking without one (400 PHONE_REQUIRED or PHONE_INVALID). A booking through a key is always a guest booking: userId and who-is-playing fields are not for it.
Body: slotId, guestName (the contact the venue asks for), guestEmail, teamSize and paymentMode are required; phone, discountCode, voucherCode and invoice are optional. paymentMode is online or at_location, and the room's allowedPaymentModes (GET /api/embed/rooms) says which it takes. The booking stores no game language, notes or e-mail locale, so keep those on your side.
Invoices. At a Belgian venue the booker can ask for invoices ("Need an invoice?"): send invoice (buyerKind business or person, name, vatNumber for an EU business, address, postalCode, city, country, email) with the booking, or later POST /api/embed/bookings/{ref}/invoice-request. Once the booking is confirmed MTEC Games VOF emails the buyer two PDFs: its own service fee invoice, and the room invoice it issues in the venue's company's name (after the game when the room is paid at the venue; none when a gift voucher paid the whole room; the venue is emailed to invoice the room itself when its invoicing profile is not complete). An EU VAT number is checked with VIES first, and a VIES that does not answer never fails the booking. A venue outside Belgium answers 422 INVOICE_NOT_AVAILABLE and nothing is booked. GET /api/embed/bookings/{ref}/invoices says where they stand.
Hold and payment. The booking is pending and holds its slot for 15 minutes from creation; a sweep runs every minute, so an unpaid slot is back on sale within about a minute after that. It becomes confirmed when Stripe's payment_intent.succeeded reaches MyEscapeRoom, which is what the player returning from the payment page does not do: poll GET /api/embed/bookings/{ref} and read booking.status until it is confirmed or cancelled. Stripe's delivery time is not ours to promise (normally seconds; Stripe retries a failed delivery with backoff for days), so keep polling until the hold has ended before telling a player their payment failed. To give a hold back sooner, POST /api/embed/bookings/{ref}/cancel.
Money. Everything is recomputed from the room's pricing; an amount in the body is ignored. quote.roomSubtotalCents is the room price for teamSize; discountCents and voucherCents come off it; platformFeeCents is MyEscapeRoom's flat service fee, always paid online and never discounted, on top of the room price. totalCents = room − discount − voucher + service fee, which is onlineChargeCents + dueAtVenueCents: with online the whole total is charged now, with at_location only the service fee is and the rest is dueAtVenueCents. A site's displayed total should be quote.totalCents.
Checks run in this order and the first refusal is the answer: 1) the body's shape, all fields at once (400 INVALID_JSON, INVALID_INPUT); 2) the slot exists at the key's venue (404 SLOT_NOT_FOUND); 3) the venue is live (409 VENUE_UNAVAILABLE); 4) teamSize is within the room's minPlayers and maxPlayers (422 INVALID_TEAM_SIZE); 5) the slot has not started (409 SLOT_IN_PAST); 6) the slot is still available, and, inside the venue's staffing window, a host has said they can work it (409 SLOT_UNAVAILABLE); 7) paymentMode is one the room allows (422 PAYMENT_MODE_NOT_ALLOWED); 8) the venue's phone rule (400 PHONE_REQUIRED, PHONE_INVALID); 9) who is playing (400); 10) the discount and voucher codes (422 DISCOUNT_INVALID, VOUCHER_INVALID, VOUCHER_EMPTY); 11) the room can be priced online (422 CONTACT_PRICING). Only then is the slot held, and a code spent or a slot taken in the same instant by someone else is a 409.
Request body
| Field | Type | Description |
|---|---|---|
slotIdrequired | string | |
guestNamerequired | string | |
guestEmailrequired | string | |
phoneoptional | string | |
teamSizerequired | integer | |
paymentModerequired | string | |
discountCodeoptional | string | |
voucherCodeoptional | string | |
invoiceoptional | object | "Need an invoice?": the same object as POST /api/embed/bookings/{ref}/invoice-request. Belgian venues only (422 INVOICE_NOT_AVAILABLE) |
Example request
curl -X POST https://myescaperoom.eu/api/embed/bookings \
-H "Authorization: Bearer $MER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slotId":"slot-123","guestName":"Sofie V.","guestEmail":"sofie@example.com","teamSize":4,"paymentMode":"online"}'Example request body
{
"slotId": "slot-123",
"guestName": "Sofie V.",
"guestEmail": "sofie@example.com",
"teamSize": 4,
"paymentMode": "online"
}Responses
| Status | Meaning |
|---|---|
201 | { bookingId, ref, clientSecret, quote, stripePublishableKey, booking, room, slot } — booking, room and slot (its start and end) are what GET /api/embed/bookings/{ref} returns, so no second call is needed. Confirm the payment with Stripe.js: stripe = Stripe(stripePublishableKey); stripe.confirmPayment({ clientSecret, … }). The booking holds its slot for 15 minutes and is confirmed by the webhook once paid. booking.guestName, booking.guestEmail, booking.phone and booking.cancellationReason are null unless the key was granted "May read guest details" on the dashboard's API page (grant it only to a key kept on your own server). |
400 | INVALID_JSON; INVALID_INPUT; PHONE_REQUIRED or PHONE_INVALID — the venue's phone rule |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | SLOT_NOT_FOUND — no such slot at the key's venue |
409 | SLOT_UNAVAILABLE — taken or held, or inside the venue's staffing window with no host available; SLOT_IN_PAST; VENUE_UNAVAILABLE; DISCOUNT_EXHAUSTED; VOUCHER_EMPTY |
422 | INVALID_TEAM_SIZE; PAYMENT_MODE_NOT_ALLOWED — see the room's allowedPaymentModes; CONTACT_PRICING; DISCOUNT_INVALID; VOUCHER_INVALID; INVOICE_NOT_AVAILABLE — an invoice at a venue outside Belgium |
502 | STRIPE_ERROR — the slot was released, retry |
503 | STRIPE_NOT_CONFIGURED — the venue cannot take cards yet |
One booking by its ref (e.g. BK-ABCD1234)#
/api/embed/bookings/{ref}API key (embed)Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Responses
| Status | Meaning |
|---|---|
200 | { booking, room, slot }. booking.guestName, booking.guestEmail, booking.phone and booking.cancellationReason are null unless the key was granted "May read guest details" on the dashboard's API page (grant it only to a key kept on your own server). |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | BOOKING_NOT_FOUND |
Ask for a booking's invoices after checkout#
/api/embed/bookings/{ref}/invoice-requestAPI key (embed)For a booker who asks after the booking was made. Any key may call it: the ref is the booker's own, as for cancelling. The rules are the portal's. Belgian venues only, for a pending or confirmed booking, or a paid cancellation (its service fee is kept, so only the service fee invoice is issued). Allowed until 23:59 Brussels time on the 15th of the month after the last tax point: the service fee's payment, and the room's payment or, paid at the venue, the game. One request per booking (409 INVOICE_ALREADY_REQUESTED); correct it with PUT. An EU VAT number is checked with VIES first. On a confirmed booking the invoices are issued at once and emailed to the buyer as PDFs (over Peppol as well to a Belgian business that receives them); on a pending one, once it is confirmed.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Request body
| Field | Type | Description |
|---|---|---|
buyerKindrequired | string | |
namerequired | string | The person's or the company's name |
vatNumberoptional | string | Required for a business in the EU, with its country prefix |
addressrequired | string | |
postalCoderequired | string | |
cityrequired | string | |
countryrequired | string | ISO 3166-1 alpha-2, e.g. BE |
emailrequired | string | The invoices are emailed here |
Example request body
{
"buyerKind": "business",
"name": "Test Buyer NV",
"vatNumber": "BE0712345630",
"address": "Straat 1",
"postalCode": "1000",
"city": "Brussel",
"country": "BE",
"email": "billing@testbuyer.be"
}Responses
| Status | Meaning |
|---|---|
201 | The booking's invoice state, as GET /api/embed/bookings/{ref}/invoices returns it |
400 | INVALID_JSON; INVALID_INPUT — for example an EU business without its VAT number |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | BOOKING_NOT_FOUND — no such booking at the key's venue |
409 | INVOICE_ALREADY_REQUESTED |
422 | INVOICE_NOT_AVAILABLE (a venue outside Belgium, a released hold, or nothing to invoice) or INVOICE_DEADLINE_PASSED |
Correct a booking's invoice request before it is issued#
/api/embed/bookings/{ref}/invoice-requestAPI key (embed)The same body and checks as POST. Once either invoice is issued, or the venue was emailed to invoice the room itself, it is 409 INVOICE_ALREADY_ISSUED: a correction then is a credit note and a reissue, which only an operator can do.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Responses
| Status | Meaning |
|---|---|
200 | The booking's invoice state |
400 | INVALID_JSON; INVALID_INPUT |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | BOOKING_NOT_FOUND, or INVOICE_REQUEST_NOT_FOUND (nothing to correct) |
409 | INVOICE_ALREADY_ISSUED |
422 | INVOICE_NOT_AVAILABLE or INVOICE_DEADLINE_PASSED |
Where a booking's invoices stand#
/api/embed/bookings/{ref}/invoicesAPI key (embed){ available (a Belgian venue), requested, deadline (ISO; null while a tax point is still to come), canRequest, feeInvoice (none, pending, issued), roomInvoice (none, pending, issued, venue_issues — the venue invoices it itself, voucher_paid — a gift voucher paid the room, so there is none), roomAfterGame, invoices: [{ id, kind (service_fee or room), type, number, issuerName, issueDate, totalGrossCents, currency, status, pdfPath }] }. pdfPath is always null here: the PDFs are emailed to the buyer, never served through this API.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Responses
| Status | Meaning |
|---|---|
200 | The booking's invoice state |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this |
404 | BOOKING_NOT_FOUND |
Cancel a booking, or release a pending hold#
/api/embed/bookings/{ref}/cancelAPI key (embed)A pending booking (a customer who abandoned checkout) is released: its slot is freed at once and its PaymentIntent cancelled. Whether it is unpaid is asked of Stripe, not read from its status: a payment that went through before its webhook landed counts as paid (BOOKING_PAID; retry once the booking is confirmed). A confirmed booking is cancelled with a refund: the room money paid online goes back to the player, never the service fee; a booking paid at the venue refunds nothing.
Who is cancelling. By default the call is a player's cancellation, held to the venue's player cancellation policy exactly as the player's own cancel in MyEscapeRoom is: allowed until the venue's notice before the slot's start (the more generous of the notice the booking was made under and the venue's current one), and refused after it with 409 CANCELLATION_DEADLINE_PASSED. A venue that lets players cancel at no time answers 403 CANCELLATION_NOT_ALLOWED. After the deadline only the venue cancels: send asVenue: true, which cancels until the slot's start as the dashboard does. That needs a key the venue has granted "May cancel after the player deadline" on its dashboard API page, and otherwise answers 403 API_KEY_NOT_PERMITTED. Grant it only to a key used from your own server: a key in a web page can be called with any body by any visitor. A key without it still cancels for players inside the policy.
The refund itself follows within minutes, so the answer's booking.refundStatus is pending (or not_needed when nothing is owed) and booking.refundedCents the amount decided. This replaces the earlier 409 BOOKING_PAID for confirmed bookings: a caller that relied on it to refuse paid bookings now cancels them. The optional body { reason } (up to 500 characters) is recorded on the booking, with cancelledAt and cancelledBy (api), for either kind of cancellation; for a confirmed booking it is also given in the player's email, while a released pending booking emails nobody. Cancelling a booking that is already cancelled is a no-op that answers 200 with the booking, and its body is not read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Request body
| Field | Type | Description |
|---|---|---|
reasonoptional | string | |
asVenueoptional | boolean | Cancel as the venue, until the slot's start, past the player deadline. Needs a key granted that permission, else 403 API_KEY_NOT_PERMITTED. |
Example request body
{
"reason": "The group asked to move to another day",
"asVenue": false
}Responses
| Status | Meaning |
|---|---|
200 | { booking, room, slot } — cancelled, now or already; for a confirmed booking with refundStatus and refundedCents. booking.guestName, booking.guestEmail, booking.phone and booking.cancellationReason are null unless the key was granted "May read guest details" on the dashboard's API page (grant it only to a key kept on your own server). |
400 | INVALID_JSON; INVALID_INPUT — the reason is over 500 characters |
401 | UNAUTHORIZED — no key, an unknown or revoked one, or a venue that is not live. The 401 carries no CORS headers, so a browser sees a failed request rather than this body. |
403 | ORIGIN_NOT_ALLOWED — the request carries an Origin that is not one of the venue's allowed origins (and not the portal's own site). Nothing is done. A call with no Origin header (from a server) is never refused for this; CANCELLATION_NOT_ALLOWED — the venue lets players cancel at no time, so only the venue cancels (asVenue); API_KEY_NOT_PERMITTED — asVenue was sent with a key the venue has not granted that permission |
404 | BOOKING_NOT_FOUND |
409 | CANCELLATION_DEADLINE_PASSED — the player deadline has passed, so only the venue cancels (asVenue); GAME_STARTED — the slot has started, nothing is cancelled; BOOKING_PLAYED — a session outcome is logged; BOOKING_PAID — a pending booking whose payment went through, retry once it is confirmed; PAYMENT_PROCESSING — Stripe is still processing a pending booking's payment, try again later |
502 | STRIPE_ERROR — Stripe could not be asked whether a pending booking is paid; nothing was released, retry |