Everything you need to integrate your systems with our WhatsApp automation platform.
Get your first API call working in under 5 minutes with this step-by-step guide.
Request API access through our partner form or contact your account manager. You will receive:
gmkr_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
}
]
}'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"
}
}Configure webhook endpoints in your dashboard to receive real-time notifications when guests reply or journeys complete.
Continue reading below to learn about events, webhooks, error handling, and best practices for production integrations.
All API requests require authentication using a Bearer token in the Authorization header.
Authorization: Bearer gmkr_your_api_keyAPI 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.
| 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 |
bookings:write | Create/update reservations |
webhooks:manage | Configure outbound webhooks |
* | Full access (all scopes) |
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.
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.
/api/v1/reservations| 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" |
agency | string | Optional | Travel agency name |
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 |
| 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. |
| 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 |
| 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 |
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
}
]
}'{
"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.
/api/v1/reservations/batch| 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 | — |
no_show | Guest did not arrive | — |
Create and manage guest contacts with their booking information. Guests are automatically linked to hotels for AI-powered conversations and journey automation.
/api/v1/guests| 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 | Array of tags for segmentation |
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 |
| 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) |
| 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. |
| 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 |
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.
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"
}
}'{
"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"
}
}/api/v1/guests/batch/api/v1/guestsSend events to trigger journey automations. Events are matched against active journey triggers to start automated WhatsApp conversations.
/api/v1/events| 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 |
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)
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"
}'{
"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.
/api/v1/eventsDefine 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.
/api/v1/fields| 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) |
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"
}
]
}'{
"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"
}
}/api/v1/fieldsIntegration 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"] |
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.
/api/v1/cdp/eventsRequires visitor tracking to be enabled in CDP Settings.
| 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. |
| 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 |
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"
}
]
}'{
"success": true,
"data": {
"accepted": 3
}
}/api/v1/cdp/identify/api/v1/cdp/reservations/api/v1/cdp/reservations/batch/api/v1/cdp/reservations/bulk/api/v1/cdp/batches/{id}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.
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>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.
/api/v1/loyalty/check| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Required | Guest email address |
{
"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"
}
}
}{
"success": true,
"data": {
"is_member": false,
"member_summary": null
}
}/api/v1/loyalty/enroll/api/v1/loyalty/unsubscribe/api/v1/loyalty/member/api/v1/loyalty/points/api/v1/loyalty/redeem/api/v1/loyalty/redemptions/batch/api/v1/loyalty/rewards/api/v1/loyalty/tiers/api/v1/loyalty/transactions/api/v1/loyalty/calculateguests: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/POSPOST /api/v1/loyalty/redeem-free-night — redeem points for a free-night voucher codeGET /api/v1/loyalty/conversion — points ↔ currency calculator guests:readGET /api/v1/loyalty/households · POST — household pool balance & pooled redemptionGET /api/v1/loyalty/liability — points-liability & program-health report guests:readPOST /api/v1/loyalty/experiences/bid — bid points on an auction experienceAuthenticate 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.
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.
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.
guests:write)GET /api/v1/loyalty/credit/quote — read spendable + presetsPOST /api/v1/loyalty/credit/hold — reserve points (atomic, idempotent on external_reference_id)POST /api/v1/loyalty/credit/confirm — commit deductionPOST /api/v1/loyalty/credit/release — release an active holdPOST /api/v1/loyalty/credit/reverse — claw back after cancellationGET /api/v1/loyalty/reservation-snapshot — PMS folio render in one callGuestMaker 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)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');
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');
});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.
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 }
]
}| 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>| 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. |
| 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. |
| 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.
/api/contacts/{contactId}/call/api/contacts/{contactId}/calls| 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.
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.
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.
/api/v1/b2b/accounts| 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 |
| 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) |
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 |
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"
}'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"
}
}/api/v1/b2b/accounts/api/v1/b2b/accounts/{id}/api/v1/b2b/accounts/batch/api/v1/b2b/deals/api/v1/guestsEvery 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_LIMITED{
"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_UNAVAILABLEAPI requests are rate limited per API key. The default limit is 1,000 requests per minute.
| 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 |
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.
Always include an idempotency_key in event requests to prevent duplicate processing if you need to retry a failed request.
checkin-{booking_id})Implement robust error handling for a reliable integration:
Ensure your webhook endpoints are reliable:
X-Webhook-Delivery header for deduplicationAlways use E.164 format for phone numbers:
+34612345678 (Spain)Use our sandbox for development and testing:
gmkr_test_) for sandboxPOST /api/v1/fields (once)POST /api/v1/guestsPOST /api/v1/eventsX-RateLimit-Remaining to stay within limits429 responses (2s, 4s, 8s, 16s)update_if_exists: true (default) for safe upsertsbooking_id + hotel_code as the unique keyidempotency_key for dedup (24-hour validity)hotel_code instead of hotel_name for faster hotel lookupscustom_fields in the guest request to populate both JSONB storage and the segment builderInitial 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.