Skip to content
On this page

Website & booking-engine events

v1

Tell GuestMaker what visitors do on a website or booking engine, such as leaving a booking half-finished, and start a journey from it. One contract, three ways in, and every tenant defines the events that matter to them.

Overview

A booking engine knows the moment a guest leaves; a hotel wants to write to that guest while the trip is still on their mind. Events connect the two. Something on your website or booking engine sends an event, the guest is matched to a contact, and a journey whose trigger is that event starts.

  YOUR SIDE                                  GUESTMAKER

  Server API   POST /api/v1/website-events  --+
  Webhook      POST .../hooks/gme_wh_...    --+-->  one contract  -->  contact + consent evidence
  Browser SDK  gm-events.js                 --+                   |
                                                                  +->  journey trigger --> wait --> email / WhatsApp
                                                                                      |
                                                                                      +--> stops when the booking completes

There is nothing to register first. Send an event with any valid name and it appears in the journey editor's dropdown as soon as it has been received, next to the standard events below. Each source (one system that sends events) gets its own key, so you can pause or replace one without touching the others.

Availability

Website & booking-engine events are switched on per account. If Settings → Integrations → Website & booking-engine events says the feature is not on for your account, ask your GuestMaker contact.

Quickstart

  1. In GuestMaker open Settings → Integrations → Website & booking-engine events, choose New source, pick Server and copy the secret key. It is shown once.
  2. Send your first event. Add "test": true so it is checked and logged without touching any guest.
  3. Open the source's Delivery log: the event is there with its result. Fix anything it flags.
  4. In a journey, choose the trigger Website or booking-engine event, pick Cart abandoned, add a Wait and an email, and activate.
curl -X POST 'https://www.guestmaker.ai/api/v1/website-events' \
  -H 'Authorization: Bearer gme_sk_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"event":"cart.abandoned","idempotency_key":"cart_8841","contact":{"email":"ana@example.com","first_name":"Ana","language":"es"},"consent":{"email_marketing":true,"text":"I agree to receive offers from this hotel by email","captured_at":"2026-09-30T10:11:58Z","language":"en","form_id":"checkout-details"},"context":{"hotel_code":"HD-MAD","stage":"payment","check_in":"2026-11-20","check_out":"2026-11-23","adults":2,"room_name":"Deluxe","value":612.4,"currency":"EUR","cart_id":"8841","resume_url":"https://book.example.com/resume?cart=8841"}}'

A successful request answers 202 with a result per event. The example above is a real abandoned cart including consent evidence; add "test": true to try it safely.

{
  "received": 1,
  "accepted": 1,
  "duplicate": 0,
  "rejected": 0,
  "results": [
    {
      "index": 0,
      "status": "accepted",
      "code": "accepted",
      "message": "Accepted",
      "event_id": "9b0e…",
      "contact": "matched",
      "warnings": []
    }
  ]
}

Choose a door

All three doors accept the same events and apply the same rules. Pick by what your side can do.

Server API

Your backend or booking engine can call an API. The most reliable door: you choose exactly when to send and can retry.

Credential:
A secret key, gme_sk_…, in the Authorization header.
You send:
POST /api/v1/website-events with a JSON body in our format.
Keep it safe:
Keep the key on your server. If it leaks, revoke it in Settings and add a new one: nothing else changes.

Webhook

A booking engine can only post its own JSON to a URL. You map its fields onto ours once, in Settings, and preview the result against a real request.

Credential:
A private URL, …/hooks/gme_wh_…, optionally with an HMAC signature header.
You send:
POST to the URL with the engine's own JSON. No code on either side.
Keep it safe:
The URL is the credential, so share it only with the engine. Add a signing secret if the engine can sign.

Browser SDK

Your website or booking page runs JavaScript you control. The only door that sees a visitor close the tab, which is when an abandoned cart is decided.

Credential:
A public key, gme_pk_…, that only works from the websites you list.
You send:
The SDK posts from the visitor's browser. It also works from Google Tag Manager.
Keep it safe:
The key is safe to publish: it only works from your allowed websites, and it cannot read anything.

A browser key is public on purpose

A gme_pk_… key is meant to sit in your page. What makes that safe is that it only works from the websites you list on the source (an exact origin such as https://book.example.com, no wildcards), it can only send events, and it is limited per visitor. A secret gme_sk_… key must never appear in a web page or a mobile app.

The event

The event

One event is one JSON object. Send it on its own, or up to 50 of them as { "events": [ … ] }. Unknown keys are refused rather than ignored: a misspelt `consents` would otherwise be accepted and quietly record nothing.

FieldTypeDescription
event *stringWhat happened: lowercase, dotted, 1–4 segments, up to 64 characters. Use a standard name where one fits; any other valid name is a custom event. Example: cart.abandoned
occurred_atstring (ISO 8601 with offset)When it happened. Defaults to the time we receive it. Not more than 7 days old, nor 5 minutes ahead. Example: 2026-11-02T18:04:11Z
idempotency_keystringYour own id for this event, up to 200 characters. Sending the same key again is a safe no-op, so retry freely. Without one, the same event for the same cart or session from the same guest inside one minute counts as a duplicate. Example: cart_8841
contactobjectWho it was. An event with no email or phone is still counted, but no journey can start from it: there is nobody to message.
consentobjectProof the visitor agreed to marketing email. Without it the contact is created but marketing email does not send.
contextobjectThe booking in progress: hotel, dates, guests, room, value.
propertiesobjectAnything else worth keeping, up to 25 keys. Names are lowercase letters, digits and underscores; values are strings (up to 500 characters), numbers or booleans. Example: { "promo": "SPRING" }
testbooleanValidate and log it, but start no journey and write no contact. Use it while you are wiring things up.

contact

The guest is matched to an existing contact by email or phone, or created if new. A visitor whose address is an agency desk, a placeholder or a role account is refused as a person, so an OTA relay address never becomes a marketing contact.

FieldTypeDescription
emailstringLower-cased for you. Required, with or without phone, for an email journey. Example: ana@example.com
phonestringInternational format, digits with an optional leading +. Example: +34600111222
first_namestringUp to 100 characters.
last_namestringUp to 100 characters.
languagestringISO 639-1, optionally with a region: es, pt-BR. The journey writes to the guest in this language when a template has one. Example: es

Consent is evidence or it is ignored. `email_marketing: true` is only a claim: it is honoured when `text` (the exact sentence shown) and a fresh `captured_at` come with it. `false` writes nothing, and no event can ever re-subscribe a guest who opted out.

FieldTypeDescription
email_marketing *booleantrue claims the visitor ticked the box; false records nothing. Example: true
textstringThe exact wording the visitor agreed to, at least 10 characters, up to 2000. Example: I agree to receive offers from this hotel by email
captured_atstring (ISO 8601 with offset)When they agreed. Not older than 7 days: consent is recorded when it is given, never replayed later.
languagestringThe language the sentence was shown in.
form_idstringWhich form or step collected it, for your own audit trail. Example: checkout-details

context

What the visitor was doing. Every field is optional; the journey can use whichever you send in its messages.

FieldTypeDescription
hotel_idstring (UUID)A hotel id from Settings → Properties.
hotel_codestringYour own code for the hotel, if the property carries one. Example: HD-MAD
hotel_namestringMatched against the group's hotels by name when no id or code is sent.
stageone of dates · room · extras · details · paymentWhere in the booking the visitor was. On cart.abandoned it is the step they left at, and a journey can be limited to some stages. Example: payment
check_instring (YYYY-MM-DD)A real calendar date. Example: 2026-11-20
check_outstring (YYYY-MM-DD)After check_in. Example: 2026-11-23
adultsinteger 0–30
childreninteger 0–30
roomsinteger 1–20
room_namestringThe room or room type chosen. Example: Deluxe
rate_namestringThe rate chosen.
valuenumberThe basket value. Example: 612.4
currencystring (ISO 4217, capitals) Example: EUR
cart_idstringYour id for the booking in progress. A journey uses it to tell one cart from another and to stop when that cart completes. Example: 8841
session_idstringThe visitor's session, when there is no cart id yet.
resume_urlstring (https)Where the guest can pick the booking up again. It goes into the email, so it must be an https link. Example: https://book.example.com/resume?cart=8841
page_urlstring (https)The page the visitor was on.
referrerstring (https)Where they came from.

* marks a required field. Everything else is optional, and a field the contract does not list is refused rather than ignored.

Standard events

These have their own place at the top of the journey editor's dropdown. Any other name that is lowercase and dotted (for example custom.spa_viewed) is a custom event: send it and it becomes selectable once received.

EventMeaning
cart.abandonedA visitor left mid-booking without completing. `context.stage` says where: dates, room, extras, details or payment.
lead.capturedA visitor left their email or phone (a form, a price alert, a callback request).
search.performedA visitor searched availability for dates.
room.selectedA visitor picked a room or rate.
checkout.startedA visitor reached the guest-details step. Pair it with a Wait and an exit on `checkout.completed` to build an abandoned-cart flow without the vendor timing anything.
payment.failedA payment attempt failed and the booking was not completed.
checkout.completed
exit
The visitor completed the booking. Stops any journey waiting on this visitor's cart.

On cart.abandoned, context.stage says where the guest left: dates, room, extras, details, payment. A journey can be limited to some stages, for example only guests who reached payment.

Server API

POST /api/v1/website-events with Authorization: Bearer gme_sk_… and a JSON body: one event, or { "events": [ … ] } with up to 50. In a batch every event is judged on its own: the request succeeds and each event carries its own result, so one bad event does not reject the others.

Idempotency and retries

Give each event an idempotency_key. Sending the same key again is answered duplicate and changes nothing, so retry freely. (An event that no active journey is waiting for is counted but not stored, so a repeat of it is answered accepted again: nothing happens either time.) Retry on 429 (wait Retry-After seconds) and on 5xx; any other 4xx will not succeed with the same bytes.

A single event that fails validation

is answered 422 with results[0].issues naming each field and the problem. The same event inside a batch is reported inside a 202.

{
  "received": 1,
  "accepted": 0,
  "duplicate": 0,
  "rejected": 1,
  "results": [
    {
      "index": 0,
      "status": "rejected",
      "code": "invalid_event",
      "message": "The event does not match the contract",
      "event_id": null,
      "contact": "none",
      "warnings": [],
      "issues": [
        {
          "field": "context.check_out",
          "message": "check_out must be after check_in"
        }
      ]
    }
  ]
}

The full event

{
  "event": "cart.abandoned",
  "idempotency_key": "cart_8841",
  "contact": {
    "email": "ana@example.com",
    "first_name": "Ana",
    "language": "es"
  },
  "consent": {
    "email_marketing": true,
    "text": "I agree to receive offers from this hotel by email",
    "captured_at": "2026-09-30T10:11:58Z",
    "language": "en",
    "form_id": "checkout-details"
  },
  "context": {
    "hotel_code": "HD-MAD",
    "stage": "payment",
    "check_in": "2026-11-20",
    "check_out": "2026-11-23",
    "adults": 2,
    "room_name": "Deluxe",
    "value": 612.4,
    "currency": "EUR",
    "cart_id": "8841",
    "resume_url": "https://book.example.com/resume?cart=8841"
  }
}

The machine-readable description is the OpenAPI document, generated from the same definitions the API validates with, so it cannot drift from what the server accepts.

Webhook and field mapping

Many booking engines can only post their own JSON to a URL. Create a Webhook source, give the engine the private URL, and tell GuestMaker once how the engine's fields map onto ours. No code on either side.

POST https://www.guestmaker.ai/api/v1/website-events/hooks/gme_wh_…?test=1
Content-Type: application/json

{
  "type": "CartAbandoned",
  "step": "PAYMENT",
  "user": {
    "mail": "ana@example.com",
    "name": "Ana"
  },
  "cart": {
    "id": 8841,
    "total": "612,40",
    "currency": "eur"
  },
  "stay": {
    "in": "20/11/2026",
    "out": "23/11/2026"
  }
}

The mapping editor

In the source's Set up field mapping, GuestMaker opens on the newest request the engine actually sent (personal details in it are masked), lists that request's own fields to choose from, and previews the resulting event exactly as the server will build it. Each row takes a value from a field in the payload, a fixed value, or a template such as cart_{{cart.id}}. Transforms available: lower, upper, trim, number, boolean, date, datetime. A word map translates the engine's vocabulary, for example CartAbandoned → cart.abandoned.

A mapping is a translator, not a bypass

The mapped result goes through the same strict contract as every other event, so a mapping cannot produce something the API would refuse. It is deliberately not a scripting language: paths, fixed values, templates and a closed list of transforms, nothing else. If a payload is an array it is treated as a batch of up to 50.

Checking the URL

Many booking engines and webhook tools check a URL with a GET before they will save it. A GET on the webhook URL takes no event: it answers 200 with { "ready": true, "signature_required": … } when a POST would be accepted, and otherwise the same 401 or 403 a POST would get, so a paused source is told apart from a mistyped token.

Signing

If the engine can sign its requests, set a signing secret on the source and every request must carry X-GM-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" under the secret, the construction Stripe uses. Sign the exact bytes you send, and use a timestamp within 5 minutes of the server's clock. While you rotate a secret you may send two v1 values.

import { createHmac } from 'node:crypto'

const t = Math.floor(Date.now() / 1000)
const v1 = createHmac('sha256', SIGNING_SECRET).update(`${t}.${rawBody}`).digest('hex')

// rawBody is the EXACT string you send. Then add this header to the request:
const signature = `t=${t},v1=${v1}`   // X-GM-Signature: t=1767225600,v1=5257a869…

Without a secret the private URL is the credential: share it only with the engine, and revoke it from the source if it leaks.

Browser SDK

/sdk/gm-events.js is a small dependency-free script (under 6 KB compressed) that sets no cookies. It sends events from the visitor's browser, and is the only door that sees a guest close the tab, which is exactly when an abandoned cart is decided.

<script async src="https://www.guestmaker.ai/sdk/gm-events.js"></script>
<script>
  window.gme = window.gme || function () { (gme.q = gme.q || []).push(arguments) }
  gme('init', { key: 'gme_pk_YOUR_PUBLIC_KEY' /* , hotelCode: 'HD-MAD' */ })

  // The moment the guest gives an email or phone in your booking form:
  // gme('identify', { email: 'ana@example.com' })

  // Report the booking when the guest leaves without finishing it:
  gme('abandonOnLeave', {
    getCart: function () {
      // Return the booking in progress, or null when there is none.
      return { cart_id: 'YOUR-CART-ID', stage: 'payment', check_in: '2026-11-20', check_out: '2026-11-23', value: 612.4, currency: 'EUR' }
    }
  })

  // When the booking is paid:
  // gme('completed', { cart_id: 'YOUR-CART-ID' })
</script>

CommandWhat it does
initStart with your public key. Options: hotelCode, hotelId, debug, persistContact, waitForConsent, endpoint.
trackSend any event: gme('track', 'search.performed', { context: { adults: 2 } }).
identifyTell us who the guest is once they give an email or phone. Kept for the tab only (sessionStorage), so a multi-page checkout still knows them.
abandonOnLeaveGive it a getCart function. When the guest leaves the page with a cart and an email or phone, it reports cart.abandoned once per cart and step.
completedThe booking was paid: sends checkout.completed and stops reporting that cart as abandoned.
allow / denyAnswer your cookie banner. With waitForConsent: true nothing is sent or stored until allow().
flush / resetSend what is queued now / forget the guest and start over (for example at logout).

Consent ticks

identify(contact, { consent: { email_marketing: true, text, form_id } }) records a consent claim with the sentence you pass and the time of the tick. Without text the tick is sent but ignored by the server, so marketing email will not send.

Google Tag Manager

Paste the Browser SDK snippet above into a Custom HTML tag that fires on your booking pages. Keep the cart in a global variable your site sets, and call gme('identify', …) and gme('completed', …) from further tags.

What it does when things go wrong

It never throws into your page and never blocks it. A network error, 429 or 5xx is retried with backoff (1, 2, 4, 8 seconds) up to five times and honours Retry-After; a 4xx is reported once in the console with the server's reason and not retried. Events tracked while a retry waits queue behind it, so order is kept. At most 50 events wait at once; beyond that the oldest is dropped.

Use it in a journey

Choose the trigger Website or booking-engine event (category Website & Booking Engine). The event dropdown lists the standard events first, then every custom event your account has received, marked “not received yet” until one arrives.

OptionWhat it does
EventThe event that starts the journey, for example cart.abandoned.
SourceOnly events from one source start it. Leave empty for any source.
StagesFor cart.abandoned: only carts that stopped at these steps. Empty means any step.
Stop whenEvents that end the journey for the same cart or session while it is running. Default: checkout.completed. Empty means never stop on an event.
Stop on a real bookingA booking arriving from your PMS or CRS also ends it. On by default.

A typical abandoned-cart flow: trigger on cart.abandoned (or checkout.started), Wait 30 minutes, send the reminder email with a button to the guest's resume link. If the guest completes the booking meanwhile, checkout.completed for the same cart ends the journey before the email goes out.

Reading the event in a message

The event's details can be used in the email a journey sends, as merge tags, and in a WhatsApp template as variables. Both read the event that started that journey run.

Email merge tags

Type the tag in the email, or pick it from Website event in the merge-tag picker. Dates are written in the guest's own language (“20 de noviembre de 2026” for a Spanish-speaking guest) and money in their currency style. A value the event did not carry renders as empty text, so give the copy around it a version that reads well without it.

TagWhat it holds
{{event_resume_url}}Resume booking link, for example https://book.example.com/resume?cart=abc123
{{event_hotel_name}}Hotel they were booking, for example Hotel Mar Azul
{{event_check_in}}Check-in they chose, for example November 20, 2026
{{event_check_out}}Check-out they chose, for example November 23, 2026
{{event_room_name}}Room they looked at, for example Deluxe Sea View
{{event_rate_name}}Rate they looked at, for example Bed & breakfast
{{event_value}}Cart value, for example €450.00
{{event_currency}}Cart currency, for example EUR
{{event_adults}}Adults, for example 2
{{event_children}}Children, for example 0
{{event_rooms}}Rooms, for example 1
{{event_stage}}Where they left, for example payment
{{event_name}}Event name, for example cart.abandoned
{{event_prop_<name>}}A custom property you sent under properties, for example {{event_prop_spa_package}}.

A resume link that is missing is never sent as an empty button

If the email uses {{event_resume_url}} and the event did not carry context.resume_url, GuestMaker skips that email and records why, instead of sending a button that goes nowhere. Send resume_url on every cart event you want to follow up.

Declare your own events in advance

A custom event normally appears in the trigger's dropdown once your website has sent one. To build the journey before the integration is finished, declare it in Settings → Integrations → Website events → Events you can start a journey from. It is listed as “not received yet”, and the first real event simply counts against it. A declared event nothing has sent can be removed; one that has been received stays in your history. Names use lowercase letters, digits and underscores with dots between parts, and must match what your website sends.

WhatsApp variables

PathWhat it holds
event.payload.context.resume_urlWhere the guest can pick the booking up.
event.payload.context.check_inArrival date.
event.payload.context.check_outDeparture date.
event.payload.context.room_nameThe room they were looking at.
event.payload.context.valueThe basket value.
event.payload.properties.<name>Any property you sent.

Events are only stored when a guest is identified and an active journey is listening for them, so sending an event nobody waits for costs nothing and creates nothing.

Errors and results

Request-level errors answer with { error: { code, message, detail?, fix, docs } } and the HTTP status shown. The docs link points at the entry below, and fix says what to change.

Refused: fix the request

The event was not accepted. Request-level errors answer the whole request with the HTTP status shown; the others are reported per event.

invalid_json
HTTP 400
The body is not valid JSON
Why:
The request body could not be parsed as JSON.
Fix:
Send a JSON body with Content-Type: application/json (or text/plain from navigator.sendBeacon).
invalid_body
HTTP 422
The body has the wrong shape
Why:
The body is not one event object and not { "events": [...] }.
Fix:
Send one event object, or an object with an "events" array.
body_too_large
HTTP 413
The body is too large
Why:
The request body exceeds the limit: 64 KB, or 256 KB on a webhook.
Fix:
Send fewer events per request, or trim `properties`.
batch_too_large
HTTP 413
Too many events in one request
Why:
More than 50 events in "events".
Fix:
Split the batch into requests of at most 50 events.
batch_empty
HTTP 422
The batch is empty
Why:
"events" is an empty array.
Fix:
Send at least one event.
unauthorized
HTTP 401
Missing or invalid credential
Why:
No credential was sent, or it is unknown, revoked or expired.
Fix:
Send the source's key as `Authorization: Bearer <key>` (server), in the webhook URL (webhook), or as `X-GM-Key` or a `?key=` query parameter (browser key only: `navigator.sendBeacon` cannot set headers). Check the key in Settings → Integrations → Website & booking-engine events.
source_disabled
HTTP 403
This source is disabled
Why:
The event source was paused or deleted.
Fix:
Re-enable the source in Settings, or use another one.
feature_disabled
HTTP 403
Website events are not enabled for this account
Why:
The tenant does not have website events enabled.
Fix:
Ask your GuestMaker contact to enable Website & booking engine events.
origin_not_allowed
HTTP 403
This origin is not allowed for this key
Why:
A browser key was used from a website that is not in the source's allowed origins.
Fix:
Add the site's origin (for example https://www.example.com) to the source's allowed origins.
signature_missing
HTTP 401
The webhook signature is missing
Why:
The source requires signed webhooks and no X-GM-Signature header was sent.
Fix:
Sign the request: X-GM-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">.
signature_invalid
HTTP 401
The webhook signature does not match
Why:
The signature is malformed, was computed over different bytes, or used the wrong secret.
Fix:
Sign the exact raw body bytes with the source's signing secret; do not re-serialise the JSON before signing.
signature_expired
HTTP 401
The webhook signature is too old
Why:
The timestamp in the signature is more than 5 minutes from the server's clock.
Fix:
Sign with the current time and check the sender's clock.
rate_limited
HTTP 429
Too many requests
Why:
The source exceeded its per-minute limit.
Fix:
Retry after the number of seconds in the Retry-After header; batch events instead of sending one request each.
internal_error
HTTP 500
Something went wrong on our side
Why:
An unexpected failure while processing the request.
Fix:
Retry with the same idempotency_key; the event will not be duplicated.
invalid_eventThe event does not match the contract
Why:
A field is missing, has the wrong type or format, or is not part of the contract.
Fix:
Read the `issues` list: each entry names the field and the problem.
event_not_allowedThis source may not send this event
Why:
The source has an allow-list of events and this name is not on it.
Fix:
Add the event to the source's allowed events, or send an allowed one.
hotel_not_allowedThis source may not send for this hotel
Why:
The source is limited to specific hotels and this event names another.
Fix:
Send the hotel the source is allowed for, or widen the source's hotels.
hotel_unknownThe hotel could not be found
Why:
hotel_id, hotel_code or hotel_name matches none of the tenant's hotels.
Fix:
Use a hotel_id from Settings → Properties, or a hotel code that exists.
occurred_at_too_oldThe event is too old
Why:
occurred_at is more than 7 days in the past.
Fix:
Send events when they happen. Older data belongs in an import, not the live events stream.
occurred_at_in_futureThe event is dated in the future
Why:
occurred_at is more than 5 minutes ahead of the server clock.
Fix:
Check the sender's clock, or omit occurred_at to use the receive time.

Accepted, but something you intended did not happen

The event was accepted and the response says what was ignored, so a claim never disappears silently.

no_contactNo email or phone, so no journey can start
Why:
The event has no contact.email or contact.phone, so there is nobody to message.
Fix:
Include contact.email (or phone) on events you want to start a journey. Anonymous events are still counted in the catalogue.

Normal outcomes

Per-event results inside a 202.

acceptedAccepted
Why:
The event was stored and, if a journey listens for it, queued.
Fix:
Nothing to do.
duplicateAlready received
Why:
An event with the same idempotency key was already accepted.
Fix:
Nothing to do; retries are safe.
test_eventTest event
Why:
test was true: the event is logged but starts no journey and writes no contact.
Fix:
Remove test: true to send for real.
refused_not_personThe email is not a person
Why:
The address is an agency desk, a placeholder or a role account, so no contact was created.
Fix:
Nothing to fix unless the address is a real guest; see the identity rules in Settings.

Limits

LimitValue
Request body64 KB (256 KB for a webhook, which carries the engine's whole payload)
Events per request50
Event namelowercase, dotted, 1–4 segments, 64 characters
Idempotency key200 characters
Properties25 keys, string values up to 500 characters
occurred_atup to 7 days old, at most 5 minutes ahead
Consent captured_atup to 7 days old
Rate600 requests a minute per source by default (adjustable per source); a browser key is also limited per visitor
Keys per sourcetwo live keys at once, so a key can be replaced without downtime
Delivery log14 days

Testing

  • Add "test": true (or ?test=1 on a webhook URL): the event is validated and logged, and no journey starts and no contact is written.
  • Send test event on a source sends one from the dashboard, so you can check the connection before your engine is ready.
  • The Delivery log shows every request with its result, the reason it was refused and a link to how to fix it. Personal details in it are masked.
  • A request with a missing or wrong key cannot be tied to any source, so it appears in nobody's delivery log: the sender sees the 401, the tenant sees nothing. If a vendor says events are not arriving, start by checking the key with a "test": true request.
  • A browser source: add debug: true to init to see what the SDK sends in the console.

Booking-engine vendors

If you are a booking-engine vendor, you do not need an integration with us: your customer creates a Webhook source, maps your fields once, and you post to their URL. This message is what a hotel can send you.

Subject: Send booking events to GuestMaker

Hello,

We use GuestMaker to follow up with guests who leave a booking half-finished. Please send these events
from our booking engine to the webhook URL below, as JSON over HTTPS (POST):

  1. CartAbandoned  – the guest left before paying (please include which step they were on)
  2. BookingPaid    – the guest completed the booking (so we stop following up)

Webhook URL:   https://www.guestmaker.ai/api/v1/website-events/hooks/gme_wh_YOUR_WEBHOOK_TOKEN
Signing:       optional. If you can sign requests, we will give you a secret; the header is
               X-GM-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

Each payload should carry, in your own field names (we map them, you do not need to change anything):
  - the guest's email (and phone if you have it)
  - the cart or booking id
  - the hotel
  - check-in and check-out dates, adults, room name, total and currency
  - the step reached, and a link that resumes the booking
  - if you collect marketing consent: the exact sentence shown and when it was ticked

An example of what you might send:

{
  "type": "CartAbandoned",
  "step": "PAYMENT",
  "user": {
    "mail": "ana@example.com",
    "name": "Ana"
  },
  "cart": {
    "id": 8841,
    "total": "612,40",
    "currency": "eur"
  },
  "stay": {
    "in": "20/11/2026",
    "out": "23/11/2026"
  }
}

If your tool checks a URL before saving it, a GET on the URL answers 200 when it is live.
Please add "?test=1" to the URL for a first request: it is checked and logged but starts nothing.
Questions: developers@guestmaker.ai

Thank you.

Changelog

VersionChange
1.1.0Email merge tags from the event that started a journey ({{event_resume_url}}, {{event_check_in}}, {{event_prop_<name>}} and more), declaring custom events in Settings before the first one arrives, and a skip with a recorded reason when an email needs a resume link the event did not carry.
1.0.0First release: server API, webhook with field mapping and optional signing, browser SDK, the Website or booking-engine event trigger, consent evidence, and this reference with a generated OpenAPI document.