On this page
Website & booking-engine events
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 completesThere 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
- In GuestMaker open Settings → Integrations → Website & booking-engine events, choose New source, pick Server and copy the secret key. It is shown once.
- Send your first event. Add
"test": trueso it is checked and logged without touching any guest. - Open the source's Delivery log: the event is there with its result. Fix anything it flags.
- 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.
| Field | Type | Description |
|---|---|---|
| event * | string | What 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_at | string (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_key | string | Your 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 |
| contact | object | Who it was. An event with no email or phone is still counted, but no journey can start from it: there is nobody to message. |
| consent | object | Proof the visitor agreed to marketing email. Without it the contact is created but marketing email does not send. |
| context | object | The booking in progress: hotel, dates, guests, room, value. |
| properties | object | Anything 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" } |
| test | boolean | Validate 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.
| Field | Type | Description |
|---|---|---|
| string | Lower-cased for you. Required, with or without phone, for an email journey. Example: ana@example.com | |
| phone | string | International format, digits with an optional leading +. Example: +34600111222 |
| first_name | string | Up to 100 characters. |
| last_name | string | Up to 100 characters. |
| language | string | ISO 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
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.
| Field | Type | Description |
|---|---|---|
| email_marketing * | boolean | true claims the visitor ticked the box; false records nothing. Example: true |
| text | string | The 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_at | string (ISO 8601 with offset) | When they agreed. Not older than 7 days: consent is recorded when it is given, never replayed later. |
| language | string | The language the sentence was shown in. |
| form_id | string | Which 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.
| Field | Type | Description |
|---|---|---|
| hotel_id | string (UUID) | A hotel id from Settings → Properties. |
| hotel_code | string | Your own code for the hotel, if the property carries one. Example: HD-MAD |
| hotel_name | string | Matched against the group's hotels by name when no id or code is sent. |
| stage | one of dates · room · extras · details · payment | Where 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_in | string (YYYY-MM-DD) | A real calendar date. Example: 2026-11-20 |
| check_out | string (YYYY-MM-DD) | After check_in. Example: 2026-11-23 |
| adults | integer 0–30 | |
| children | integer 0–30 | |
| rooms | integer 1–20 | |
| room_name | string | The room or room type chosen. Example: Deluxe |
| rate_name | string | The rate chosen. |
| value | number | The basket value. Example: 612.4 |
| currency | string (ISO 4217, capitals) | Example: EUR |
| cart_id | string | Your 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_id | string | The visitor's session, when there is no cart id yet. |
| resume_url | string (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_url | string (https) | The page the visitor was on. |
| referrer | string (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.
| Event | Meaning |
|---|---|
| cart.abandoned | A visitor left mid-booking without completing. `context.stage` says where: dates, room, extras, details or payment. |
| lead.captured | A visitor left their email or phone (a form, a price alert, a callback request). |
| search.performed | A visitor searched availability for dates. |
| room.selected | A visitor picked a room or rate. |
| checkout.started | A 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.failed | A 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>| Command | What it does |
|---|---|
init | Start with your public key. Options: hotelCode, hotelId, debug, persistContact, waitForConsent, endpoint. |
track | Send any event: gme('track', 'search.performed', { context: { adults: 2 } }). |
identify | Tell 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. |
abandonOnLeave | Give 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. |
completed | The booking was paid: sends checkout.completed and stops reporting that cart as abandoned. |
allow / deny | Answer your cookie banner. With waitForConsent: true nothing is sent or stored until allow(). |
flush / reset | Send 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.
Consent and compliance
These are rules of the platform, not options, and they are the same on every door.
- No consent evidence, no marketing email. A guest created from an abandoned cart is still created and still starts the journey, but an email step is skipped, with a reason the tenant can see, unless the contact has marketing consent.
- Consent is evidence or it is ignored.
consent.email_marketing: trueis honoured only with the exacttextthe guest agreed to and acaptured_atwithin 7 days. GuestMaker records the sentence, the time, the language, the form and the request's IP and user agent in the consent audit log. - An event can never re-subscribe a guest who opted out. If the contact has unsubscribed or is suppressed, a consent claim is ignored and the response says so (
consent_ignored_opted_out). A buggy pre-ticked box cannot undo an unsubscribe. - Never transactional. A reminder to a guest who left a booking half-finished is a commercial message. Journeys started by a website event send email in the marketing category, and every WhatsApp template they send requires marketing consent, whatever category the template was registered under.
- Only people. An agency desk, a placeholder or a role account is refused as a person, so an OTA relay address never becomes a marketing contact.
Your legal basis stays yours
GuestMaker enforces the evidence, not the law. The wording your booking form shows and the basis you rely on are your responsibility, and your data protection officer should sign them off. The safe default here is deliberately strict.
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.
| Option | What it does |
|---|---|
| Event | The event that starts the journey, for example cart.abandoned. |
| Source | Only events from one source start it. Leave empty for any source. |
| Stages | For cart.abandoned: only carts that stopped at these steps. Empty means any step. |
| Stop when | Events 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 booking | A 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.
| Tag | What 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
| Path | What it holds |
|---|---|
| event.payload.context.resume_url | Where the guest can pick the booking up. |
| event.payload.context.check_in | Arrival date. |
| event.payload.context.check_out | Departure date. |
| event.payload.context.room_name | The room they were looking at. |
| event.payload.context.value | The 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.
- 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).
- Why:
- The body is not one event object and not { "events": [...] }.
- Fix:
- Send one event object, or an object with an "events" array.
- Why:
- The request body exceeds the limit: 64 KB, or 256 KB on a webhook.
- Fix:
- Send fewer events per request, or trim `properties`.
- Why:
- More than 50 events in "events".
- Fix:
- Split the batch into requests of at most 50 events.
- Why:
- "events" is an empty array.
- Fix:
- Send at least one event.
- Why:
- The event source was paused or deleted.
- Fix:
- Re-enable the source in Settings, or use another one.
- Why:
- The tenant does not have website events enabled.
- Fix:
- Ask your GuestMaker contact to enable Website & booking engine events.
- 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.
- 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>">.
- 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.
- 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.
- 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.
- Why:
- An unexpected failure while processing the request.
- Fix:
- Retry with the same idempotency_key; the event will not be duplicated.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Why:
- consent.email_marketing was true but consent.text (the exact sentence shown) and/or a valid captured_at were missing or stale.
- Fix:
- Send the exact wording the visitor agreed to and when. Without it the contact is created but marketing email will not send.
- Why:
- The contact previously unsubscribed or is suppressed. A website event can never re-subscribe someone.
- Fix:
- Nothing to fix on your side: the guest must opt in again through an email preference link.
- Why:
- consent.captured_at is more than 7 days old. Consent must be recorded when the guest gives it, not replayed later.
- Fix:
- Send the consent block in the same request that follows the guest ticking the box, with captured_at set to that moment.
- Why:
- The source is set to ignore consent, so a consent claim on its events is never written to the contact.
- Fix:
- Switch the source's consent mode to 'evidence required' if this website really collects marketing consent.
- 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.
- Why:
- The event was stored and, if a journey listens for it, queued.
- Fix:
- Nothing to do.
- Why:
- An event with the same idempotency key was already accepted.
- Fix:
- Nothing to do; retries are safe.
- Why:
- test was true: the event is logged but starts no journey and writes no contact.
- Fix:
- Remove test: true to send for real.
- 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
| Limit | Value |
|---|---|
| Request body | 64 KB (256 KB for a webhook, which carries the engine's whole payload) |
| Events per request | 50 |
| Event name | lowercase, dotted, 1–4 segments, 64 characters |
| Idempotency key | 200 characters |
| Properties | 25 keys, string values up to 500 characters |
| occurred_at | up to 7 days old, at most 5 minutes ahead |
| Consent captured_at | up to 7 days old |
| Rate | 600 requests a minute per source by default (adjustable per source); a browser key is also limited per visitor |
| Keys per source | two live keys at once, so a key can be replaced without downtime |
| Delivery log | 14 days |
Testing
- Add
"test": true(or?test=1on 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": truerequest. - A browser source: add
debug: truetoinitto 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
| Version | Change |
|---|---|
| 1.1.0 | Email 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.0 | First 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. |