Skip to content
MyEscapeRoomDocs
Esc

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

    Webhooks

    Have your own server told when a booking is confirmed or cancelled, or a gift voucher changes.

    Last updated

    Setting up an endpoint#

    On the dashboard's API page (DashboardSetupAPI), add the https URL of your server and choose the events you want. A venue can have five endpoints. MyEscapeRoom generates a signing secret (whsec_…) for each; copy it when it is shown, or reveal it again from the endpoint. Everyone who may manage API keys may manage webhooks.

    The URL must be https and its host must resolve to a public address: private, loopback and link-local addresses are refused when you save it and again before every delivery. Redirects are not followed. Use Send test event to check your setup, and see each endpoint's last 20 deliveries with their status and your server's answer.

    Events#

    • voucher.sold — A voucher became active: its online payment succeeded, it was created with collectPayment false, or you created it on the dashboard. data.voucher.
    • voucher.redeemed — Some of a voucher's balance was spent, by a host in person or by an online booking. An online booking draws the voucher when its hold is made, before it is paid, so this fires then; a hold that is abandoned or expires gives the draw back with voucher.redemption_reversed. data.voucher (with the new remainingCents) and data.redemption.
    • voucher.redemption_reversed — A spend was given back: the booking was cancelled or released, or a host undid the redemption. data.voucher and data.redemption (reversed: true).
    • voucher.cancelled — An active voucher was cancelled, through the API or the dashboard. Deleting an active voucher on the dashboard is reported the same way. A voucher cancelled while still waiting for its payment was never sold and sends nothing. data.voucher.
    • booking.confirmed — A booking became confirmed: its online payment succeeded. This is the only way a booking is confirmed, whichever channel made it (the player site or your Embed API key), and it is sent once per booking. data.booking.
    • booking.cancelled — A confirmed booking was cancelled, by the player, by your staff on the dashboard, by an API key or by MyEscapeRoom. Sent once per booking, and only for a booking that was confirmed: an unpaid hold that is released or runs out was never confirmed and sends nothing. data.booking, with data.booking.cancellation.
    • booking.rescheduled — A confirmed booking was moved to another session of the same room by an owner or admin on the dashboard. The booking keeps its ref and what was paid; only the slot changes. data.booking has the new slot, and data.previousSlot the start and end it left. Sent once per move.
    • ping — The dashboard's Send test event button. Sent to that endpoint only, whatever it subscribes to. data.message.

    Voucher events carry the voucher as the embed API reads it, including its code: it is bearer money, so keep the endpoint private and verify every request.

    Booking events carry the booking: its ref, status, the room, the slot's start and end (UTC) with the venue's timezone, the party size, the amounts in minor units of currency (cents), the payment mode and the guest's name, email and phone. On booking.cancelled, data.booking.cancellation says who cancelled (cancelledBy: player, staff, api or operator), when, the optional reason, and the refund decided (refundedCents and refundStatus); it is null on a confirmation. The refund itself follows within minutes of the event, so refundStatus is pending or not_needed when you receive it. On booking.rescheduled, data.previousSlot carries the start and end (UTC) of the session the booking left. Guest details are personal data: keep the endpoint private and store only what you need. A new endpoint subscribes to every event by default; an endpoint you added earlier keeps the events it had, so to receive booking.rescheduled edit the endpoint on the dashboard and add it.

    The request#

    A POST with a JSON body and these headers:

    • MER-Webhook-Id — the event id, the same as id in the body.
    • MER-Webhook-Timestamp — Unix seconds when this attempt was sent.
    • MER-Signature — v1= and the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your endpoint's secret.
    Example payload: a voucher event
    {
      "id": "evt_3f9a1c0d2b7e4a6f8c1d5e92",
      "type": "voucher.redeemed",
      "createdAt": "2026-10-18T14:03:21.512Z",
      "venueId": "5b1e…",
      "data": {
        "voucher": {
          "code": "GIFT-7K3M-Q9XD-2VBN",
          "status": "active",
          "amountCents": 10000,
          "remainingCents": 4000,
          "expiresAt": "2027-04-16T14:00:00.000Z",
          "roomSlug": null,
          "buyerName": "Sam",
          "buyerEmail": "sam@example.com",
          "paidAt": "2026-10-01T09:12:44.000Z",
          "soldOnline": true,
          "createdAt": "2026-10-01T09:11:58.000Z"
        },
        "redemption": {
          "id": "0e9c…",
          "source": "online",
          "amountCents": 6000,
          "bookingRef": "BK-ABCD1234",
          "reversed": false,
          "createdAt": "2026-10-18T14:03:21.480Z"
        }
      }
    }
    Example payload: a booking event
    {
      "id": "evt_8c2d4e6a0b1f3a5c7e9d1b34",
      "type": "booking.cancelled",
      "createdAt": "2026-10-20T08:15:02.114Z",
      "venueId": "5b1e…",
      "data": {
        "booking": {
          "ref": "BK-ABCD1234",
          "status": "cancelled",
          "room": { "slug": "the-vault", "name": "The Vault" },
          "slot": {
            "start": "2026-11-01T18:00:00.000Z",
            "end": "2026-11-01T19:00:00.000Z",
            "timezone": "Europe/Brussels"
          },
          "teamSize": 4,
          "guest": { "name": "Ada Lovelace", "email": "ada@example.com", "phone": "+32 470 00 00 00" },
          "paymentMode": "online",
          "currency": "eur",
          "roomSubtotalCents": 6000,
          "discountCents": 0,
          "voucherCents": 0,
          "platformFeeCents": 100,
          "totalCents": 6100,
          "onlineChargeCents": 6100,
          "dueAtVenueCents": 0,
          "createdAt": "2026-10-18T09:00:00.000Z",
          "feePaidAt": "2026-10-18T09:01:12.000Z",
          "paidAt": "2026-10-18T09:01:12.000Z",
          "cancellation": {
            "cancelledAt": "2026-10-20T08:15:02.000Z",
            "cancelledBy": "player",
            "reason": null,
            "refundedCents": 6000,
            "refundStatus": "pending"
          }
        }
      }
    }

    Verifying the signature#

    Compute the signature over the raw body and compare it in constant time. Reject old timestamps.

    Node.js
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    // rawBody must be the exact bytes received: read the body as text before any JSON parsing.
    export function verifyWebhook(rawBody, headers, secret) {
      const timestamp = headers["mer-webhook-timestamp"];
      const signature = headers["mer-signature"]; // "v1=<hex>"
    
      // Refuse anything older than five minutes, so a captured request cannot be replayed.
      const ts = Number(timestamp);
      if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
    
      const expected =
        "v1=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
      const a = Buffer.from(expected);
      const b = Buffer.from(signature ?? "");
      return a.length === b.length && timingSafeEqual(a, b);
    }
    
    // Then dedupe on the event id (headers["mer-webhook-id"], also "id" in the body):
    // a delivery is at least once, so the same event can arrive twice.

    Delivery and retries#

    Answer with any 2xx within 10 seconds to acknowledge. We ignore the response body. Anything else (another status, a timeout, a connection error) is retried with exponential backoff: 10 minutes after the first failure, then 20, 40, 80, 160, 320 and 640 minutes, eight attempts in all over about 21 hours, after which the delivery is marked failed on the dashboard and not sent again.

    Delivery is at least once: the same event can arrive more than once (a retry after your server answered too late, for one), and events can arrive out of order, so use createdAt, the voucher's remainingCents and the booking's status rather than arrival order. A booking's booking.cancelled can reach you before its booking.confirmed if the first is being retried. Dedupe on the event id.

    Events are written together with the change they report: if the change did not happen, there is no event. Switching an endpoint off stops new events for it; deliveries still waiting for it fail.

    MyEscapeRoom Docs