Build with GuestMaker
API Documentation
Authenticate your integration, sync guest and reservation data, and receive updates through webhooks.
On this page
Quick Start
Create an API key, choose your hotel, and send your first request.
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
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://www.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
}
]
}'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"
}
}Set up webhooks (optional)
Configure webhook endpoints in your dashboard to receive real-time notifications when guests reply or journeys complete.
Explore the full API
Continue reading below to learn about events, webhooks, error handling, and best practices for production integrations.
Authentication
Core v1 API requests require authentication using a Bearer token in the Authorization header.
Authorization: Bearer gmkr_your_api_keyAPI Key Format
Core API keys are prefixed with gmkr_. Your API key is provided when you set up your integration with us.
Available Scopes
| Scope | Description |
|---|---|
guests:read | Read guest/contact information |
guests:write | Create/update guests with bookings |
events:write | Send events to trigger journeys |
bookings:read | Read reservation data |
reservations:write | Create/update reservations via API |
bookings:write | Create/update spa, restaurant and other venue appointments |
conversions:write | Report booking conversions for campaign attribution |
custom_fields:write | Register and read custom field definitions |
webhooks:manage | Configure outbound webhooks |
cdp:read | Read CDP profiles and reservation data |
cdp:write | Ingest reservations into the Customer Data Platform |
b2b:read | Read B2B accounts and deals |
b2b:write | Create/update B2B accounts, contacts and deals |
surveys:write | Forward legacy Hotelinking survey answers into Surveys |
* | Full access (all scopes) |
Security Best Practices
- Never expose a secret API key in client-side code. The exception is a public browser key (
gme_pk_…) for website events: it is built to sit in a page and only works from the websites you allow. - 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.
Required Scope
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
reservation_id | string | Required | Your PMS unique reservation ID (used for upsert matching) |
confirmation_number | string | Optional | Booking confirmation/localizer code (can be shared across reservations) |
booking_id | string | Optional | Parent booking ID (for grouping multiple reservations) |
hotel_id | string | Optional | Hotel UUID (one of hotel_id, hotel_code, or hotel_name required) |
hotel_code | string | Optional | Hotel code (alternative to hotel_id) |
hotel_name | string | Optional | Hotel name (alternative to hotel_id) |
check_in | string | Required | Check-in date (YYYY-MM-DD) |
check_out | string | Required | Check-out date (YYYY-MM-DD) |
booking_date | string | Optional | Date the booking was made (YYYY-MM-DD) |
status | string | Optional | Reservation status: confirmed, pending (booking engine awaiting payment confirmation), modified, cancelled, no_show, checked_in, checked_out |
channel | string | Optional | Booking channel (e.g., booking.com, expedia, direct) |
source | string | Optional | System type that sent this data: pms, booking_engine, crm, channel_manager, website, etc. Defaults to "api" |
company | string | Optional | Corporate booking company |
amount | number | Optional | Total reservation amount (gross) |
amount_net | number | Optional | Total reservation amount (net, before tax) |
currency | string | Optional | ISO 4217 currency code (e.g., EUR, USD) |
market_segment | string | Optional | PMS market segment code (e.g., "LEISURE", "CORPORATE") |
agency | string | Optional | Selling agency / tour operator / OTA name (e.g. "BOOKING.COM", "EASYJET HOLIDAYS"). Max 200 chars |
agency_code | string | Optional | Travel agency code from the PMS, where the source sends one. Max 100 chars |
nights | number | Optional | Number of nights (accepted but not persisted: auto-computed from dates) |
external_created_at | string | Optional | PMS creation timestamp (ISO 8601 with timezone) |
external_updated_at | string | Optional | PMS last-updated timestamp (ISO 8601 with timezone) |
guests | array | Required | Array of guests (1-20). See Guest Object below. |
stays | array | Optional | Array of room stays (max 10). See Stay Object below. |
extras | array | Optional | Array of extras/charges (max 100). See Extra Object below. |
comments | string | Optional | Internal notes or special requests |
metadata | object | Optional | Custom key-value data |
Guest Object
| Parameter | Type | Required | Description |
|---|---|---|---|
first_name | string | Required | Guest first name |
last_name | string | Optional | Guest last name |
email | string | Optional | Guest email address. At least one of email or phone is required to create/match a contact. |
phone | string | Optional | Phone number in E.164 format. At least one of email or phone is required to create/match a contact. |
is_holder | boolean | Optional | Whether this is the primary guest/booker |
pax_type | string | Optional | Passenger type: adult, child, infant |
date_of_birth | string | Optional | Date of birth (YYYY-MM-DD) |
gender | string | Optional | Guest gender: male, female, other, prefer_not_to_say |
nationality | string | Optional | ISO 3166-1 alpha-2 country code |
document_type | string | Optional | ID document type: passport, national_id, drivers_license, other |
document_number | string | Optional | ID document number (max 50 characters) |
address | object | Optional | Guest'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_consent | boolean | Optional | PMS-reported email marketing consent. Takes precedence over CDP auto-consent settings. |
whatsapp_marketing_consent | boolean | Optional | PMS-reported WhatsApp/phone marketing consent. Takes precedence over CDP auto-consent settings. |
language | string | Optional | Guest language code (e.g., "en", "es", "fr"). Defaults to "en" for new contacts. |
pre_checkin_completed_at | string | Optional | ISO 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_source | string | Optional | Where the pre-checkin was completed (e.g., "mews", "cloudbeds", "apaleo", "self_service"). Max 50 chars. |
Stay Object
| Parameter | Type | Required | Description |
|---|---|---|---|
start_date | string | Required | Stay start date (YYYY-MM-DD) |
end_date | string | Required | Stay end date (YYYY-MM-DD) |
room_type | string | Optional | Room type/category |
room_number | string | Optional | Assigned room number |
board_type | string | Optional | Meal plan: RO (Room Only), BB (Bed & Breakfast), HB (Half Board), FB (Full Board), AI (All Inclusive) |
rate_code | string | Optional | Rate code/plan |
adults | number | Optional | Number of adults |
children | number | Optional | Number of children |
babies | number | Optional | Number of infants |
Extra Object
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Required | Charge description |
amount | number | Required | Charge amount |
code | string | Optional | Charge code from PMS |
quantity | number | Optional | Quantity (default: 1) |
date | string | Optional | Charge date (YYYY-MM-DD) |
notes | string | Optional | Additional notes |
Example Request
curl -X POST https://www.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. Sync from your PMS without tracking which reservations already exist.
| Status | Description | Guest Stage |
|---|---|---|
confirmed | Reservation confirmed | Pre-arrival |
pending | Booking engine awaiting payment confirmation | Pre-arrival |
modified | Reservation modified after confirmation | Pre-arrival |
checked_in | Guest has arrived | In-house |
checked_out | Guest has departed | Post-stay |
cancelled | Reservation cancelled | Not applicable |
no_show | Guest did not arrive | Not applicable |
Guests API
Create and manage guest contacts with their booking information. Guests are automatically linked to hotels for AI-powered conversations and journey automation.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | Required | Phone number in E.164 format (e.g., +1234567890) |
first_name | string | Required | Guest's first name |
last_name | string | Required | Guest's last name |
email | string | Optional | Guest's email address |
language | string | Optional | Preferred language code (e.g., en, es, fr). Default: en |
tags | string[] | Optional | Tags for segmentation. ADDED to the guest's existing tags, never replacing them (tags set by the hotel or other integrations are kept). |
tags_remove | string[] | Optional | Tags to remove from the guest, e.g. a status tag your system no longer applies. Only the tags listed here are removed. |
custom_fields | object | Optional | Key-value pairs matching your registered custom field definitions. See Custom Fields API for setup. |
hotel_name | string | Optional | Hotel name to link the guest to |
hotel_code | string | Optional | Hotel code (alternative to hotel_name) |
source | string | Optional | System type that sent this data: pms, booking_engine, crm, channel_manager, website, etc. Defaults to "api" |
booking | object | Optional | Booking/reservation details (see below) |
trigger_event | boolean | Optional | Trigger booking.created event (default: true) |
update_if_exists | boolean | Optional | Update existing contact by phone (default: true) |
opt_in_status | string | Optional | Legacy field (deprecated). Use whatsapp_marketing_consent instead. |
whatsapp_marketing_consent | boolean | Optional | WhatsApp marketing consent (default: false). When true, guest receives marketing WhatsApp messages. |
email_consent | boolean | Optional | Email marketing consent (default: false). When true, guest receives marketing emails. |
date_of_birth | string | Optional | Guest date of birth (YYYY-MM-DD) |
gender | string | Optional | Guest gender: male, female, other, prefer_not_to_say |
nationality | string | Optional | ISO 3166-1 alpha-2 country code (e.g., US, ES, GB) |
document_type | string | Optional | ID document type: passport, national_id, drivers_license, other |
document_number | string | Optional | ID document number (max 50 characters) |
address | object | Optional | Guest'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_status | string | Optional | Communication subscription status: active, unsubscribed |
communication_preferences | object | Optional | Granular communication preferences (see below) |
consent_source | string | Optional | Your identifier for where consent was collected |
consent_timestamp | string | Optional | ISO 8601 timestamp when consent was given |
Booking Object
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_id | string | Required | Parent booking ID (groups multiple reservations, e.g., Expedia booking with 4 rooms) |
reservation_id | string | Optional | Individual reservation/localizer code (defaults to booking_id if not provided) |
check_in | string | Required | Check-in date (YYYY-MM-DD) |
check_out | string | Required | Check-out date (YYYY-MM-DD) |
room_type | string | Optional | Room type/category |
room_number | string | Optional | Assigned room number (if known) |
rate_plan | string | Optional | Rate plan name |
board_type | string | Optional | Meal plan: RO (Room Only), BB (Bed & Breakfast), HB (Half Board), FB (Full Board), AI (All Inclusive) |
booking_channel | string | Optional | Raw 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_amount | number | Optional | Total booking amount |
currency | string | Optional | Currency code (default: EUR) |
status | string | Optional | Confirmed, Modified, CheckedIn, CheckedOut, Cancelled, NoShow |
guests | array | Optional | Additional guests on the booking (see Booking Guests below) |
extras | array | Optional | Booking extras like spa, minibar, restaurant (see Booking Extras below) |
Booking Guests Array
| Parameter | Type | Required | Description |
|---|---|---|---|
first_name | string | Required | Guest's first name |
last_name | string | Optional | Guest's last name |
email | string | Optional | Guest's email address |
is_holder | boolean | Optional | Whether this guest is the reservation holder (default: false) |
pax_type | string | Optional | Adult, Child, or Infant |
relationship_to_holder | string | Optional | Spouse, Child, Colleague, Friend, etc. |
pre_checkin_completed_at | string | Optional | ISO 8601 timestamp (with offset) when this companion guest completed online/pre-checkin. |
pre_checkin_source | string | Optional | Where the pre-checkin was completed (e.g., "mews", "cloudbeds", "self_service"). Max 50 chars. |
Booking Extras Array
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Required | Extra name (e.g., Spa Services, Minibar, Restaurant) |
amount | number | Required | Amount charged |
quantity | number | Optional | Quantity (default: 1) |
notes | string | Optional | Additional 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
marketing | boolean | Optional | Consent to receive marketing messages (promotions, offers). Default: false. Maps to whatsapp_marketing_consent. |
utility | boolean | Optional | Utility 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://www.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"
}
}Events API
Send events to trigger journey automations. Events are matched against active journey triggers to start automated WhatsApp conversations.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
event_type | string | Required | Event type identifier (e.g., guest.checked_in) |
payload | object | Required | Event data available in journey context |
contact_id | string | Optional | Contact UUID (if known) |
guest_phone | string | Optional | Phone number to identify the contact (E.164) |
hotel_id | string | Optional | Hotel UUID (if known) |
hotel_name | string | Optional | Hotel name to resolve hotel_id |
hotel_code | string | Optional | Hotel code to resolve hotel_id |
idempotency_key | string | Optional | Unique key to prevent duplicate events |
source | string | Optional | Source system identifier for tracking |
Standard Event Types
These standard event types are recognized by the system. You can also create custom event types.
booking.createdNew booking received
booking.updatedBooking modified
booking.cancelledBooking cancelled
guest.checked_inGuest arrived
guest.checked_outGuest departed
guest.messageGuest sent message
payment.receivedPayment confirmed
review.requestedReview requested
Custom event types should follow the pattern category.action (e.g., spa.appointment_booked)
Example Request
curl -X POST https://www.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.
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.
Required Scope
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
fields | array | Required | Array of field definitions (max 50) |
fields[].field_key | string | Required | Unique key (lowercase, a-z, 0-9, underscores, must start with letter) |
fields[].field_label | string | Required | Display label shown to hotel staff |
fields[].field_type | string | Required | One of: string, text, number, boolean, date, datetime, select, multiselect |
fields[].options | string[] | Optional | Required for select/multiselect types |
fields[].description | string | Optional | Help text for hotel staff |
fields[].is_required | boolean | Optional | Whether field must have a value (default: false) |
fields[].is_visible | boolean | Optional | Show in contact details UI (default: true) |
fields[].display_order | number | Optional | Sort order within your fields (default: 0) |
Example Request
curl -X POST https://www.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"
}
}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.
| Type | Description | Example Value |
|---|---|---|
string | Short text (single line) | "John Doe" |
text | Long text (multi-line) | "Special dietary requirements..." |
number | Numeric value | 1500 |
boolean | True/false | true |
date | Date (YYYY-MM-DD) | "2026-03-15" |
datetime | Date and time (ISO 8601) | "2026-03-15T14:30:00Z" |
select | Single choice from options | "Gold" |
multiselect | Multiple choices from options | ["spa", "golf"] |
CDP API
The Customer Data Platform API enables anonymous visitor tracking and identity resolution on your hotel websites: record what visitors do and link them to the guests you already know. The reservation and guest intake endpoints further down are retired (they answer 410 Gone); send bookings and guests through the standard Reservations and Contacts endpoints.
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.
Required Scope
Requires visitor tracking to be enabled in CDP Settings.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
visitor_id | string | Required | Anonymous visitor identifier (from SDK cookie) |
session_id | string | Required | Session identifier (from SDK sessionStorage) |
device | object | Optional | { type: "desktop"|"mobile"|"tablet", browser, os, language } |
utm | object | Optional | { source, medium, campaign, term, content } |
referrer | string | Optional | HTTP referrer URL |
events | array | Required | Array of event objects (1-100). See Event Object below. |
Event Object
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Required | Event type: page_view, scroll, click, form_start, form_submit, identify, custom |
url | string | Optional | Page URL (domain stripped server-side for privacy) |
title | string | Optional | Page title |
data | object | Optional | Custom event data (e.g., scroll depth, click element) |
ts | string | Required | ISO 8601 timestamp |
Example Request
curl -X POST https://www.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
}
}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 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.
Authorization: Bearer <api_key>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.
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.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | 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
}
}guests:writePOST /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 balancePOST /api/v1/loyalty/points/void: void a pending earn before confirmationPOST /api/v1/loyalty/reverse: reverse earned points on cancellation / no-showPOST /api/v1/loyalty/apply-discount: spend points as a currency discount on a booking/POS.reference_idis metadata, not a retry key; resubmitting can debit points againPOST /api/v1/loyalty/redeem-free-night: redeem points for a free-night voucher code.engine_apifulfillment is not implemented and returns 501
GET /api/v1/loyalty/conversion: points ↔ currency calculatorguests:readGET /api/v1/loyalty/households·POST: household pool balance & pooled redemptionGET /api/v1/loyalty/liability: points-liability & program-health reportguests:readPOST /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, conversion, cash-credit quote and reservation snapshot) require the guests:read scope; write endpoints (enroll, points, redeem and cash-credit hold, confirm, release and reverse) require guests:write. All endpoints share the standard v1 transport limit of 2,000 requests/minute, on top of your key's own limit (1,000/minute by default).
Which bookings count towards a tier
A member’s nights, stays and spend are measured on the reservations linked to them with status Confirmed, CheckedIn or CheckedOut, once the stay has begun. By default every booking channel counts. A hotel group can limit tiers to some channels (for example direct bookings only) in Loyalty → Program → Tiers → Qualification → Only count some booking channels.
The channel of a reservation
Each reservation carries a channel type, one of direct, ota, tour_operator, wholesaler, metasearch, agency, corporate, walkin, affiliate, gds. When you send a reservation, put the PMS or booking-engine channel code in channel. GuestMaker resolves the type from the hotel’s own channel mappings (Settings → Booking Channels), then from a built-in classifier for well-known channels. If neither recognises the code, the reservation has no channel type. An update that arrives with a channel that resolves to no type keeps an existing ota or tour_operator type instead of erasing it.
What a limit does
- Only reservations whose channel type is in the list count. A reservation with no channel type does not count while a limit is on, because it cannot be shown to be direct. If a program limits tiers to
direct, send the channel consistently so direct reservations resolve to it, and ask the hotel to map any code that does not. - Nothing changes for a program that never sets a limit, and the list of bookings a member sees in the portal is a separate display setting that never changes what counts.
- It applies to tier qualification only. Points are earned as before, on every channel.
- Saving the setting changes nobody’s level at that moment. Levels are re-evaluated by the scheduled level check; a member who only qualified through other channels can move down then, unless the program does not allow downgrades or status protection is keeping their level.
Where you see it
GET /api/v1/loyalty/tiers:tiers[].qualification.qualifying_channels(null= every channel) and aqualification.textthat ends with, for example, “Direct bookings only”.GET /api/v1/loyalty/member:next_tier.requirementuses the same sentence, andnext_tier.progress_percentis measured on the counting reservations only.- Loyalty SSO: for a program without points, the
loyalty_stays,loyalty_nightsandloyalty_total_spendclaims, andloyalty_next_tier_requirement, follow the same limit.
Cash Credit
Let members spend their points as money credit on direct bookings, in-stay folio charges, and at checkout. Quote → Hold → Confirm/Release/Expire lifecycle: see the deep-dive pages for the full integration walkthrough.
Lifecycle diagram, surfaces, error codes, and the OTA denylist.
5-minute drop-in <script> embed with apply callback.
/reservation-snapshot + 5 HMAC webhooks for real-time balance sync.
| Endpoint | Scope | Behavior |
|---|---|---|
GET /api/v1/loyalty/credit/quote | guests:read | Read spendable balance and presets |
POST /api/v1/loyalty/credit/hold | guests:write | Reserve points atomically; external_reference_id identifies retries |
POST /api/v1/loyalty/credit/confirm | guests:write | Commit a hold's points deduction |
POST /api/v1/loyalty/credit/release | guests:write | Release a hold; released: true does not prove an active hold was found |
POST /api/v1/loyalty/credit/reverse | guests:write | Return confirmed points after cancellation; reconcile before retrying |
GET /api/v1/loyalty/reservation-snapshot | guests:read | Read the reservation's member balance and credit history |
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.
The flow, scopes & claims, mandatory security requirements, token model, and endpoints.
A copy-paste, framework-neutral walkthrough: authorize URL, PKCE, token exchange, id_token verification, and reading member data.
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 keysGET /api/loyalty/me/balance: member tier + points (Bearer)GET /api/loyalty/me/transactions: points history (Bearer)
Webhooks
Receive real-time notifications when events occur in the platform. Configure webhook endpoints in your dashboard settings.
message.receivedIncoming messagemessage.sentOutgoing message sentmessage.deliveredMessage deliveredmessage.readMessage read by recipientcontact.createdNew contact addedcontact.updatedContact info changedcontact.deletedContact deletedconversation.createdNew conversation startedconversation.closedConversation closedjourney.startedJourney automation beganjourney.completedJourney automation finishedjourney.failedJourney execution errorbroadcast.sentBroadcast campaign sentbroadcast.completedBroadcast finishedreservation.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).
{
"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"
}
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
X-Webhook-Signature | string | Required | HMAC-SHA256 signature: sha256=... |
X-Webhook-Event | string | Required | The event type being delivered |
X-Webhook-Delivery | string | Required | Unique delivery ID (UUID) |
X-Webhook-Timestamp | string | Required | ISO 8601 timestamp of the event |
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');
const wanted = Buffer.from(`sha256=${expected}`);
const actual = Buffer.from(signature || '');
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
// 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');
}
// Retries retain the original event timestamp.
// Use stable payload identifiers for idempotent processing.
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, or another supported provider.
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.
call_ringingCall ringing: triggers contact lookup + screen popcall_answeredCall picked up by an agentcall_hangupCall ended (carries duration in seconds)call_missedCall not answeredrecord_availableRecording URL readycall_completedOne post-call record (metadata + optional transcript/summary): see Completed-Call Push belowPOST /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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
event | string | Required | The literal "call_completed". |
call_id | string | Required | Your stable unique id: the sole idempotency key, scoped per (tenant, provider). |
direction | string | Required | inbound or outbound. Omit it entirely to send a content patch (see below). |
from / to | string | Required | E.164 numbers. The guest side (from on inbound, to on outbound) resolves to or creates a contact. |
started_at / ended_at | string | Required | ISO 8601. A full record missing either is dropped; a content patch does not need them. |
answered_at | string | Optional | ISO 8601. Absent or null means the call was never answered. |
duration_seconds | number | Optional | Derived from ended_at − (answered_at or started_at) when omitted. |
agent | object | Optional | { 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. |
caller | object | Optional | { name | first_name/last_name, email }: the guest's known details. Fills blank contact fields only; never overwrites. |
outcome | string | Optional | completed | missed | no_answer | voicemail | free text. missed, no_answer and voicemail store the call as missed. |
recording_url | string | Optional | Stored verbatim and shown as a play link on the contact. The audio stays on your servers. |
transcript | array | Optional | [{ speaker, text, offset_ms? }]: feeds guest-memory extraction. Capped at 500 turns / 200,000 characters; oversized pushes are truncated, not rejected. |
transcript_text | string | Optional | Plain-text alternative. Guest:/Agent: prefixed lines are split into turns when at least two are detected. |
summary | string | Optional | Your AI summary: shown italicised under the call in the contact's history. |
language, metadata | string, object | Optional | Merged 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).
<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)
| Parameter | Type | Required | Description |
|---|---|---|---|
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 } | Optional | Free text, max 200 chars. Opens the contacts list with the search prefilled. Returns no records. |
openCreateContact({ phone }) | { shown } | Optional | Opens the create-contact form prefilled. A human saves it: the call writes nothing. |
Events (GuestMaker → panel)
| Parameter | Type | Required | Description |
|---|---|---|---|
clickToCall | { phone, contactId? } | Optional | An agent clicked a phone number anywhere in the dashboard. |
panelModeChanged | { mode } | Optional | The agent collapsed or restored the panel from our chrome, so yours can stay in sync. |
Error codes
| Parameter | Type | Required | Description |
|---|---|---|---|
UNKNOWN_METHOD | host | Optional | Not a method in this protocol version. |
INVALID_PARAMS | host | Optional | Failed validation; the message names the offending field. |
NOT_FOUND | host | Optional | Well-formed request, nothing matched. |
NOT_PERMITTED | host | Optional | Origin or tenant not allowed to make that call. |
TIMEOUT | sdk | Optional | No 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
auto_navigate | boolean | Optional | Default true. Current tab navigates to /contacts/{id} on ring. |
notification | boolean | Optional | Default true. Slide-in banner with caller name + Open Contact button. |
new_tab | boolean | Optional | Default false. Opens /contacts/{id} in a background tab. |
widget_position | string | Optional | Provider SDK widget anchor: 'bottom-right' (default) or 'bottom-left'. |
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.
{
"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"
}{
"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"
}unknownNo booking data
pre_stayBefore check-in
during_stayAt hotel
post_stayAfter 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.
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.
Required Scope
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
legal_name | string | Required | Legal company name (max 200 chars) |
trade_name | string | Optional | Commercial / brand name |
tax_id | string | Optional | CIF/NIF or international Tax ID. Unique per tenant (case-insensitive): used for upsert matching and parent_account references |
iata_code | string | Optional | IATA agency code: also a matching identifier for reservation attribution |
sector | string | Optional | Industry sector (free text) |
profile | string | Optional | corporate or mice (default: corporate). Drives which deal flow and account blocks apply |
account_type | string | Optional | travel_agency, tour_operator, dmc, incentive_agency, corporate_booking, corporate, event_organizer, other |
source | string | Optional | Where the account came from (free text) |
website | string | Optional | Company website URL |
email_domains | string[] | Optional | Up to 20 domains (e.g. @acme.com). Reservation holder emails on these domains generate attribution suggestions |
billing_address | object | Optional | Fiscal address (see Billing Address Object below) |
billing_details | object | Optional | Key-value string map (e.g. invoice_email, VAT notes) |
parent_account | object | Optional | Reference to an existing parent account (see Account Reference Object below). Must resolve (422 PARENT_NOT_FOUND); hierarchy max 3 levels (422 HIERARCHY_TOO_DEEP) |
status | string | Optional | prospect, active, inactive (default: active) |
promo_codes | string[] | Optional | Up to 50 promo/rate codes (e.g. CORP_ACME2026). Matched against reservation rate codes for attribution |
commitment_room_nights | number | Optional | Annual contracted room-night commitment |
contract_start_date | string | Optional | Contract start (YYYY-MM-DD) |
contract_end_date | string | Optional | Contract end (YYYY-MM-DD): drives renewal alerts |
payment_method | string | Optional | credit or direct |
credit_limit | number | Optional | Credit limit amount |
payment_days | number | Optional | Payment terms in days (0–365) |
cancellation_policy | string | Optional | Agreed cancellation policy (max 2000 chars) |
commission_rate | number | Optional | Commission percentage on lodging (0–100). Child accounts inherit from the parent when unset |
commission_settlement | string | Optional | deducted_invoice or post_checkout |
external_id | string | Optional | Your PMS/CRM identifier. Unique per tenant: the primary upsert key when provided |
channel_manager_code | string | Optional | Channel manager agency code (matching identifier) |
crs_code | string | Optional | CRS agency code (matching identifier) |
mirai_agency_id | string | Optional | Mirai Pro agency id (matching identifier) |
custom_fields | object | Optional | Free-form key-value pairs |
Billing Address Object
| Parameter | Type | Required | Description |
|---|---|---|---|
line1 | string | Optional | Address line 1 |
line2 | string | Optional | Address line 2 |
city | string | Optional | City |
region | string | Optional | Region / state / province |
postal_code | string | Optional | Postal code |
country | string | Optional | ISO 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Optional | GuestMaker account UUID |
external_id | string | Optional | Your PMS/CRM identifier |
tax_id | string | Optional | Tax ID (case-insensitive match) |
iata_code | string | Optional | IATA agency code |
Example Request
curl -X POST https://www.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"
}
}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.
Agency code
The reservation's agency/channel code matches an account's iata_code, channel_manager_code, crs_code or external_id: auto-linked.
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.
Mirai agency ID
The booking source's Mirai agency identifier matches mirai_agency_id: auto-linked.
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.
Invalid request body or query parameters
VALIDATION_ERRORInvalid or missing API key
UNAUTHORIZEDThe B2B CRM module is not enabled for this tenant (also FORBIDDEN when the key lacks the b2b scope)
MODULE_NOT_ENABLEDAccount id does not exist for this tenant
NOT_FOUNDparent_account reference does not resolve to an existing account
PARENT_NOT_FOUNDLinking the parent would exceed the 3-level account hierarchy
HIERARCHY_TOO_DEEPLinking the parent would create a cycle in the hierarchy
HIERARCHY_CYCLEDeals: the account reference does not resolve to an existing account
ACCOUNT_NOT_FOUNDDeals: no pipeline exists for the flow type (module defaults not seeded)
NO_PIPELINERate limit exceeded
RATE_LIMITEDError Handling
{
"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" }
]
}
}Request successful
Resource created successfully
Request accepted for async processing
Invalid request parameters
VALIDATION_ERRORInvalid or missing API key
UNAUTHORIZEDInsufficient permissions/scopes
FORBIDDENResource not found
NOT_FOUNDResource already exists
CONFLICTRequest understood but cannot be processed
UNPROCESSABLERate limit exceeded
RATE_LIMITEDServer error
INTERNAL_ERRORService temporarily unavailable
SERVICE_UNAVAILABLERate Limits
API requests are rate limited per API key. The default limit is 1,000 requests per minute.
Rate Limit Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
X-RateLimit-Limit | number | Required | Maximum requests per minute |
X-RateLimit-Remaining | number | Required | Requests remaining in current window |
X-RateLimit-Reset | number | 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. Limits depend on the API key and endpoint; core v1 routes also apply a 2,000 requests/minute IP guard. Website events use their own source limits.
Best Practices
For POST /api/v1/events, include a stable idempotency_key when retrying the same event. Other endpoints use their own retry and idempotency rules.
- Use a unique, deterministic key (e.g.,
checkin-{booking_id}) - Core events match the key against previously stored events; no universal 24-hour window applies
- A duplicate core event returns its event ID with
status: "duplicate"
Handle these failure cases in your integration:
- Retry transient 5xx errors with backoff according to the endpoint’s idempotency rules
- For HTTP 429, honour Retry-After. Fix other 4xx failures before retrying
- Log error responses for debugging
- Inspect error codes and per-item batch results; response formats vary by API family
- Choose timeouts for the endpoint and avoid concurrent duplicate submissions
Ensure your webhook endpoints are reliable:
- Acknowledge verified payloads promptly with a 2xx response
- Process webhooks asynchronously if needed
- Configure a webhook signing secret and verify the signature before processing
- Implement idempotent processing (webhooks may be delivered more than once)
- Use stable identifiers in the payload for idempotent processing.
X-Webhook-Deliveryidentifies an attempt
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)
Use our sandbox for development and testing:
- Use Mock mode for sample responses. Live mode sends real requests with your API key
- Test all error scenarios (validation, auth, rate limits)
- Use the interactive sandbox at /developers/sandbox
- Test webhook signature verification locally
Recommended Integration Flow
- Register custom field definitions via
POST /api/v1/fields(once) - Send guest + booking data via
POST /api/v1/guests - 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-Remainingto stay within limits - Implement exponential backoff on
429responses (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 is scoped to the hotel and tries
reservation_idfirst (stored asexternal_id), falling back tobooking_id. Send the samereservation_idto update a room line; send a sharedbooking_idto group the rooms of one key - Event endpoint uses
idempotency_keyfor deduplication; follow the endpoint’s documented retry contract
Performance Tips
- Use
hotel_codeinstead ofhotel_namefor faster hotel lookups - Register custom field definitions before sending guest data
- Include
custom_fieldsin 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.