Skip to content
MyEscapeRoomDocs
Esc

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

    Embed booking widget

    Add the booking widget to your own website.

    Last updated

    There are two ways to put booking on your own website.

    • The booking widget is a ready-made form on an /embed/widget page, that you show on your site in an iframe. Nothing to build.
    • The Embed API is a set of endpoints, /api/embed/*, that your venue's API key unlocks. You build the booking flow in your own design and the API does the rest: availability, holding a session, pricing, payment and invoices.

    The booking widget#

    The widget shows your venue's rooms and free sessions, takes the player's contact details and their card payment, all inside the frame. It makes real bookings: the session is held at once, the booking is confirmed when the payment arrives, and the player gets the same confirmation email as a booking on MyEscapeRoom. It is built on the Embed API below, with the key you give it.

    The widget: pick a room, a day and a session. Then the player's details, then payment.
    The booking widget showing a room picker, a week of days, session times, group size and a Continue button

    Set it up#

    1. Create a key for the widget

      On DashboardSetupAPI, create a new key and name it for the widget. Leave every extra switch off: the widget does not need them, and it refuses a key that has one on. See API keys.

    2. Add your site to Allowed origins

      On the same page, add the website that will show the widget, for example https://www.your-venue.com. Only the sites on this list can show the widget in a frame. Any other site gets an empty frame.

    3. Paste the iframe

      Put the iframe on your page, with your key in key. Give it a height of 700 pixels or more: the payment step is the tallest, and the widget scrolls inside the frame if it needs more. Keep allow="payment": without it a browser does not offer Apple Pay or Google Pay inside the frame.

      Your page
      <iframe
        src="https://myescaperoom.eu/embed/widget?key=mer_…&brand=%23cc3311&accent=%234fb8c4&bg=%230e0c0a"
        title="Book an escape room"
        allow="payment"
        width="100%"
        height="780"
        style="border:0"
      ></iframe>

    The address#

    The widget is served from the MyEscapeRoom host at /embed/widget. A colour is a hex colour (remember to encode # as %23); one that is missing or is not a colour falls back to its default.

    ParameterMeaning
    keyRequiredThe widget's own API key, starting with mer_.
    roomOptionalA room's slug, as the rooms list of the Embed API gives it, to start on that room. Players can still pick another.
    brandOptionalThe main colour, for buttons and highlights, as a hex colour. Default #cc3311.
    accentOptionalThe colour of the total, as a hex colour. Default #4fb8c4.
    bgOptionalThe background, as a hex colour. Default #0e0c0a. Text turns dark on a light background and light on a dark one by itself.

    What a player goes through#

    1. Picks a room (when there is more than one), a day of the week and a session. Times are the venue's time, and only free sessions can be picked.
    2. Chooses the group size and gives a name, an email and, if your venue asks for it, a phone number. Where the room allows both, they choose to pay online or at the venue, and they can enter a discount code or a gift voucher code. The price is shown as the API works it out.
    3. Pays by card in the widget. The session is held for 15 minutes meanwhile. A room paid at the venue still takes the service fee online. If the bank asks for a 3-D Secure check, the player comes back to the widget, which carries on. Going back from the payment releases the session at once.
    4. Sees the confirmation, with the booking reference, the room, the time and the amount, once the booking is confirmed.

    The widget follows the same rules as everything else: your booking window, closed days, the staffing rule and each room's payment options decide what players can pick.

    If something does not work#

    What you seeWhat it means
    The frame is empty, or the browser says it refused to connectYour site is not in the key's Allowed origins. Add it exactly as visitors see it: https, with www if you use it, no path. https://www.your-venue.com and https://your-venue.com are two origins.
    “Its key is not valid”The key in the address is wrong or was revoked, or the venue is not live. Copy the key again from the dashboard, or create a new one.
    “This key has extra permissions”The key has one of the extra switches on (May cancel after the player deadline, May manage vouchers or May read guest details). The widget refuses such a key and only this app can frame it, because the key is readable in your page. Create a key with every switch off and use that one.
    No rooms, or no free sessionsThe widget shows the venue's active rooms and their free sessions, the same ones as your public page. A room needs a schedule to have sessions.
    “Confirming your booking” takes a whileThe booking confirms when Stripe reports the payment, usually within seconds. The player still gets the confirmation email if they close the page.

    To build the form in your own design instead, use the Embed API, as the rest of this page shows.

    Build your own booking flow#

    Everything a booking widget needs is available through the API: your rooms, free sessions, pricing, payment, gift vouchers and invoices.

    Before you start#

    • Your venue is live. To take online bookings that owe the venue money, it also needs Stripe connected.
    • You have a key, see API keys. Keep it on your server.
    • Your payment page can load https://js.stripe.com/v3/. The player pays inside your page with Stripe's own elements, so card details never touch your server.

    The shape of the flow#

    Your server calls MyEscapeRoom with the key and passes the results to your page. Your page shows rooms and times, collects the contact details, and runs Stripe.js with the clientSecret your server received. This keeps the key out of the browser. You can call the API straight from a page instead, if the page's origin is in your Allowed origins (a browser request from any other origin is refused with 403 ORIGIN_NOT_ALLOWED), but the key is then public: read API keys first.

    The examples use a small helper on your server. Every mer(…) call below is this:

    The helper
    // server.js: a tiny helper your server uses for every call.
    const BASE = "https://myescaperoom.eu";
    const YOUR_API_KEY = "mer_…"; // keep it on your server; read it from your own secret store
    
    export async function mer(path, init = {}) {
      const res = await fetch(BASE + path, {
        ...init,
        headers: {
          Authorization: "Bearer " + YOUR_API_KEY, // mer_…
          "Content-Type": "application/json",
          ...init.headers,
        },
      });
      const body = await res.json();
      if (!res.ok) throw Object.assign(new Error(body.code), { status: res.status, body });
      return body;
    }

    A worked example#

    1. List your rooms

      The answer is your venue and its active rooms. Show them from here, rather than maintaining a copy.

      curl https://myescaperoom.eu/api/embed/rooms \
        -H "Authorization: Bearer $MER_API_KEY"
      Response 200
      {
        "venue": {
          "id": "5b1e0c7a-8d3f-4e21-9a64-1f0b7c2d9e35",
          "name": "The Vault Ghent",
          "address": "Korenmarkt 1",
          "city": "Ghent",
          "country": "BE",
          "timezone": "Europe/Brussels",
          "contactEmail": "hello@vault.example",
          "contactPhone": "+32 9 000 00 00",
          "description": "Five rooms in a former bank.",
          "images": [],
          "bookerPhone": "optional",
          "playerCancellationNoticeHours": 24,
          "lat": 51.0543,
          "lng": 3.7174
        },
        "rooms": [
          {
            "id": "0b8e5c1a-3d47-4f6a-9a21-7c5e1d2b4f90",
            "venueId": "5b1e0c7a-8d3f-4e21-9a64-1f0b7c2d9e35",
            "slug": "the-vault",
            "name": "The Vault",
            "genre": "Heist",
            "difficulty": 3,
            "minPlayers": 2,
            "maxPlayers": 6,
            "durationMin": 60,
            "pricing": { "mode": "per-person", "pricePerPersonCents": 2500 },
            "rating": 4.7,
            "reviewCount": 38,
            "coverImage": "https://…/the-vault.jpg",
            "images": ["https://…/the-vault.jpg"],
            "description": "Break into the bank before the guards return.",
            "tags": ["team", "puzzles"],
            "certifiedGreat": false,
            "allowedPaymentModes": ["online", "at_location"]
          }
        ]
      }

      allowedPaymentModes says which paymentModes a booking of this room accepts. pricing says how it is priced, see Pricing modes below. venue.timezone is the zone to show times in. To read one room, use GET /api/embed/rooms/{slug}.

    2. Read the free sessions

      Ask for one day with date=2026-11-02, or a window with from (included) and to (excluded). Days are in the venue's time zone. The example is the seven days 2 to 8 November.

      curl "https://myescaperoom.eu/api/embed/rooms/the-vault/availability?from=2026-11-02&to=2026-11-09" \
        -H "Authorization: Bearer $MER_API_KEY"
      Response 200
      {
        "slots": [
          {
            "id": "7d2a91c4-5e60-4b8f-a3d1-92c0e4f61b08",
            "roomId": "0b8e5c1a-3d47-4f6a-9a21-7c5e1d2b4f90",
            "start": "2026-11-02T17:00:00.000Z",
            "end": "2026-11-02T18:00:00.000Z",
            "status": "available"
          }
        ],
        "total": 14,
        "hasMore": false,
        "hasLater": true
      }

      Only sessions that can be booked appear: never a started, held or booked one. The group size does not filter anything, because a group takes the whole room. With to, hasLater tells you whether any session lies beyond the window, so you know when "next week" would be empty. Page with offset until hasMore is false.

    3. Price it (optional)

      Show the player the total before they commit. The quote endpoint is public and changes nothing: it does not hold the session or spend a code.

      curl -X POST https://myescaperoom.eu/api/bookings/quote \
        -H "Content-Type: application/json" \
        -d '{"slotId":"7d2a91c4-5e60-4b8f-a3d1-92c0e4f61b08","teamSize":4,"paymentMode":"online"}'
      Response 200
      {
        "quote": {
          "roomSubtotalCents": 10000,
          "discountCents": 0,
          "voucherCents": 0,
          "platformFeeCents": 500,
          "totalCents": 10500,
          "onlineChargeCents": 10500,
          "dueAtVenueCents": 0
        },
        "allowedPaymentModes": ["online", "at_location"]
      }

      The service fee in these examples is 5.00 euros. It is a platform setting and is always read from the answer, never hard-coded. You can also pass discountCode or voucherCode to check them. A voucher code that is real but empty is reported as VOUCHER_INVALID here, on purpose.

    4. Create the booking

      Send the session, the contact and the group. The session is held for the player at once.

      curl -X POST https://myescaperoom.eu/api/embed/bookings \
        -H "Authorization: Bearer $MER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "slotId": "7d2a91c4-5e60-4b8f-a3d1-92c0e4f61b08",
          "guestName": "Sofie V.",
          "guestEmail": "sofie@example.com",
          "phone": "+32 470 00 00 00",
          "teamSize": 4,
          "paymentMode": "online"
        }'
      Response 201
      {
        "bookingId": "c41e8a07-2b9d-4d63-8f15-0a7e3b6c9d21",
        "ref": "BK-ABCD1234",
        "clientSecret": "pi_3Qx…_secret_…",
        "stripePublishableKey": "pk_live_…",
        "quote": {
          "roomSubtotalCents": 10000,
          "discountCents": 0,
          "voucherCents": 0,
          "platformFeeCents": 500,
          "totalCents": 10500,
          "onlineChargeCents": 10500,
          "dueAtVenueCents": 0
        },
        "booking": {
          "id": "c41e8a07-2b9d-4d63-8f15-0a7e3b6c9d21",
          "ref": "BK-ABCD1234",
          "slotId": "7d2a91c4-5e60-4b8f-a3d1-92c0e4f61b08",
          "roomId": "0b8e5c1a-3d47-4f6a-9a21-7c5e1d2b4f90",
          "venueId": "5b1e0c7a-8d3f-4e21-9a64-1f0b7c2d9e35",
          "guestName": null,
          "guestEmail": null,
          "phone": null,
          "teamSize": 4,
          "roomSubtotalCents": 10000,
          "discountCents": 0,
          "voucherCents": 0,
          "platformFeeCents": 500,
          "onlineChargeCents": 10500,
          "dueAtVenueCents": 0,
          "totalCents": 10500,
          "currency": "eur",
          "status": "pending",
          "paymentMode": "online",
          "paymentLast4": null,
          "feePaidAt": null,
          "paidAt": null,
          "refundedCents": null,
          "cancelledAt": null,
          "cancelledBy": null,
          "cancellationReason": null,
          "refundStatus": null,
          "createdAt": "2026-10-19T09:41:07.120Z"
        },
        "room": { "slug": "the-vault", "name": "The Vault" },
        "slot": { "start": "2026-11-02T17:00:00.000Z", "end": "2026-11-02T18:00:00.000Z" }
      }

      Keep the ref (BK-ABCD1234): it is how you look the booking up and how the player refers to it. Show quote.totalCents as the amount. The booking, room and slot are what GET /api/embed/bookings/{ref} returns, so you do not need a second call for the session's time. A booking always counts as a guest booking: it is not attached to a MyEscapeRoom player account. The guest's name, email and phone are null in the answer, as in the example, unless the key has May read guest details: you just sent them, so keep them on your side.

    5. Take the payment in the browser

      Pass the clientSecret and stripePublishableKey to your payment page and confirm with Stripe.js.

      Your payment page
      <!-- Your payment page, in the player's browser. -->
      <script src="https://js.stripe.com/v3/"></script>
      <div id="payment-element"></div>
      <button id="pay">Pay</button>
      
      <script>
        // clientSecret and stripePublishableKey come from your server's POST /api/embed/bookings.
        const stripe = Stripe(stripePublishableKey);
        const elements = stripe.elements({ clientSecret });
        elements.create("payment").mount("#payment-element");
      
        document.getElementById("pay").addEventListener("click", async () => {
          const { error } = await stripe.confirmPayment({
            elements,
            // Where a bank's 3-D Secure page sends the player back to.
            confirmParams: { return_url: "https://www.your-venue.com/booking/done?ref=BK-ABCD1234" },
            redirect: "if_required",
          });
          if (error) {
            // Show error.message and let the player try again with the same elements.
            return;
          }
          // Paid, or being processed. Now ask your server whether the booking is confirmed.
        });
      </script>

      The payment is created on the MyEscapeRoom platform account, so use the stripePublishableKey from the answer, and no connected-account option. Venues and keys never see card data.

    6. Wait until it is confirmed

      The browser coming back from Stripe does not confirm the booking. It becomes confirmed when Stripe's payment notification reaches MyEscapeRoom. Add a webhook endpoint with the booking.confirmed event and MyEscapeRoom tells your server the moment it happens, with the booking and the contact details in the body. Your payment page can then ask your own server whether it has heard. If you would rather not run an endpoint, read the booking from your server instead.

      curl https://myescaperoom.eu/api/embed/bookings/BK-ABCD1234 \
        -H "Authorization: Bearer $MER_API_KEY"
      Response 200
      {
        "booking": {
          "ref": "BK-ABCD1234",
          "status": "confirmed",
          "paymentMode": "online",
          "paymentLast4": "4242",
          "feePaidAt": "2026-10-19T09:42:31.000Z",
          "paidAt": "2026-10-19T09:42:31.000Z",
          "totalCents": 10500
          // …the other booking fields, as above
        },
        "room": { "slug": "the-vault", "name": "The Vault" },
        "slot": { "start": "2026-11-02T17:00:00.000Z", "end": "2026-11-02T18:00:00.000Z" }
      }

      booking.status goes from pending to confirmed, or to cancelled if the hold ran out unpaid. MyEscapeRoom emails the player their confirmation when it confirms. Do not tell the player a payment failed until the hold has ended: Stripe's delivery time is not ours to promise, and it retries a failed delivery.

    7. Cancel or release (when needed)

      # Release an unpaid hold, or cancel a confirmed booking inside the player notice.
      curl -X POST https://myescaperoom.eu/api/embed/bookings/BK-ABCD1234/cancel \
        -H "Authorization: Bearer $MER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"reason":"The group asked to move to another day"}'
      
      # After the player deadline, as the venue (needs "May cancel after the player deadline"):
      curl -X POST https://myescaperoom.eu/api/embed/bookings/BK-ABCD1234/cancel \
        -H "Authorization: Bearer $MER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"asVenue":true,"reason":"Room out of order"}'

      An unpaid booking is released: the session is free again at once and the payment is cancelled. Do this when a player leaves checkout. A confirmed booking is cancelled, with a refund of the room money paid online, never the service fee. The answer's booking.refundStatus is pending (the refund follows within minutes) or not_needed, and booking.refundedCents is the amount decided. A cancelled booking sends a booking.cancelled webhook, whoever cancelled it; a released one does not.

      By default a cancellation is held to the same notice as a player's own: the more generous of the notice in force when the booking was made and the venue's current one. You get 403 CANCELLATION_NOT_ALLOWED if neither lets players cancel, and 409 CANCELLATION_DEADLINE_PASSED once the deadline is behind. Only a key with May cancel after the player deadline can send asVenue: true, which cancels until the game starts.

    Request fields#

    The body of POST /api/embed/bookings:

    FieldMeaning
    slotIdRequiredThe session's id, from the availability answer.
    guestNameRequiredThe contact the venue asks for, up to 200 characters. Not a participant.
    guestEmailRequiredWhere the confirmation and the receipt go.
    teamSizeRequiredHow many people are coming. Whole number, within the room's minPlayers and maxPlayers.
    paymentModeRequiredonline or at_location. One of the room's allowedPaymentModes.
    phoneOptionalUp to 40 characters. Whether it is needed depends on the venue's setting: see below.
    discountCodeOptionalA discount code of the venue. Up to 64 characters.
    voucherCodeOptionalA gift voucher code of the venue. Up to 64 characters.
    invoiceOptionalThe "Need an invoice?" object. Belgian venues only.

    The booking stores no game language, notes or email language, so keep those on your side. An amount in the body is ignored: the price is always worked out on the server from the room's own pricing.

    Phone. The venue's bookerPhone setting (in venue on the rooms answer) decides: required refuses a booking without a plausible number, optional keeps what you send, and hidden drops any number sent. Mirror it in your form.

    Payment modes#

    paymentModeWhat is charged
    onlineThe player pays the whole total now: room price less any discount or voucher, plus the service fee.
    at_locationThe player pays the service fee now, and the rest at the venue (dueAtVenueCents). Only when the room's allowedPaymentModes includes it.

    Pricing modes#

    pricing.modeThe room price
    per-personpricePerPersonCents multiplied by teamSize.
    per-gameOne price, priceCents, whatever the group size.
    tieredThe price for exactly that group size, from the tiers map. A size with no tier cannot be priced.
    contactNot bookable through the API: a booking is refused with 422 CONTACT_PRICING.

    The price breakdown#

    totalCents = roomSubtotalCents − discountCents − voucherCents + platformFeeCents, and the total equals onlineChargeCents + dueAtVenueCents. The service fee is always paid online and is never discounted. The venue receives 100% of the room price, less Stripe's card fee on that share.

    Payment handling#

    • The hold. A new booking holds its session for 15 minutes. A sweep runs every minute, so an unpaid session is back on sale about a minute after the hold ends.
    • Always a payment. Even with at_location, or with a voucher that covers the whole room, the service fee is paid online, so every booking returns a clientSecret.
    • Failed card attempts. Let the player try again with the same payment element. The hold keeps the session meanwhile.
    • Leaving checkout. Call the cancel endpoint to free the session at once. Do not rely on the page unloading.
    • Confirmation. Subscribe to booking.confirmed and booking.cancelled in your webhooks, or read GET /api/embed/bookings/{ref} from your server.booking.cancelled is sent for a confirmed booking only, not for an unpaid hold that is released or runs out.
    • Stripe not answering. 502 STRIPE_ERROR means the session was released, so try again.

    Gift vouchers#

    The same key sells gift vouchers from your own shop. You choose the amount (100 to 100,000 cents) and MyEscapeRoom mints the code. A voucher is valid for 180 days, for the whole venue or for one room (roomSlug).

    1. Create the sale

      curl -X POST https://myescaperoom.eu/api/embed/vouchers \
        -H "Authorization: Bearer $MER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"amountCents":10000,"buyerName":"Sofie V.","buyerEmail":"sofie@example.com"}'
      Response 201
      {
        "voucher": {
          "code": "GIFT-7K3M-Q9XD-2VBN",
          "status": "pending_payment",
          "amountCents": 10000,
          "remainingCents": 10000,
          "expiresAt": "2027-04-17T09:50:12.000Z",
          "roomSlug": null,
          "buyerName": "Sofie V.",
          "buyerEmail": "sofie@example.com",
          "paidAt": null,
          "soldOnline": false,
          "createdAt": "2026-10-19T09:50:12.000Z"
        },
        "clientSecret": "pi_3Qy…_secret_…",
        "stripePublishableKey": "pk_live_…"
      }
    2. Confirm the payment

      Confirm clientSecret with Stripe.js in the buyer's browser, exactly as for a booking. No service fee is charged on a voucher sale.
    3. Wait until it is active

      Poll GET /api/embed/vouchers/{code} until voucher.status is active, then show the buyer the code. MyEscapeRoom emails the voucher to buyerEmail once it is active, unless you sent notifyBuyer: false. Or let a webhook (voucher.sold) tell you.

    To spend a voucher, add its code to a booking. The service fee is still paid online.

    Book with a voucher
    // Spend a voucher on a booking: add its code to the booking request.
    await mer("/api/embed/bookings", {
      method: "POST",
      body: JSON.stringify({
        slotId: "7d2a91c4-5e60-4b8f-a3d1-92c0e4f61b08",
        guestName: "Sofie V.",
        guestEmail: "sofie@example.com",
        teamSize: 4,
        paymentMode: "online",
        voucherCode: "GIFT-7K3M-Q9XD-2VBN", // and/or discountCode: "ESCAPE10"
      }),
    });
    // quote.voucherCents is what the voucher covered. The service fee is still paid online.

    Selling a voucher you were paid for in your own shop (collectPayment: false), listing vouchers and cancelling one you sold yourself need a key with May manage vouchers: use it from your server only. See API reference: Vouchers for every endpoint, including cancelling and the voucher's invoice. The buyer's buyerName and buyerEmail come back in voucher answers only to a key with May manage vouchers or May read guest details; for any other key they are null.

    Invoices#

    At a Belgian venue a booker can ask for invoices. A venue outside Belgium answers 422 INVOICE_NOT_AVAILABLE and, if you sent the invoice with the booking, nothing is booked. Once the booking is confirmed, MTEC Games VOF emails the buyer two PDFs: the service fee invoice, and the room invoice issued in the venue's company name (after the game when the room is paid at the venue, and none when a voucher paid the whole room). The PDFs are only emailed, never served through the API.

    Send the buyer's details with the booking in an invoice field, or ask afterwards, until 23:59 Brussels time on the 15th of the month after the payment (the game, for a room paid at the venue). An EU business needs its VAT number, which is checked with VIES first. A VIES that does not answer never fails the booking.

    curl -X POST https://myescaperoom.eu/api/embed/bookings/BK-ABCD1234/invoice-request \
      -H "Authorization: Bearer $MER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "buyerKind": "business",
        "name": "Test Buyer NV",
        "vatNumber": "BE0000000097",
        "address": "Straat 1",
        "postalCode": "1000",
        "city": "Brussel",
        "country": "BE",
        "email": "billing@testbuyer.be"
      }'
    Response 201: the invoice state
    {
      "available": true,
      "requested": true,
      "deadline": "2026-11-15T22:59:59.999Z",
      "canRequest": true,
      "feeInvoice": "pending",
      "roomInvoice": "pending",
      "roomAfterGame": false,
      "invoices": []
    }
    • GET /api/embed/bookings/{ref}/invoices returns this state again, with each issued invoice listed in invoices: its number, kind (service_fee or room) and amount. pdfPath is always null.
    • feeInvoice is none, pending or issued. roomInvoice is none, pending, issued, venue_issues (the venue invoices the room itself) or voucher_paid.
    • One request per booking: a second is 409 INVOICE_ALREADY_REQUESTED. Correct a request with PUT (from your server) until an invoice is issued, after which it is 409 INVOICE_ALREADY_ISSUED.
    • Past the deadline, 422 INVOICE_DEADLINE_PASSED.

    Reading your bookings#

    List the venue's bookings to mirror them in your own calendar or accounting. They come earliest session first, with from (included) and to (excluded) on the session's start as ISO 8601 with an offset, an optional status (pending, confirmed or cancelled), and the usual limit and offset.

    # Bookings whose session starts in November 2026. The + in an offset may be sent as %2B.
    curl "https://myescaperoom.eu/api/embed/bookings?from=2026-11-01T00:00:00%2B01:00&to=2026-12-01T00:00:00%2B01:00&status=confirmed&limit=50" \
      -H "Authorization: Bearer $MER_API_KEY"

    Troubleshooting#

    What you seeWhat it means
    401 UNAUTHORIZED on every callThe key is wrong or revoked, or the venue is not live. See API keys.
    409 SLOT_UNAVAILABLEThe session was taken or is held by someone else, or it falls in the venue's staffing window and no host has said they can work it. Offer another session.
    422 PAYMENT_MODE_NOT_ALLOWEDThe room does not accept that mode. Read the room's allowedPaymentModes first.
    422 INVALID_TEAM_SIZEteamSize is outside the room's minPlayers to maxPlayers.
    400 PHONE_REQUIRED or PHONE_INVALIDThe venue requires a phone number (venue.bookerPhone is required), and none or an invalid one was sent.
    503 STRIPE_NOT_CONFIGUREDAn online booking that still owes the venue money was requested, and the venue has not finished connecting Stripe.
    A booking stays pendingThe payment was not completed, or Stripe's notification has not arrived yet. Wait until the hold has ended (a booking.confirmed webhook may still arrive until then) before you tell the player it failed.
    A browser shows a CORS errorThe page's origin is not in your Allowed origins (the API answered 403 ORIGIN_NOT_ALLOWED and did nothing), or the key is wrong. Neither answer carries the Access-Control-Allow-Origin header, so the browser hides it.
    guestName, guestEmail, phone and cancellationReason are nullThe key does not have May read guest details. Switch it on for a key that stays on your server.

    The full list of codes is on Authentication & errors.

    MyEscapeRoom Docs