GuestMakerDeveloper Portal
DocumentationQuick StartAPI ReferenceSandboxNEWChangelogSDKsNEW
Developer Portal

Integrate your PMS, booking engine, or CRM with our WhatsApp automation platform.

Documentation

  • Quick Start Guide
  • Authentication
  • API Reference
  • Webhooks
  • Loyalty Cash Credit
  • Loyalty SSO (OIDC)

Resources

  • API Sandbox
  • Changelog
  • SDKs & Libraries
  • Become a Partner

Support

  • developers@guestmaker.com
  • Contact Sales
Privacy PolicyTerms of ServiceLegal Notice
GuestMaker

© 2026 GuestMaker

Documentation

ChangelogTry Sandbox

API Documentation

Everything you need to integrate your systems with our WhatsApp automation platform.

API Version 1.0
Base URL: guestmaker.ai

Quick Start

Get your first API call working in under 5 minutes with this step-by-step guide.

1

Get your API credentials

Request API access through our partner form or contact your account manager. You will receive:

  • An API key starting with gmkr_
  • Your hotel IDs from Settings → Hotels
  • Documentation on available scopes
2

Make your first API call

Create a reservation with guest information. This single call creates the reservation, links guests to a hotel, and triggers automation journeys.

curl -X POST https://guestmaker.ai/api/v1/reservations \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reservation_id": "RES-2026-001234",
    "confirmation_number": "CONF-ABC123",
    "hotel_name": "Grand Hotel",
    "check_in": "2026-03-15",
    "check_out": "2026-03-18",
    "status": "confirmed",
    "source": "pms",
    "amount": 450.00,
    "amount_net": 371.90,
    "currency": "EUR",
    "channel": "booking.com",
    "market_segment": "LEISURE",
    "agency_code": "AGY-001",
    "external_created_at": "2026-01-20T14:30:00+01:00",
    "external_updated_at": "2026-02-05T09:15:00+01:00",
    "guests": [
      {
        "first_name": "John",
        "last_name": "Doe",
        "email": "john@example.com",
        "phone": "+1234567890",
        "is_holder": true,
        "pax_type": "adult",
        "opt_in_status": "opted_in",
        "nationality": "US",
        "language": "en",
        "document_type": "passport",
        "document_number": "AB1234567"
      },
      {
        "first_name": "Jane",
        "last_name": "Doe",
        "pax_type": "adult"
      }
    ],
    "stays": [
      {
        "start_date": "2026-03-15",
        "end_date": "2026-03-18",
        "room_type": "Deluxe Suite",
        "room_number": "301",
        "board_type": "BB",
        "adults": 2
      }
    ]
  }'
3

Verify the response

A successful response includes the contact ID and event ID:

{
  "success": true,
  "data": {
    "reservation_id": "550e8400-e29b-41d4-a716-446655440002",
    "contact_id": "550e8400-e29b-41d4-a716-446655440000",
    "hotel_id": "550e8400-e29b-41d4-a716-446655440001",
    "is_new_contact": true,
    "event_id": "550e8400-e29b-41d4-a716-446655440003",
    "message": "Reservation created successfully"
  }
}
4

Set up webhooks (optional)

Configure webhook endpoints in your dashboard to receive real-time notifications when guests reply or journeys complete.

5

Explore the full API

Continue reading below to learn about events, webhooks, error handling, and best practices for production integrations.

Authentication

All API requests require authentication using a Bearer token in the Authorization header.

Authorization: Bearer gmkr_your_api_key

API Key Format

API keys are prefixed with gmkr_ for production keys or gmkr_test_ for sandbox keys. Your API key is provided when you set up your integration with us.

Available Scopes

ScopeDescription
guests:readRead guest/contact information
guests:writeCreate/update guests with bookings
events:writeSend events to trigger journeys
bookings:readRead reservation data
bookings:writeCreate/update reservations
webhooks:manageConfigure outbound webhooks
*Full access (all scopes)

Security Best Practices

  • Never expose your API key in client-side code
  • Store your API key in environment variables
  • Rotate your API key periodically
  • Use IP allowlists when possible
  • Request only the scopes you need

Keep your API key secure

Your API key grants access to your tenant's data. Treat it like a password and never share it publicly.

Reservations API

Create and update reservations from your PMS or booking engine. Supports multiple guests per reservation, room stays, and extras. Designed for enterprise PMS integrations with comprehensive data sync.

POST
/api/v1/reservations
Create or update a reservation with guests, stays, and extras. Uses upsert based on reservation_id.

Required Scope

reservations:write

Request Body

ParameterTypeRequiredDescription
reservation_idstring
Required
Your PMS unique reservation ID (used for upsert matching)
confirmation_numberstringOptionalBooking confirmation/localizer code (can be shared across reservations)
booking_idstringOptionalParent booking ID (for grouping multiple reservations)
hotel_idstringOptionalHotel UUID (one of hotel_id, hotel_code, or hotel_name required)
hotel_codestringOptionalHotel code (alternative to hotel_id)
hotel_namestringOptionalHotel name (alternative to hotel_id)
check_instring
Required
Check-in date (YYYY-MM-DD)
check_outstring
Required
Check-out date (YYYY-MM-DD)
booking_datestringOptionalDate the booking was made (YYYY-MM-DD)
statusstringOptionalReservation status: confirmed, pending (booking engine awaiting payment confirmation), modified, cancelled, no_show, checked_in, checked_out
channelstringOptionalBooking channel (e.g., booking.com, expedia, direct)
sourcestringOptionalSystem type that sent this data: pms, booking_engine, crm, channel_manager, website, etc. Defaults to "api"
agencystringOptionalTravel agency name
companystringOptionalCorporate booking company
amountnumberOptionalTotal reservation amount (gross)
amount_netnumberOptionalTotal reservation amount (net, before tax)
currencystringOptionalISO 4217 currency code (e.g., EUR, USD)
market_segmentstringOptionalPMS market segment code (e.g., "LEISURE", "CORPORATE")
agencystringOptionalSelling agency / tour operator / OTA name (e.g. "BOOKING.COM", "EASYJET HOLIDAYS"). Max 200 chars
agency_codestringOptionalTravel agency code from the PMS, where the source sends one. Max 100 chars
nightsnumberOptionalNumber of nights (accepted but not persisted — auto-computed from dates)
external_created_atstringOptionalPMS creation timestamp (ISO 8601 with timezone)
external_updated_atstringOptionalPMS last-updated timestamp (ISO 8601 with timezone)
guestsarray
Required
Array of guests (1-20). See Guest Object below.
staysarrayOptionalArray of room stays (max 10). See Stay Object below.
extrasarrayOptionalArray of extras/charges (max 100). See Extra Object below.
commentsstringOptionalInternal notes or special requests
metadataobjectOptionalCustom key-value data

Guest Object

ParameterTypeRequiredDescription
first_namestring
Required
Guest first name
last_namestringOptionalGuest last name
emailstringOptionalGuest email address. At least one of email or phone is required to create/match a contact.
phonestringOptionalPhone number in E.164 format. At least one of email or phone is required to create/match a contact.
is_holderbooleanOptionalWhether this is the primary guest/booker
pax_typestringOptionalPassenger type: adult, child, infant
date_of_birthstringOptionalDate of birth (YYYY-MM-DD)
genderstringOptionalGuest gender: male, female, other, prefer_not_to_say
nationalitystringOptionalISO 3166-1 alpha-2 country code
document_typestringOptionalID document type: passport, national_id, drivers_license, other
document_numberstringOptionalID document number (max 50 characters)
addressobjectOptionalGuest's postal address. All sub-fields optional: street (max 300), city (200), postal_code (40), province (200), state (200), country (ISO 3166-1 alpha-2 or alpha-3, normalized to alpha-2). Longer values are truncated and unrecognized ones dropped — a malformed address never rejects the reservation. A sub-field you omit is left untouched rather than cleared. In Spain, province is the two-digit INE code ("07" for the Balearics). Note country here is the country of RESIDENCE, which is not nationality.
email_consentbooleanOptionalPMS-reported email marketing consent. Takes precedence over CDP auto-consent settings.
whatsapp_marketing_consentbooleanOptionalPMS-reported WhatsApp/phone marketing consent. Takes precedence over CDP auto-consent settings.
languagestringOptionalGuest language code (e.g., "en", "es", "fr"). Defaults to "en" for new contacts.
pre_checkin_completed_atstringOptionalISO 8601 timestamp (with offset) when this guest completed online/pre-checkin in the PMS or booking-engine portal. Omit or send null if the guest hasn't pre-checked-in yet.
pre_checkin_sourcestringOptionalWhere the pre-checkin was completed (e.g., "mews", "cloudbeds", "apaleo", "self_service"). Max 50 chars.

Stay Object

ParameterTypeRequiredDescription
start_datestring
Required
Stay start date (YYYY-MM-DD)
end_datestring
Required
Stay end date (YYYY-MM-DD)
room_typestringOptionalRoom type/category
room_numberstringOptionalAssigned room number
board_typestringOptionalMeal plan: RO (Room Only), BB (Bed & Breakfast), HB (Half Board), FB (Full Board), AI (All Inclusive)
rate_codestringOptionalRate code/plan
adultsnumberOptionalNumber of adults
childrennumberOptionalNumber of children
babiesnumberOptionalNumber of infants

Extra Object

ParameterTypeRequiredDescription
namestring
Required
Charge description
amountnumber
Required
Charge amount
codestringOptionalCharge code from PMS
quantitynumberOptionalQuantity (default: 1)
datestringOptionalCharge date (YYYY-MM-DD)
notesstringOptionalAdditional notes

Example Request

curl -X POST https://guestmaker.ai/api/v1/reservations \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reservation_id": "RES-2026-001234",
    "confirmation_number": "CONF-ABC123",
    "hotel_name": "Grand Hotel",
    "check_in": "2026-03-15",
    "check_out": "2026-03-18",
    "status": "confirmed",
    "source": "pms",
    "amount": 450.00,
    "amount_net": 371.90,
    "currency": "EUR",
    "channel": "booking.com",
    "market_segment": "LEISURE",
    "agency_code": "AGY-001",
    "external_created_at": "2026-01-20T14:30:00+01:00",
    "external_updated_at": "2026-02-05T09:15:00+01:00",
    "guests": [
      {
        "first_name": "John",
        "last_name": "Doe",
        "email": "john@example.com",
        "phone": "+1234567890",
        "is_holder": true,
        "pax_type": "adult",
        "opt_in_status": "opted_in",
        "nationality": "US",
        "language": "en",
        "document_type": "passport",
        "document_number": "AB1234567"
      },
      {
        "first_name": "Jane",
        "last_name": "Doe",
        "pax_type": "adult"
      }
    ],
    "stays": [
      {
        "start_date": "2026-03-15",
        "end_date": "2026-03-18",
        "room_type": "Deluxe Suite",
        "room_number": "301",
        "board_type": "BB",
        "adults": 2
      }
    ]
  }'

Response

{
  "success": true,
  "data": {
    "reservation_id": "550e8400-e29b-41d4-a716-446655440000",
    "external_reservation_id": "RES-2026-001234",
    "is_new": true,
    "hotel_id": "550e8400-e29b-41d4-a716-446655440001",
    "contacts": [
      {
        "contact_id": "550e8400-e29b-41d4-a716-446655440002",
        "phone": "+1234567890",
        "email": "john@example.com",
        "is_new": true,
        "is_holder": true
      },
      {
        "contact_id": null,
        "phone": null,
        "email": null,
        "is_new": false,
        "is_holder": false
      }
    ],
    "warnings": []
  }
}

Contact Creation

Guests with a valid phone number are created as contacts in Guestmaker. Guests without phone numbers are stored in the reservation but cannot receive WhatsApp messages. The response indicates which guests were created as contacts via contact_id.

Upsert Behavior

The reservation_id field is your PMS's unique identifier. Sending the same reservation_id again will update the existing reservation rather than creating a duplicate. This enables seamless sync from your PMS without tracking which reservations already exist.

POST
/api/v1/reservations/batch
Create or update up to 50 reservations in a single request. Uses a partial success model — each reservation is processed independently.
Reservation Status Values
Status values and their meaning
StatusDescriptionGuest Stage
confirmedReservation confirmedPre-arrival
pendingBooking engine awaiting payment confirmationPre-arrival
modifiedReservation modified after confirmationPre-arrival
checked_inGuest has arrivedIn-house
checked_outGuest has departedPost-stay
cancelledReservation cancelled—
no_showGuest did not arrive—

Guests API

Create and manage guest contacts with their booking information. Guests are automatically linked to hotels for AI-powered conversations and journey automation.

POST
/api/v1/guests
Create a new guest or update an existing one with booking information

Request Body

ParameterTypeRequiredDescription
phonestring
Required
Phone number in E.164 format (e.g., +1234567890)
first_namestring
Required
Guest's first name
last_namestring
Required
Guest's last name
emailstringOptionalGuest's email address
languagestringOptionalPreferred language code (e.g., en, es, fr). Default: en
tagsstring[]OptionalArray of tags for segmentation
custom_fieldsobjectOptionalKey-value pairs matching your registered custom field definitions. See Custom Fields API for setup.
hotel_namestringOptionalHotel name to link the guest to
hotel_codestringOptionalHotel code (alternative to hotel_name)
sourcestringOptionalSystem type that sent this data: pms, booking_engine, crm, channel_manager, website, etc. Defaults to "api"
bookingobjectOptionalBooking/reservation details (see below)
trigger_eventbooleanOptionalTrigger booking.created event (default: true)
update_if_existsbooleanOptionalUpdate existing contact by phone (default: true)
opt_in_statusstringOptionalLegacy field (deprecated). Use whatsapp_marketing_consent instead.
whatsapp_marketing_consentbooleanOptionalWhatsApp marketing consent (default: false). When true, guest receives marketing WhatsApp messages.
email_consentbooleanOptionalEmail marketing consent (default: false). When true, guest receives marketing emails.
date_of_birthstringOptionalGuest date of birth (YYYY-MM-DD)
genderstringOptionalGuest gender: male, female, other, prefer_not_to_say
nationalitystringOptionalISO 3166-1 alpha-2 country code (e.g., US, ES, GB)
document_typestringOptionalID document type: passport, national_id, drivers_license, other
document_numberstringOptionalID document number (max 50 characters)
addressobjectOptionalGuest's postal address. All sub-fields optional: street (max 300), city (200), postal_code (40), province (200), state (200), country (ISO 3166-1 alpha-2 or alpha-3, normalized to alpha-2). Longer values are truncated and unrecognized ones dropped — a malformed address never rejects the request. A sub-field you omit is left untouched rather than cleared. In Spain, province is the two-digit INE code ("07" for the Balearics). Note country here is the country of RESIDENCE, which is not nationality.
subscription_statusstringOptionalCommunication subscription status: active, unsubscribed
communication_preferencesobjectOptionalGranular communication preferences (see below)
consent_sourcestringOptionalYour identifier for where consent was collected
consent_timestampstringOptionalISO 8601 timestamp when consent was given

Booking Object

ParameterTypeRequiredDescription
booking_idstring
Required
Parent booking ID (groups multiple reservations, e.g., Expedia booking with 4 rooms)
reservation_idstringOptionalIndividual reservation/localizer code (defaults to booking_id if not provided)
check_instring
Required
Check-in date (YYYY-MM-DD)
check_outstring
Required
Check-out date (YYYY-MM-DD)
room_typestringOptionalRoom type/category
room_numberstringOptionalAssigned room number (if known)
rate_planstringOptionalRate plan name
board_typestringOptionalMeal plan: RO (Room Only), BB (Bed & Breakfast), HB (Half Board), FB (Full Board), AI (All Inclusive)
booking_channelstringOptionalRaw PMS channel code (e.g. BDC, EXP). Stored as booking_channel_code. Auto-resolved to human-readable name and channel type (direct/ota/tour_operator) if tenant has mappings configured.
total_amountnumberOptionalTotal booking amount
currencystringOptionalCurrency code (default: EUR)
statusstringOptionalConfirmed, Modified, CheckedIn, CheckedOut, Cancelled, NoShow
guestsarrayOptionalAdditional guests on the booking (see Booking Guests below)
extrasarrayOptionalBooking extras like spa, minibar, restaurant (see Booking Extras below)

Booking Guests Array

ParameterTypeRequiredDescription
first_namestring
Required
Guest's first name
last_namestringOptionalGuest's last name
emailstringOptionalGuest's email address
is_holderbooleanOptionalWhether this guest is the reservation holder (default: false)
pax_typestringOptionalAdult, Child, or Infant
relationship_to_holderstringOptionalSpouse, Child, Colleague, Friend, etc.
pre_checkin_completed_atstringOptionalISO 8601 timestamp (with offset) when this companion guest completed online/pre-checkin.
pre_checkin_sourcestringOptionalWhere the pre-checkin was completed (e.g., "mews", "cloudbeds", "self_service"). Max 50 chars.

Booking Extras Array

ParameterTypeRequiredDescription
namestring
Required
Extra name (e.g., Spa Services, Minibar, Restaurant)
amountnumber
Required
Amount charged
quantitynumberOptionalQuantity (default: 1)
notesstringOptionalAdditional notes or description

Communication Preferences Object (legacy)

Legacy communication preferences. Prefer using whatsapp_marketing_consent and email_consent boolean fields directly on the guest object.

ParameterTypeRequiredDescription
marketingbooleanOptionalConsent to receive marketing messages (promotions, offers). Default: false. Maps to whatsapp_marketing_consent.
utilitybooleanOptionalUtility messages (booking confirmations, reminders) are always delivered. This field is ignored.

Note: When a guest unsubscribes via WhatsApp chat, they can choose to stop all communications or only marketing messages while keeping utility messages active.

Example Request

curl -X POST https://guestmaker.ai/api/v1/guests \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+1234567890",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "nationality": "US",
    "document_type": "passport",
    "document_number": "AB1234567",
    "source": "pms",
    "hotel_name": "Grand Hotel",
    "booking": {
      "booking_id": "BK-12345",
      "check_in": "2026-03-15",
      "check_out": "2026-03-18",
      "room_type": "Deluxe Suite",
      "total_amount": 450.00,
      "status": "Confirmed"
    }
  }'

Response

{
  "success": true,
  "data": {
    "contact_id": "550e8400-e29b-41d4-a716-446655440000",
    "is_new": true,
    "hotel_id": "550e8400-e29b-41d4-a716-446655440001",
    "reservation_id": "550e8400-e29b-41d4-a716-446655440002",
    "event_id": "550e8400-e29b-41d4-a716-446655440003",
    "message": "Guest created successfully"
  }
}
POST
/api/v1/guests/batch
Create or update up to 500 guests in a single request. Uses a partial success model — each guest is processed independently.
GET
/api/v1/guests
Retrieve a guest by phone number with their reservations

Events API

Send events to trigger journey automations. Events are matched against active journey triggers to start automated WhatsApp conversations.

POST
/api/v1/events
Create an event to trigger matching journey automations

Request Body

ParameterTypeRequiredDescription
event_typestring
Required
Event type identifier (e.g., guest.checked_in)
payloadobject
Required
Event data available in journey context
contact_idstringOptionalContact UUID (if known)
guest_phonestringOptionalPhone number to identify the contact (E.164)
hotel_idstringOptionalHotel UUID (if known)
hotel_namestringOptionalHotel name to resolve hotel_id
hotel_codestringOptionalHotel code to resolve hotel_id
idempotency_keystringOptionalUnique key to prevent duplicate events
sourcestringOptionalSource system identifier for tracking

Standard Event Types

These standard event types are recognized by the system. You can also create custom event types.

booking.created

New booking received

booking.updated

Booking modified

booking.cancelled

Booking cancelled

guest.checked_in

Guest arrived

guest.checked_out

Guest departed

guest.message

Guest sent message

payment.received

Payment confirmed

review.requested

Review requested

Custom event types should follow the pattern category.action (e.g., spa.appointment_booked)

Example Request

curl -X POST https://guestmaker.ai/api/v1/events \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "guest.checked_in",
    "guest_phone": "+1234567890",
    "hotel_name": "Grand Hotel",
    "payload": {
      "room_number": "301",
      "checked_in_at": "2026-03-15T14:30:00Z"
    },
    "idempotency_key": "checkin-BK-12345"
  }'

Response

{
  "success": true,
  "data": {
    "event_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "accepted",
    "contact_id": "550e8400-e29b-41d4-a716-446655440001",
    "hotel_id": "550e8400-e29b-41d4-a716-446655440002",
    "matching_journeys": 2,
    "created_at": "2026-03-15T14:30:00Z",
    "message": "Event accepted and queued for processing"
  }
}

Idempotency

Always include an idempotency_key to prevent duplicate events. If you send the same idempotency key twice, the second request returns the original event.

GET
/api/v1/events
List available event types for your tenant

Custom Fields API

Define custom data fields for your integration to extend guest profiles with partner-specific data. Custom fields appear in the segment builder, enabling hotel teams to create targeted segments based on your data.

POST
/api/v1/fields
Create or update custom field definitions. Uses upsert based on field_key — sending an existing key updates it.

Required Scope

custom_fields:write

Request Body

ParameterTypeRequiredDescription
fieldsarray
Required
Array of field definitions (max 50)
fields[].field_keystring
Required
Unique key (lowercase, a-z, 0-9, underscores, must start with letter)
fields[].field_labelstring
Required
Display label shown to hotel staff
fields[].field_typestring
Required
One of: string, text, number, boolean, date, datetime, select, multiselect
fields[].optionsstring[]OptionalRequired for select/multiselect types
fields[].descriptionstringOptionalHelp text for hotel staff
fields[].is_requiredbooleanOptionalWhether field must have a value (default: false)
fields[].is_visiblebooleanOptionalShow in contact details UI (default: true)
fields[].display_ordernumberOptionalSort order within your fields (default: 0)

Example Request

curl -X POST https://guestmaker.ai/api/v1/fields \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      {
        "field_key": "loyalty_tier",
        "field_label": "Loyalty Tier",
        "field_type": "select",
        "options": ["Bronze", "Silver", "Gold", "Platinum"],
        "description": "Guest loyalty program tier"
      },
      {
        "field_key": "loyalty_points",
        "field_label": "Loyalty Points",
        "field_type": "number",
        "description": "Current loyalty point balance"
      },
      {
        "field_key": "member_since",
        "field_label": "Member Since",
        "field_type": "date"
      }
    ]
  }'

Response

{
  "success": true,
  "data": {
    "fields": [
      { "field_key": "loyalty_tier", "field_label": "Loyalty Tier", "field_type": "select", "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:00:00Z" },
      { "field_key": "loyalty_points", "field_label": "Loyalty Points", "field_type": "number", "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:00:00Z" },
      { "field_key": "member_since", "field_label": "Member Since", "field_type": "date", "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:00:00Z" }
    ],
    "message": "3 field definition(s) created/updated successfully"
  }
}
GET
/api/v1/fields
List all custom field definitions registered by your integration

Integration with Segments

Custom fields set via the Guests API (custom_fields object) are automatically stored against the matching field definitions. Hotel staff can use these fields in the segment builder under the 'Custom Fields' category to create targeted audience segments.

Field Types Reference
Supported field types and their expected values
TypeDescriptionExample Value
stringShort text (single line)"John Doe"
textLong text (multi-line)"Special dietary requirements..."
numberNumeric value1500
booleanTrue/falsetrue
dateDate (YYYY-MM-DD)"2026-03-15"
datetimeDate and time (ISO 8601)"2026-03-15T14:30:00Z"
selectSingle choice from options"Gold"
multiselectMultiple choices from options["spa", "golf"]

CDP API

The Customer Data Platform API enables anonymous visitor tracking, identity resolution, and post-checkout reservation ingestion. Track website visitors, link them to known identities, and ingest reservation data for profile consolidation across your hotel group.

GuestMaker.js SDK

For website tracking, use the GuestMaker.js SDK which handles visitor ID management, event batching, and automatic page view tracking. See the SDKs page for setup instructions.

Domain Restrictions

When domain restrictions are configured in CDP Settings, the events endpoint validates the Origin header against the allowed domains list. Requests from non-allowed origins will be rejected with a 403 error.

POST
/api/v1/cdp/events
Submit batched visitor tracking events from your website's tracking snippet. Returns 202 Accepted -- events are processed asynchronously.

Required Scope

cdp:write

Requires visitor tracking to be enabled in CDP Settings.

Request Body

ParameterTypeRequiredDescription
visitor_idstring
Required
Anonymous visitor identifier (from SDK cookie)
session_idstring
Required
Session identifier (from SDK sessionStorage)
deviceobjectOptional{ type: "desktop"|"mobile"|"tablet", browser, os, language }
utmobjectOptional{ source, medium, campaign, term, content }
referrerstringOptionalHTTP referrer URL
eventsarray
Required
Array of event objects (1-100). See Event Object below.

Event Object

ParameterTypeRequiredDescription
typestring
Required
Event type: page_view, scroll, click, form_start, form_submit, identify, custom
urlstringOptionalPage URL (domain stripped server-side for privacy)
titlestringOptionalPage title
dataobjectOptionalCustom event data (e.g., scroll depth, click element)
tsstring
Required
ISO 8601 timestamp

Example Request

curl -X POST https://guestmaker.ai/api/v1/cdp/events \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "visitor_id": "vis_abc123def456",
    "session_id": "sess_789xyz",
    "device": {
      "type": "desktop",
      "browser": "Chrome 120",
      "os": "Windows 11",
      "language": "en-US"
    },
    "utm": {
      "source": "google",
      "medium": "cpc",
      "campaign": "summer-2026"
    },
    "referrer": "https://www.google.com/search?q=luxury+hotels",
    "events": [
      {
        "type": "page_view",
        "url": "/rooms/deluxe-suite",
        "title": "Deluxe Suite | Grand Hotel",
        "ts": "2026-02-19T14:30:00.000Z"
      },
      {
        "type": "scroll",
        "url": "/rooms/deluxe-suite",
        "data": { "depth": 75 },
        "ts": "2026-02-19T14:30:45.000Z"
      },
      {
        "type": "click",
        "url": "/rooms/deluxe-suite",
        "data": { "element": "book-now-btn", "text": "Book Now" },
        "ts": "2026-02-19T14:31:02.000Z"
      }
    ]
  }'

Response (202 Accepted)

{
  "success": true,
  "data": {
    "accepted": 3
  }
}
POST
/api/v1/cdp/identify
Link an anonymous visitor to a known identity (email, phone, name). Creates or links a CDP profile and enriches the visitor record.
POST
/api/v1/cdp/reservations
Ingest a single post-checkout reservation with guests and extras for identity resolution. Uses the CDP-specific ingestion pipeline (separate from the main Reservations API).
POST
/api/v1/cdp/reservations/batch
Synchronous batch of up to 100 reservations. Each reservation is processed independently with partial success semantics.
POST
/api/v1/cdp/reservations/bulk
Async bulk ingestion of up to 5,000 reservations. Returns 202 Accepted with a batch_id for status polling.
GET
/api/v1/cdp/batches/{id}
Check the status of a bulk ingestion batch. Poll this endpoint after submitting a bulk ingestion request.

Loyalty API

The loyalty program offers two integration models — choose by who is acting. Your backend can act for the hotel over the REST API, or a guest can sign themselves in with Loyalty SSO. Most booking-engine integrations use both.

Server-to-server

Server REST API

Your backend acts for the hotel with an API key. Look up any guest by email; enroll, award, redeem, sync points and cash credit.

Auth · Authorization: Bearer <api_key>
Endpoints below
Guest sign-in · OIDC

Loyalty SSO (OIDC)

The guest signs in with their loyalty account (OAuth2 + PKCE). Read only that guest’s tier, balance and history to personalise the booking flow.

Auth · per-guest tokens (id_token + Bearer access token)
Integration guide

Not sure which? Use the REST API when your server acts on the hotel’s behalf (PMS sync, awarding, redemption, cash credit). Use Loyalty SSO when a guest logs in on your site and you personalise for them. Balance & history exist in both worlds by design — same data, different trust boundary.

GET
/api/v1/loyalty/check
Quick check whether an email address is enrolled in the loyalty program

Query Parameters

ParameterTypeRequiredDescription
emailstring
Required
Guest email address

Response (Member Found)

{
  "success": true,
  "data": {
    "is_member": true,
    "member_summary": {
      "member_id": "uuid",
      "tier_name": "Gold",
      "tier_slug": "gold",
      "points_balance": 1500,
      "lifetime_points": 3000,
      "joined_at": "2025-01-15T10:30:00Z"
    }
  }
}

Response (Not a Member)

{
  "success": true,
  "data": {
    "is_member": false,
    "member_summary": null
  }
}
POST
/api/v1/loyalty/enroll
Enroll a new member in the loyalty program (creates contact if needed)
POST
/api/v1/loyalty/unsubscribe
Deactivate a member (leave the program). The inverse of enroll — keeps points, blocks SSO sign-in, and is reversible by re-enrolling.
GET
/api/v1/loyalty/member
Retrieve full loyalty member details including tier, points, and available rewards
POST
/api/v1/loyalty/points
Award points to a loyalty member (e.g., for bookings, referrals, promotions)
POST
/api/v1/loyalty/redeem
Redeem a reward for a loyalty member. Designed for PMS / POS / booking-engine auto-ingest at scale (idempotent + auto-fulfilled supported).
POST
/api/v1/loyalty/redemptions/batch
Submit up to 100 redemptions in a single call. Each item is independently processed + independently idempotent. Built for nightly reconciliation jobs across a hotel group.
GET
/api/v1/loyalty/rewards
List all available rewards with optional category filtering and pagination
GET
/api/v1/loyalty/tiers
Retrieve all loyalty program tiers with benefits and qualification thresholds
GET
/api/v1/loyalty/transactions
Retrieve points transaction history for a loyalty member
GET
/api/v1/loyalty/calculate
Calculate how many points would be earned for a given spend amount
More REST endpoints — same Bearer auth and response envelope, grouped for completeness.
Earning & adjustments guests:write
  • POST /api/v1/loyalty/actions — award a non-stay action bonus (review, survey, app download)
  • POST /api/v1/loyalty/points/confirm — confirm a pending earn into the live balance
  • POST /api/v1/loyalty/points/void — void a pending earn before confirmation
  • POST /api/v1/loyalty/reverse — reverse earned points on cancellation / no-show
  • POST /api/v1/loyalty/apply-discount — spend points as a currency discount on a booking/POS
  • POST /api/v1/loyalty/redeem-free-night — redeem points for a free-night voucher code
Advanced
  • GET /api/v1/loyalty/conversion — points ↔ currency calculator guests:read
  • GET /api/v1/loyalty/households · POST — household pool balance & pooled redemption
  • GET /api/v1/loyalty/liability — points-liability & program-health report guests:read
  • POST /api/v1/loyalty/experiences/bid — bid points on an auction experience

Authentication & Scopes

Authenticate every REST call with Authorization: Bearer <api_key> — your tenant is derived from the key (no separate slug header). Read endpoints (check, member, rewards, tiers, transactions, calculate) require the guests:read scope; write endpoints (enroll, points, redeem, cash credit) require guests:write. All endpoints are rate limited to 10 requests/minute per IP.

JavaScript SDK Available

A client-side JavaScript SDK wraps the core loyalty endpoints with automatic header injection and error handling.

Cash Credit

New

Let members spend their points as money credit on direct bookings, in-stay folio charges, and at checkout. Atomic Quote → Hold → Confirm/Release/Expire lifecycle — see the deep-dive pages for the full integration walkthrough.

Overview

Lifecycle diagram, surfaces, error codes, and the OTA denylist.

Booking engine

5-minute drop-in <script> embed with apply callback.

PMS

/reservation-snapshot + 5 HMAC webhooks for real-time balance sync.

Endpoints (scope guests:write)
  • GET /api/v1/loyalty/credit/quote — read spendable + presets
  • POST /api/v1/loyalty/credit/hold — reserve points (atomic, idempotent on external_reference_id)
  • POST /api/v1/loyalty/credit/confirm — commit deduction
  • POST /api/v1/loyalty/credit/release — release an active hold
  • POST /api/v1/loyalty/credit/reverse — claw back after cancellation
  • GET /api/v1/loyalty/reservation-snapshot — PMS folio render in one call

Loyalty SSO (OIDC)

GuestMaker is a standard OpenID Connect Identity Provider for loyalty guests. Your website signs guests in with their loyalty account via Authorization Code + PKCE, then reads tier, points balance and history to personalise the booking flow.

Integration guide

The flow, scopes & claims, mandatory security requirements, token model, and endpoints.

Booking engine quickstart

A copy-paste, framework-neutral walkthrough — authorize URL, PKCE, token exchange, id_token verification, and reading member data.

Endpoints (Authorization Code + PKCE)
  • GET /api/oidc/.well-known/openid-configuration — discovery (load endpoints + JWKS from here)
  • GET /api/oidc/auth — authorize (browser redirect, PKCE S256)
  • POST /api/oidc/token — token exchange (client_secret_basic)
  • GET /api/oidc/jwks — RS256 public signing keys
  • GET /api/loyalty/me/balance — member tier + points (Bearer)
  • GET /api/loyalty/me/transactions — points history (Bearer)

Newsletter Signup

Embed a newsletter signup form on your own website. Confirmed subscribers become contacts with marketing consent granted, so your recurring newsletter campaigns reach them automatically. Configure and activate the widget first in Settings → Email → Newsletter Signup, then copy your publishable key from the Install tab. These routes accept two auth modes: a gm_pub_… publishable key for browser embeds (safe to ship in page HTML; the request Origin must match your configured domain allow-list), or a secret gmkr_… API key for server-to-server calls (e.g. a booking engine) — secret-key requests are trusted by the key alone and skip the Origin check. See Authentication below.

Quick install (zero-code drop-in)

<div data-gm-newsletter
     data-token="gm_pub_your_publishable_key"
     data-fields="email"></div>
<script src="https://www.guestmaker.ai/sdk/guestmaker.js" async></script>

Or build your own UI and call the SDK headlessly:

GuestMaker.newsletter.subscribe({
  email: 'guest@example.com',
  token: 'gm_pub_your_publishable_key'
}).then(function (r) {
  // r.status === 'pending' | 'subscribed'
})

Always use the host https://www.guestmaker.ai — the apex domain redirects and drops the auth header.

POST
/api/v1/newsletter/subscribe
Subscribe an email address. Double opt-in sends a confirmation email; single opt-in subscribes immediately.

Authentication

Frontend (browser widget) — publishable key as a Bearer token (Authorization: Bearer gm_pub_…). The request Origin/Referer host must be on your domain allow-list, or the request is rejected with 403. This is the path the drop-in widget and SDK use.

Backend (server-to-server) — a secret key as a Bearer token (Authorization: Bearer gmkr_…) with the guests:write scope. Use this when subscribing from your own server (no browser Origin) — the secret key is the trust anchor, so the Origin allow-list and Turnstile are skipped. Never expose a gmkr_… key in client-side code.

Either way, the newsletter widget must first be activated in Settings → Email → Newsletter Signup; a 403 with Newsletter signup not enabled means no active config exists yet.

Backend example (server-to-server)

curl -X POST https://www.guestmaker.ai/api/v1/newsletter/subscribe \
  -H "Authorization: Bearer gmkr_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "guest@example.com",
    "consent": true,
    "source": "booking_engine"
  }'

Request Body

ParameterTypeRequiredDescription
emailstring
Required
Subscriber email (max 254)
consenttrue
Required
Explicit consent; rejected if absent or false
first_namestringOptionalOnly stored if the first_name field is enabled in your config
languagestringOptionalISO 639-1 (max 10), normalised to lowercase
hotel_idUUIDOptionalProperty of interest; validated against your active hotels
sourcestringOptionalFree-form provenance label (max 200)
turnstile_tokenstringOptionalCloudflare Turnstile token, when Turnstile is enabled
_hpstringOptionalHoneypot — must be empty; a non-empty value is silently treated as a bot

Response (200)

{ "status": "pending" }

pending = double opt-in confirmation email sent; subscribed = single opt-in, marketable immediately. The response is identical for new / pending / already-confirmed addresses (no membership enumeration).

GET
/api/public/newsletter/confirm
Two-step double opt-in confirmation (browser-facing, not a JSON API)
GET
/api/v1/newsletter/config
Presentation-only config the drop-in widget uses to self-render

Webhooks

Receive real-time notifications when events occur in the platform. Configure webhook endpoints in your dashboard settings.

Available Webhook Events
Events that can be delivered to your webhook endpoints
message.receivedIncoming message
message.sentOutgoing message sent
message.deliveredMessage delivered
message.readMessage read by recipient
contact.createdNew contact added
contact.updatedContact info changed
contact.deletedContact deleted
conversation.createdNew conversation started
conversation.closedConversation closed
journey.startedJourney automation began
journey.completedJourney automation finished
journey.failedJourney execution error
broadcast.sentBroadcast campaign sent
broadcast.completedBroadcast finished
reservation.checked_inGuest checked in (daily date-scan — see note)
reservation.checked_outGuest checked out (daily date-scan — see note)

Note — reservation.checked_in / checked_out timing. These fire from a daily date-scan when the guest's check-in or check-out date is reached — not in real time. Each delivery includes a scheduled_for object with the guest's expected local check-in/out time, so you can schedule downstream actions on your side. The expected times are configurable per account (defaults: check-in 15:00, check-out 12:00, hotel timezone).

Webhook Payload Format
All webhooks follow this consistent format
{
  "event_type": "contact.created",
  "timestamp": "2026-03-15T10:30:00Z",
  "data": {
    "contact": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "phone": "+1234567890",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john@example.com",
      "hotel_id": "550e8400-e29b-41d4-a716-446655440001"
    }
  }
}

Reservation events carry the booking, the guest, and the scheduled_for hint:

{
  "event_type": "reservation.checked_in",
  "timestamp": "2026-07-10T03:12:08Z",
  "data": {
    "reservation": {
      "id": "…", "hotel_id": "…", "status": "CheckedIn",
      "check_in": "2026-07-10", "check_out": "2026-07-12",
      "localizer_code": "ABC123", "booking_id": "…",
      "source": "…", "booking_channel": "…", "total_value": 540, "currency": "EUR"
    },
    "contact": {
      "id": "…", "first_name": "John", "last_name": "Doe",
      "email": "john@example.com", "phone": "+34600000000",
      "language": "en", "nationality": "GB"
    },
    "scheduled_for": {
      "date": "2026-07-10", "local_time": "15:00",
      "timezone": "Europe/Madrid", "iso": "2026-07-10T13:00:00.000Z"
    }
  }
}
Security Headers
Headers included with every webhook request
ParameterTypeRequiredDescription
X-Webhook-Signaturestring
Required
HMAC-SHA256 signature: sha256=...
X-Webhook-Eventstring
Required
The event type being delivered
X-Webhook-Deliverystring
Required
Unique delivery ID (UUID)
X-Webhook-Timestampstring
Required
ISO 8601 timestamp of the event
Verifying Signatures
Always verify webhook signatures to ensure requests are authentic
const crypto = require('crypto');

// Sign over `timestamp + "." + rawBody` (the timestamp is signed for replay
// resistance). Verify against the RAW request body, never a re-serialized object.
function verifyWebhookSignature(rawBody, timestamp, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return `sha256=${expected}` === signature;
}

// Capture the raw body (do NOT re-serialize req.body):
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
  const rawBody = req.body.toString('utf8');
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-webhook-signature'];

  if (!verifyWebhookSignature(rawBody, timestamp, signature, 'your_webhook_secret')) {
    return res.status(401).send('Invalid signature');
  }
  // Replay protection: reject stale timestamps (±5 min); dedup on X-Webhook-Delivery.
  if (Math.abs(Date.now() - new Date(timestamp).getTime()) > 5 * 60 * 1000) {
    return res.status(401).send('Stale timestamp');
  }

  const { event_type, data } = JSON.parse(rawBody);
  console.log(`Received event: ${event_type}`, data);

  res.status(200).send('OK');
});

CTI / Telephony

Connect your call center to GuestMaker for inbound screen pop, click-to-call, call recording, and a per-contact call history. All providers expose the same unified contract — connect once, and your agents get the same experience whether the underlying system is Ringover, Microsoft Teams, or another supported provider.

Setup
Connecting a CTI provider to GuestMaker

Step 1. In the dashboard, open Settings → Integrations → {Provider} and click Connect.

Step 2. Paste your provider credentials (API key for Ringover, OAuth consent flow for Teams). GuestMaker validates the credentials and returns a per-tenant Webhook URL and Authorization token.

Step 3. In your provider's dashboard, configure a webhook with that URL and token. Enable these events: call_ringing, call_answered, call_hangup, call_missed, record_available.

Step 4. Click Test connection — a simulated incoming call fires the screen pop in your dashboard.

Step 5. Configure screen-pop modes (auto-navigate, notification banner, new-tab) and the widget position. Agents are auto-mapped from your provider's user list by email.

Webhook Events
Call lifecycle events GuestMaker accepts from every CTI provider
call_ringingCall ringing — triggers contact lookup + screen pop
call_answeredCall picked up by an agent
call_hangupCall ended (carries duration in seconds)
call_missedCall not answered
record_availableRecording URL ready
call_completedOne post-call record (metadata + optional transcript/summary) — see Completed-Call Push below
Webhook Authentication
Bearer-token auth in the Authorization header, timing-safe-compared per request
POST /api/webhooks/cti
Authorization: Bearer <your_webhook_token>
Content-Type: application/json

{
  "event_type": "call_ringing",
  "call_id": "ringover-abc-123",
  "user_id": "ringover-user-42",
  "from_number": "+34612345678",
  "to_number": "+34900111222",
  "direction": "inbound"
}

Your token maps to one integration, which identifies both the account and the provider — so this single URL works for every provider, no vendor slug needed. The scoped form /api/webhooks/cti/{provider} is equivalent and still supported.

Field names are accepted in either snake_case or camelCase, and may also appear nested under a data object. Per-tenant rate limit: 100 requests/minute.

Completed-Call Push (call_completed)
Send one record per finished call — for providers that push call details after hang-up, instead of (or alongside) the live lifecycle events

POST one call_completed event per finished call. Re-send the same call_id later to update the record in place — the intended way to attach a transcript once transcription finishes. When a transcript is present, it is mined into the guest's AI memory.

POST /api/webhooks/cti
Authorization: Bearer <your_webhook_token>
Content-Type: application/json

{
  "event": "call_completed",
  "call_id": "zn-8f3a2c91",
  "direction": "inbound",
  "from": "+34600111222",
  "to": "+34971000000",
  "agent": { "id": "zoon-user-42", "email": "agent@hotel.com" },
  "caller": { "name": "María García", "email": "maria@example.com" },
  "started_at": "2026-07-16T10:31:04Z",
  "answered_at": "2026-07-16T10:31:12Z",
  "ended_at": "2026-07-16T10:35:49Z",
  "outcome": "completed",
  "recording_url": "https://rec.example.com/zn-8f3a2c91.mp3",
  "summary": "Guest asked about a late checkout on the 18th.",
  "transcript": [
    { "speaker": "guest", "text": "Hola, quería un late checkout.", "offset_ms": 0 },
    { "speaker": "agent", "text": "Claro, dígame su habitación.", "offset_ms": 4200 }
  ]
}

Request Body

ParameterTypeRequiredDescription
eventstring
Required
The literal "call_completed".
call_idstring
Required
Your stable unique id — the sole idempotency key, scoped per (tenant, provider).
directionstring
Required
inbound or outbound. Omit it entirely to send a content patch (see below).
from / tostring
Required
E.164 numbers. The guest side (from on inbound, to on outbound) resolves to — or creates — a contact.
started_at / ended_atstring
Required
ISO 8601. A full record missing either is dropped; a content patch does not need them.
answered_atstringOptionalISO 8601. Absent or null means the call was never answered.
duration_secondsnumberOptionalDerived from ended_at − (answered_at or started_at) when omitted.
agentobjectOptional{ id, email } — matched to the GuestMaker user who handled the call. An email in either field lets us resolve the user without a manual mapping.
callerobjectOptional{ name | first_name/last_name, email } — the guest's known details. Fills blank contact fields only; never overwrites.
outcomestringOptionalcompleted | missed | no_answer | voicemail | free text. missed, no_answer and voicemail store the call as missed.
recording_urlstringOptionalStored verbatim and shown as a play link on the contact. The audio stays on your servers.
transcriptarrayOptional[{ speaker, text, offset_ms? }] — feeds guest-memory extraction. Capped at 500 turns / 200,000 characters; oversized pushes are truncated, not rejected.
transcript_textstringOptionalPlain-text alternative. Guest:/Agent: prefixed lines are split into turns when at least two are detected.
summarystringOptionalYour AI summary — shown italicised under the call in the contact's history.
language, metadatastring, objectOptionalMerged into the call's metadata. Existing keys are kept; only keys present in the push are added or overwritten.

Caller enrichment. The optional caller object fills only the resolved contact's blank name/email fields — it never overwrites a value already on file. Carrier caller-ID labels ("MOBILE CALLER", "Número privado") and placeholder names are rejected, and an email is applied only if valid and not already owned by another contact.

Idempotency. Re-sending the same call_id updates the record with COALESCE semantics — optional fields a re-push doesn't carry never erase stored content. Guest-memory extraction runs only the first time a transcript arrives.

Content patches. Recording and transcription usually finish minutes after the call. You don't have to re-send the whole record when they do — send call_id plus only the new content, omitting direction, from/to and the timestamps. The patch merges recording_url, transcript, summary, outcome and caller into the existing call and rewrites nothing else. Send as many as you need. A patch is merge-only — it can never create a call, so always send the full record at hangup first; a patch that finds no matching call_id is dropped.

Responses. 200 {"received": true} (processed asynchronously), 401 bad token, 400 invalid JSON, 429 rate limited (backfills should drip and honor retries).

Embedded CTI panel (SDK v1)
Two-way communication between a CTI panel embedded in the dashboard and GuestMaker
<script src="https://www.guestmaker.ai/cti/sdk/v1.js"></script>
<script>
  const gm = await GuestMakerCTI.connect({
    hostOrigin: "https://www.guestmaker.ai"
  });

  gm.agentEmail;  // the GuestMaker user signed in right now
  gm.locale;      // "es"

  await gm.openContactByPhone("+34661024890");
  gm.on("clickToCall", ({ phone }) => dialer.call(phone));
</script>

Methods (panel → GuestMaker)

ParameterTypeRequiredDescription
setPanelMode(mode){ mode }
Required
mode is "docked", "collapsed" or "hidden". Resolves with the mode actually applied, which may differ if the host declines.
lookupContact(phone){ found, contactId?, displayName? }
Required
E.164 phone. Read-only caller ID — resolves whether a contact exists and its display name, without moving the dashboard. Name only: never email, reservations or history.
openContactByPhone(phone){ found, contactId? }
Required
E.164 phone. Opens the guest's record on a hit; does not navigate on a miss.
screenPop(params){ found, contactId?, popped }Optional{ phone, callId?, direction?, state? }. Announces a live call in this browser and honours the tenant's screen-pop settings. popped reports whether anything actually happened.
openContactSearch(query){ shown }OptionalFree text, max 200 chars. Opens the contacts list with the search prefilled. Returns no records.
openCreateContact({ phone }){ shown }OptionalOpens the create-contact form prefilled. A human saves it — the call writes nothing.

Events (GuestMaker → panel)

ParameterTypeRequiredDescription
clickToCall{ phone, contactId? }OptionalAn agent clicked a phone number anywhere in the dashboard.
panelModeChanged{ mode }OptionalThe agent collapsed or restored the panel from our chrome, so yours can stay in sync.

Error codes

ParameterTypeRequiredDescription
UNKNOWN_METHODhostOptionalNot a method in this protocol version.
INVALID_PARAMShostOptionalFailed validation; the message names the offending field.
NOT_FOUNDhostOptionalWell-formed request, nothing matched.
NOT_PERMITTEDhostOptionalOrigin or tenant not allowed to make that call.
TIMEOUTsdkOptionalNo response within 10 seconds. Raised locally, not by the host.

The protocol is the contract. The SDK is convenience — you can implement the messages directly. Every message carries gmcti: 1; a request is { gmcti, id, type: "request", method, params } and the reply echoes your id verbatim.

Intents, not guest data. Responses carry booleans, identifiers and applied state — never names, emails, reservations or history. If you need contact data in your own systems, use GET /api/v1/guests?phone= server-to-server with a guests:read key, which also works when no browser tab is open.

Before you can connect. Send us the exact origin your panel is served from — we allowlist it on both the message boundary and our CSP, and nothing loads until we do.

POST
/api/contacts/{contactId}/call
Place an outbound call from the dashboard or your own app
GET
/api/contacts/{contactId}/calls
All calls for a contact, newest first
Screen Pop Modes
Three independent toggles, configured per tenant
ParameterTypeRequiredDescription
auto_navigatebooleanOptionalDefault true. Current tab navigates to /contacts/{id} on ring.
notificationbooleanOptionalDefault true. Slide-in banner with caller name + Open Contact button.
new_tabbooleanOptionalDefault false. Opens /contacts/{id} in a background tab.
widget_positionstringOptionalProvider SDK widget anchor: 'bottom-right' (default) or 'bottom-left'.
Auto-Created Contacts
What happens when an unknown number calls in

Unknown inbound numbers are normalized to E.164 and looked up against your tenant's contact list. If no match is found, GuestMaker creates a minimal contact with source = '{provider}' (e.g. 'ringover'). The screen pop fires with is_new_contact: true so the agent sees a fresh record.

Note: phone matching is exact-equality on E.164. Providers that deliver bare national numbers (without a country code) may produce duplicate contacts. Normalize upstream where possible.

Data Models

Reference documentation for the data structures used throughout the API.

Contact Object
Represents a guest/contact in the system
{
  "id": "uuid",                    // Unique identifier
  "phone": "+1234567890",          // Phone in E.164 format
  "phone_normalized": "1234567890", // Normalized phone (digits only)
  "first_name": "John",
  "last_name": "Doe",
  "email": "john@example.com",
  "language": "en",                // ISO 639-1 language code
  "tags": ["vip", "returning"],    // Array of tags
  "custom_fields": {},             // Custom key-value pairs
  "opt_in_status": "opted_in",     // Legacy (deprecated). Use consent booleans below.
  "subscription_status": "active", // active | unsubscribed (hard block)
  "whatsapp_marketing_consent": true,  // WhatsApp marketing gate
  "email_consent": true,           // Email marketing master gate
  "guest_stage": "pre_stay",       // unknown | pre_stay | during_stay | post_stay
  "hotel_id": "uuid",              // Linked hotel
  "current_reservation_id": "uuid", // Active reservation
  "source": "pms",                 // System type: pms, booking_engine, crm, etc.

  // Personal data fields
  "date_of_birth": "1985-06-15",   // YYYY-MM-DD format
  "gender": "male",                // male | female | other | prefer_not_to_say
  "nationality": "US",             // ISO 3166-1 alpha-2 code
  "document_type": "passport",     // passport | national_id | drivers_license | other
  "document_number": "AB1234567",  // ID document number

  // Consent & subscription fields
  "subscription_status": "active", // active | unsubscribed
  "opted_out_at": null,            // Timestamp when unsubscribed
  "consent_source": "api",         // Where consent was collected
  "consent_partner_id": "uuid",    // Integration partner who collected consent
  "consent_updated_at": "2024-03-01T10:00:00Z",

  "created_at": "2024-03-01T10:00:00Z",
  "updated_at": "2024-03-01T10:00:00Z"
}
Reservation Object
Represents a booking/reservation
{
  "id": "uuid",
  "tenant_id": "uuid",
  "hotel_id": "uuid",
  "localizer_code": "BK-12345",    // Your external booking ID
  "external_id": "BK-12345",       // External system reference
  "source": "hotelinking",         // Canonical origin (e.g. "neobookings", "roiback", "hotelinking", "manual")
  "source_code": "hotelinking",    // Raw value sent by producer (preserved for audit)
  "source_type": "pms",            // pms | booking_engine | manual | api | import
  "attribution_source": null,      // Marketing attribution: e.g. "google_ads", "meta_ads", "booking_com" (null if not supplied)
  "status": "Confirmed",           // Confirmed | Modified | CheckedIn | CheckedOut | Cancelled | NoShow
  "check_in": "2026-03-15",        // Check-in date
  "check_out": "2026-03-18",       // Check-out date
  "nights": 3,                     // Calculated nights
  "room_type": "Deluxe Suite",
  "room_number": "405",
  "board_type": "BB",
  "rate_plan": "Best Available",
  "total_value": 450.00,
  "currency": "EUR",
  "adults": 2,
  "children": 0,
  "notes": "Late checkout requested",
  "created_at": "2024-03-01T10:00:00Z",
  "updated_at": "2024-03-01T10:00:00Z"
}
Guest Stage Lifecycle
Automatic stage tracking based on reservation dates
1
unknown

No booking data

2
pre_stay

Before check-in

3
during_stay

At hotel

4
post_stay

After checkout

Guest stages are automatically updated by a daily cron job and when booking events are received. The AI assistant uses the guest stage to personalize conversations.

B2B CRM

Ingest travel agencies, tour operators and corporate accounts as first-class B2B entities. Accounts support parent/child hierarchy (up to 3 levels), commercial terms (commissions, credit, promo codes) and matching identifiers that power automatic reservation-to-account production attribution. B2B contacts are regular guest contacts flagged with contact_type: "b2b" and linked to accounts.

Scopes & Module Gating
b2b:read
Required for GET endpoints
b2b:write
Required for POST / PATCH endpoints

All /api/v1/b2b/* endpoints require the B2B CRM module to be enabled for your tenant. Requests without it return 403 MODULE_NOT_ENABLED. Contact your account manager to activate the module.

POST
/api/v1/b2b/accounts
Create a new B2B account or update an existing one. Upsert resolution order: external_id → tax_id → iata_code → insert new.

Required Scope

b2b:write

Request Body

ParameterTypeRequiredDescription
legal_namestring
Required
Legal company name (max 200 chars)
trade_namestringOptionalCommercial / brand name
tax_idstringOptionalCIF/NIF or international Tax ID. Unique per tenant (case-insensitive) — used for upsert matching and parent_account references
iata_codestringOptionalIATA agency code — also a matching identifier for reservation attribution
sectorstringOptionalIndustry sector (free text)
profilestringOptionalcorporate or mice (default: corporate). Drives which deal flow and account blocks apply
account_typestringOptionaltravel_agency, tour_operator, dmc, incentive_agency, corporate_booking, corporate, event_organizer, other
sourcestringOptionalWhere the account came from (free text)
websitestringOptionalCompany website URL
email_domainsstring[]OptionalUp to 20 domains (e.g. @acme.com). Reservation holder emails on these domains generate attribution suggestions
billing_addressobjectOptionalFiscal address (see Billing Address Object below)
billing_detailsobjectOptionalKey-value string map (e.g. invoice_email, VAT notes)
parent_accountobjectOptionalReference to an existing parent account (see Account Reference Object below). Must resolve (422 PARENT_NOT_FOUND); hierarchy max 3 levels (422 HIERARCHY_TOO_DEEP)
statusstringOptionalprospect, active, inactive (default: active)
promo_codesstring[]OptionalUp to 50 promo/rate codes (e.g. CORP_ACME2026). Matched against reservation rate codes for attribution
commitment_room_nightsnumberOptionalAnnual contracted room-night commitment
contract_start_datestringOptionalContract start (YYYY-MM-DD)
contract_end_datestringOptionalContract end (YYYY-MM-DD) — drives renewal alerts
payment_methodstringOptionalcredit or direct
credit_limitnumberOptionalCredit limit amount
payment_daysnumberOptionalPayment terms in days (0–365)
cancellation_policystringOptionalAgreed cancellation policy (max 2000 chars)
commission_ratenumberOptionalCommission percentage on lodging (0–100). Child accounts inherit from the parent when unset
commission_settlementstringOptionaldeducted_invoice or post_checkout
external_idstringOptionalYour PMS/CRM identifier. Unique per tenant — the primary upsert key when provided
channel_manager_codestringOptionalChannel manager agency code (matching identifier)
crs_codestringOptionalCRS agency code (matching identifier)
mirai_agency_idstringOptionalMirai Pro agency id (matching identifier)
custom_fieldsobjectOptionalFree-form key-value pairs

Billing Address Object

ParameterTypeRequiredDescription
line1stringOptionalAddress line 1
line2stringOptionalAddress line 2
citystringOptionalCity
regionstringOptionalRegion / state / province
postal_codestringOptionalPostal code
countrystringOptionalISO 3166-1 alpha-2 uppercase (e.g. ES, US)

Account Reference Object

Used by parent_account here, by account on deals, and by b2b_account on guest ingest. At least one identifier is required. Resolution order: id → external_id → tax_id → iata_code.

ParameterTypeRequiredDescription
idstringOptionalGuestMaker account UUID
external_idstringOptionalYour PMS/CRM identifier
tax_idstringOptionalTax ID (case-insensitive match)
iata_codestringOptionalIATA agency code

Example Request

curl -X POST https://guestmaker.ai/api/v1/b2b/accounts \
  -H "Authorization: Bearer gmkr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "legal_name": "Viajes Mediterraneo S.L.",
    "trade_name": "Viajes Mediterraneo",
    "tax_id": "B12345678",
    "iata_code": "78212345",
    "profile": "corporate",
    "account_type": "travel_agency",
    "email_domains": ["@viajesmediterraneo.com"],
    "billing_address": {
      "line1": "Calle Gran Via 28",
      "city": "Madrid",
      "postal_code": "28013",
      "country": "ES"
    },
    "commission_rate": 10,
    "commission_settlement": "post_checkout",
    "payment_method": "credit",
    "credit_limit": 50000,
    "payment_days": 30,
    "promo_codes": ["AGMED2026"],
    "commitment_room_nights": 1500,
    "contract_start_date": "2026-01-01",
    "contract_end_date": "2026-12-31",
    "parent_account": { "tax_id": "B87654321" },
    "external_id": "PMS-AG-0042"
  }'

Response

201 when a new account was created, 200 when an existing one was updated.

{
  "success": true,
  "data": {
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "is_new": true,
    "message": "Account created successfully"
  }
}
GET
/api/v1/b2b/accounts
List B2B accounts, ordered by most recently updated. Supports incremental sync via updated_since.
PATCH
/api/v1/b2b/accounts/{id}
Partially update an account by UUID — only the fields you send are written. A GET on the same path returns the full account.
POST
/api/v1/b2b/accounts/batch
Create or update up to 500 accounts in a single request. Partial success model — each account is processed independently.
POST
/api/v1/b2b/deals
Create a deal (opportunity) for an existing account. The deal lands in the default pipeline of its flow type, in the first qualification stage.
POST
/api/v1/guests
B2B contacts: the existing guest endpoint accepts contact_type and a b2b_account link — no separate contacts endpoint needed.
Identification & Attribution
How reservations are automatically linked to B2B accounts for production reporting

Every reservation you ingest is matched against your accounts' identifiers in strict priority order. The first match wins; manual links made in the dashboard are never overwritten.

1

Agency code

The reservation's agency/channel code matches an account's iata_code, channel_manager_code, crs_code or external_id — auto-linked.

2

Promo / rate code

The stay's rate code matches one of the account's promo_codes (or an open corporate deal's promo code) — auto-linked.

3

Mirai agency ID

The booking source's Mirai agency identifier matches mirai_agency_id — auto-linked.

4

Email domain (suggestion only)

The reservation holder's email domain appears in an account's email_domains — surfaced as a suggestion for a human to confirm, never auto-linked.

The more identifiers you send on your accounts (IATA, channel manager codes, promo codes, email domains), the higher your automatic attribution coverage.

B2B Error Codes
400 Bad Request

Invalid request body or query parameters

VALIDATION_ERROR
401 Unauthorized

Invalid or missing API key

UNAUTHORIZED
403 Forbidden

The B2B CRM module is not enabled for this tenant (also FORBIDDEN when the key lacks the b2b scope)

MODULE_NOT_ENABLED
404 Not Found

Account id does not exist for this tenant

NOT_FOUND
422 Unprocessable

parent_account reference does not resolve to an existing account

PARENT_NOT_FOUND
422 Unprocessable

Linking the parent would exceed the 3-level account hierarchy

HIERARCHY_TOO_DEEP
422 Unprocessable

Linking the parent would create a cycle in the hierarchy

HIERARCHY_CYCLE
422 Unprocessable

Deals: the account reference does not resolve to an existing account

ACCOUNT_NOT_FOUND
422 Unprocessable

Deals: no pipeline exists for the flow type (module defaults not seeded)

NO_PIPELINE
429 Too Many Requests

Rate limit exceeded

RATE_LIMITED

Error Handling

Error Response Format
{
  "success": false,
  "error": {
    "message": "Validation failed",
    "code": "VALIDATION_ERROR",
    "details": [
      { "field": "phone", "message": "Phone number is required" },
      { "field": "first_name", "message": "First name is required" }
    ]
  }
}
HTTP Status Codes
200 OK

Request successful

201 Created

Resource created successfully

202 Accepted

Request accepted for async processing

400 Bad Request

Invalid request parameters

VALIDATION_ERROR
401 Unauthorized

Invalid or missing API key

UNAUTHORIZED
403 Forbidden

Insufficient permissions/scopes

FORBIDDEN
404 Not Found

Resource not found

NOT_FOUND
409 Conflict

Resource already exists

CONFLICT
422 Unprocessable

Request understood but cannot be processed

UNPROCESSABLE
429 Too Many Requests

Rate limit exceeded

RATE_LIMITED
500 Internal Error

Server error

INTERNAL_ERROR
503 Service Unavailable

Service temporarily unavailable

SERVICE_UNAVAILABLE

Rate Limits

API requests are rate limited per API key. The default limit is 1,000 requests per minute.

Rate Limit Headers

ParameterTypeRequiredDescription
X-RateLimit-Limitnumber
Required
Maximum requests per minute
X-RateLimit-Remainingnumber
Required
Requests remaining in current window
X-RateLimit-Resetnumber
Required
Unix timestamp when the limit resets

Rate Limit Response

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710507600
Retry-After: 45

{
  "success": false,
  "error": {
    "message": "Rate limit exceeded. Please retry after 45 seconds.",
    "code": "RATE_LIMITED"
  }
}

Need higher limits?

Contact us to discuss higher rate limits for your integration. Enterprise plans include custom limits up to 10,000 requests/minute.

Best Practices

Idempotency

Always include an idempotency_key in event requests to prevent duplicate processing if you need to retry a failed request.

  • Use a unique, deterministic key (e.g., checkin-{booking_id})
  • Keys are valid for 24 hours
  • Same key returns the original response without reprocessing
Error Handling

Implement robust error handling for a reliable integration:

  • Retry 5xx errors with exponential backoff (2s, 4s, 8s, 16s)
  • Do not retry 4xx errors - fix the request first
  • Log error responses for debugging
  • Monitor rate limit headers to avoid hitting limits
  • Set reasonable timeouts (30s recommended)
Webhook Reliability

Ensure your webhook endpoints are reliable:

  • Return 200 OK quickly (within 5 seconds)
  • Process webhooks asynchronously if needed
  • Always verify the signature before processing
  • Implement idempotent processing (webhooks may be delivered more than once)
  • Use the X-Webhook-Delivery header for deduplication
Phone Number Formatting

Always use E.164 format for phone numbers:

  • Start with + and country code (e.g., +1 for US, +34 for Spain)
  • No spaces, dashes, or parentheses
  • URL-encode the + as %2B in query parameters
  • Example: +34612345678 (Spain)
Testing

Use our sandbox for development and testing:

  • Use test API keys (gmkr_test_) for sandbox
  • Test all error scenarios (validation, auth, rate limits)
  • Use the interactive sandbox at /developers/sandbox
  • Test webhook signature verification locally
Bulk Ingestion Guide
Recommended patterns for ingesting large volumes of contacts and bookings

Recommended Integration Flow

  1. Register custom field definitions via POST /api/v1/fields (once)
  2. Send guest + booking data via POST /api/v1/guests
  3. Trigger lifecycle events via POST /api/v1/events

Concurrency & Throughput

  • Send up to 10 concurrent requests for optimal throughput
  • Default limit: 1,000 req/min (contact us for higher limits)
  • Monitor X-RateLimit-Remaining to stay within limits
  • Implement exponential backoff on 429 responses (2s, 4s, 8s, 16s)

Deduplication & Idempotency

  • Guest endpoint uses phone-based dedup: sending the same phone number updates the existing contact
  • Set update_if_exists: true (default) for safe upserts
  • Reservation dedup uses booking_id + hotel_code as the unique key
  • Event endpoint uses idempotency_key for dedup (24-hour validity)

Performance Tips

  • Use hotel_code instead of hotel_name for faster hotel lookups
  • Register custom field definitions before sending guest data
  • Include custom_fields in the guest request to populate both JSONB storage and the segment builder
  • Booking data automatically triggers guest stage calculation and journey events

Initial Data Migration

For initial migrations of 100K+ contacts, contact our team to temporarily increase your rate limits and get a dedicated ingestion window. We can monitor the process in real-time to ensure data integrity.

Ready to integrate?

Request API access to get your credentials and start building.