Developers

Guestly Partner API

Guestly's partner API lets your platform run WhatsApp conversations for every hotel you manage.

Base URLhttps://api.guestly.ai/api/v1

Overview

Send approved WhatsApp templates (booking offers, check-in messages, links to your own offer pages) and free-form replies with text, images, videos or documents.
Read every conversation and message, so your team sees WhatsApp next to the email threads it already uses.
Stay in sync in real time: Guestly calls your server for every new message — sent by the guest, the hotel's staff, the Guestly AI assistant, a campaign, or you — and for every delivery status change.
One API key belongs to one hotel. Integrate once and use one key per property.

1. Before you begin

Base URLhttps://api.guestly.ai/api/v1
Test environmenthttps://dev-api.guestly.ai/api/v1 (keys start with gst_test_)
Authenticationx-api-key: <key> header on every request
Format JSON (UTF-8). File uploads use multipart/form-data.

On the hotel's side:

  1. The hotel is onboarded on Guestly and has connected its WhatsApp Business number.
  2. Guestly activates API access for the hotel and sends you your key. The key is shown once; keep it on your server, never in a browser or app. If a key leaks, ask Guestly to revoke it; it stops working within a minute.
  3. To send templates, the hotel has at least one template approved by WhatsApp (you can list them with the API, see section 4).
  4. For real-time sync, send Guestly your webhook URL. We set it up and send you the signing secret (section 10).

Check a key with:

HTTP
GET /partner/me
x-api-key: gst_live_…
JSON
{
  "business": { "id": "66e42ceeb622697a6f788bb3", "name": "Hotel Alpenrose" },
  "whatsapp": { "connected": true, "phone_number": "+39 0471 123456", "display_name": "Hotel Alpenrose" },
  "key": { "id": "6ab98b77754244405c647b2e", "label": "Booking system" },
  "rate_limits": {
    "all_endpoints": { "per_minute": 120, "per_day": null },
    "send_message": { "per_second": 3, "per_day": null },
    "send_template": { "per_second": 1, "per_day": 250 },
    "list_templates": { "per_second": 5, "per_day": null }
  }
}

2. Errors and rate limits

Every error has the same shape:

JSON
{
  "error": {
    "code": "OUTSIDE_24H_WINDOW",
    "message": "The guest has not written in the last 24 hours, so WhatsApp only allows an approved template.",
    "details": { "last_inbound_at": "2026-09-23T09:21:02.586Z" }
  }
}
HTTPcodeMeaning
400VALIDATION_ERRORA field is missing or invalid (the message says which).
400INVALID_PHONE The phone number can't be parsed. Use +<country code><number>.
400MISSING_TEMPLATE_VARIABLE A template variable has no value (details.missing lists them).
401INVALID_API_KEYMissing, wrong or revoked key.
404CONTACT_NOT_FOUND / ROOM_NOT_FOUND / TEMPLATE_NOT_FOUNDNot found for this hotel.
409TEMPLATE_NOT_APPROVEDThe template exists but WhatsApp hasn't approved it.
409OUTSIDE_24H_WINDOW Free-form messages need the guest to have written in the last 24 hours. Send a template instead.
409WHATSAPP_NOT_CONNECTEDThe hotel has no WhatsApp number connected.
422NOT_ON_WHATSAPPThe number isn't a WhatsApp account.
422WHATSAPP_ERROR WhatsApp rejected the message; meta_code carries WhatsApp's error code and message its reason.
429RATE_LIMITED Too many requests in a second or minute; retry after Retry-After seconds.
429DAILY_LIMIT_EXCEEDED A daily limit is used up; it resets at 00:00 UTC (Retry-After counts down to it).
500INTERNAL_ERRORSomething failed on Guestly's side. Safe to retry.
503WHATSAPP_ERRORWhatsApp couldn't be reached. Safe to retry.

Rate limits. Limits are per hotel: all keys of one hotel share them. Defaults:

NameApplies toLimitPer day
all_endpointsevery request120 per minuteno limit
send_messagePOST /partner/messages with type text, image, video or document3 per secondno limit
send_templatePOST /partner/messages with type template (any template) 1 per second250
list_templatesGET /partner/templates5 per secondno limit

GET /partner/me returns the hotel's current limits (null = no limit). Ask Guestly if you need different ones.

  • Windows are fixed: each second, minute or UTC day starts a new count. Days reset at 00:00 UTC.
  • A request rejected with 429 doesn't count toward the limit that rejected it. A send that fails (any error response) doesn't count toward its daily limit.
  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the per-minute limit. Sends with a daily limit also carry X-RateLimit-Daily-Limit and X-RateLimit-Daily-Remaining.
  • A 429 carries Retry-After (seconds) and error.details, for example { "limit": "send_template", "window": "day", "max": 250, "retry_after": 7201 }.

WhatsApp also limits how many new conversations a hotel can open per day (its messaging tier). A send over that limit returns 422 WHATSAPP_ERROR with meta_code: 131048.

3. Concepts

  • Contact: a guest of the hotel, identified by id. You can always address a guest by phone number instead; Guestly finds the existing contact or creates one (you can pass name, surname, email, language). Existing data is never overwritten — only empty fields are filled.
  • Room: the one WhatsApp conversation between the hotel and a contact.
  • 24-hour window: WhatsApp allows free-form messages (text, media) only within 24 hours of the guest's last message. Outside it, only approved templates can be sent. Rooms report window.open and window.expires_at.
  • Phone numbers: send +39 333 123 4567, 0039… or, for guests in the hotel's own country, the national number. Responses use E.164 (+393331234567).

4. List templates

HTTP
GET /partner/templates?language=it

language (en, it, de) is optional. Only templates approved by WhatsApp are returned.

JSON
{
  "data": [
    {
      "name": "cloud_offer_invite",
      "language": "it",
      "category": "MARKETING",
      "header": { "format": "IMAGE", "text": null, "media_url": "https://…/hero.jpg", "media_overridable": true },
      "body": "Ciao {{1}}, la Sua offerta per il soggiorno dal {{2}} è pronta.",
      "footer": null,
      "variables": [
        { "position": 1, "name": "FIRSTNAME", "auto_filled": true, "example": "Mario" },
        { "position": 2, "name": "arrival", "auto_filled": false, "example": "12 maggio" }
      ],
      "buttons": [
        { "index": 0, "type": "URL", "text": "Apri l'offerta", "url": "https://www.cloudoffer.it/{{1}}", "dynamic": true, "auto_filled": false }
      ]
    }
  ]
}
  • variables are the body placeholders {{1}}, {{2}}, … in order.
  • auto_filled: true means Guestly can fill the value from its own contact or booking data (first name, last name, title, check-in/out dates, nights, guests, room, booking number, pre-check-in link). You can still pass your own value, and should if the hotel's bookings aren't in Guestly.
  • example is the sample the hotel entered when creating the template. It is never sent in place of a missing value.
  • dynamic: true buttons need a value for the {{1}} in their URL (for example an offer id), unless auto_filled is true (Guestly fills it, for example with the guest's GuestNet login link). A missing value returns 400 MISSING_TEMPLATE_VARIABLE; the template's sample URL is never sent.

5. Send a message

HTTP
POST /partner/messages

Every message has a recipient to and a type: text, image, video, document or template.

Recipient

JSON
"to": { "contact_id": "65f1c0b8a2e3f4c5d6e7f8a9" }

or

JSON
"to": { "phone": "+393331234567", "name": "Mario", "surname": "Rossi", "email": "[email protected]", "language": "it" }

5.1 Template (works any time)

JSON
{
  "to": { "phone": "+393331234567", "name": "Mario" },
  "type": "template",
  "template": {
    "name": "cloud_offer_invite",
    "language": "it",
    "variables": {
      "body": ["Mario", "12 maggio"],
      "header_media_url": "https://cdn.example.com/offers/abc123.jpg",
      "buttons": ["abc123"]
    }
  }
}
FieldRequiredDescription
template.name, template.languageyesAs returned by GET /partner/templates.
variables.bodyif the template has variables Either an array in placeholder order, or an object by variable name: { "FIRSTNAME": "Mario", "arrival": "12 maggio" }. Names are case-insensitive. Leave an auto_filled variable out (or empty) to let Guestly fill it.
variables.header_media_urlno Replaces the approved header image, video or document for this send.
variables.buttonsif the template has dynamic URL buttons Values for the dynamic buttons, in order. With https://www.cloudoffer.it/{{1}} and "buttons": ["abc123"] the guest opens https://www.cloudoffer.it/abc123. variables.button is accepted as a shorthand for one value.

5.2 Text (within the 24-hour window)

JSON
{
  "to": { "contact_id": "65f1c0b8a2e3f4c5d6e7f8a9" },
  "type": "text",
  "text": "Hi Mario, your offer is ready — let me know if you have any questions!"
}

5.3 Image, video or document (within the 24-hour window)

By URL:

JSON
{
  "to": { "contact_id": "65f1c0b8a2e3f4c5d6e7f8a9" },
  "type": "document",
  "media": { "url": "https://cdn.example.com/invoices/2026-0142.pdf", "filename": "Invoice 2026-0142.pdf", "caption": "Your invoice" }
}

Or upload the file with multipart/form-data:

Bash
curl -X POST https://api.guestly.ai/api/v1/partner/messages \
  -H "x-api-key: $GUESTLY_KEY" \
  -F "to[contact_id]=65f1c0b8a2e3f4c5d6e7f8a9" \
  -F "type=image" \
  -F "media[caption]=Your room is ready" \
  -F "[email protected]"

Response (201)

The sent message, in the same shape as everywhere else (section 9):

JSON
{
  "data": {
    "id": "6ab98bb73f66ba571c7380fa",
    "message_id": "wamid.HBgMMzkz…",
    "room_id": "69134ad2f633ce7c42606a54",
    "contact": { "id": "65f1c0b8a2e3f4c5d6e7f8a9", "phone": "+393331234567", "name": "Mario Rossi" },
    "direction": "outbound",
    "sent_by": "api",
    "sender_name": "Booking system",
    "type": "template",
    "text": "Ciao Mario, la Sua offerta per il soggiorno dal 12 maggio è pronta.",
    "status": "sent",
    "created_at": "2026-09-28T10:14:03.000Z"
  }
}

status: "sent" means WhatsApp accepted the message. Delivery and read receipts follow as message.status webhooks (section 10). Store data.id and data.contact.id: you can address the guest by contact_id from then on, and the same id appears in webhooks.

6. Message format rules

Message partLimit
Text messageup to 4,096 characters
Media captionup to 1,024 characters
ImageJPEG or PNG, up to 5 MB
VideoMP4 or 3GPP (H.264 video, AAC audio), up to 16 MB
Document PDF, Word, Excel, PowerPoint, TXT and other common types, up to 100 MB
  • Formatting uses WhatsApp's own syntax, not Markdown or HTML: *bold*, _italic_, ~strikethrough~, ```monospace```, `inline code`, lists starting with - or 1. , and quotes starting with > .
  • Links are sent as plain URLs; WhatsApp shows a preview.
  • Media URLs must be publicly reachable (no login, no expiring signature shorter than a few minutes). WhatsApp downloads the file when the message is sent.
  • Templates: the text is fixed by the approved template; only variables, header media and dynamic button values change per send. New templates are created and submitted for approval in Guestly (Template Studio).

7. List conversations (rooms)

HTTP
GET /partner/rooms?status=open&updated_since=2026-09-27T00:00:00Z&page=1&limit=20
ParameterDescription
statusopen or closed.
contact_id / phoneOnly the room of this contact.
updated_since Only rooms with a message at or after this time (ISO-8601).
page, limit Pagination; limit up to 100 (default 20). Newest activity first.
JSON
{
  "data": [
    {
      "id": "69134ad2f633ce7c42606a54",
      "status": "open",
      "contact": { "id": "65f1…", "phone": "+393331234567", "name": "Mario", "surname": "Rossi", "email": null, "language": "it" },
      "last_message_at": "2026-09-23T09:21:53.948Z",
      "last_message_by": "guest",
      "last_message_preview": "Do you have a room for two next weekend?",
      "unread_count": 1,
      "window": { "open": true, "expires_at": "2026-09-24T09:21:02.586Z" }
    }
  ],
  "pagination": { "page": 1, "limit": 20, "has_more": false }
}

8. List messages in a room

HTTP
GET /partner/rooms/{room_id}/messages?limit=50
GET /partner/rooms/{room_id}/messages?before={message id}   older page
GET /partner/rooms/{room_id}/messages?after={message id}    newer messages, oldest first
JSON
{ "data": [ { …message… } ], "has_more": true, "order": "newest_first" }

Use before with the oldest id you have to page back through history, and after with the newest id to catch up after downtime.

9. The message object

FieldDescription
idGuestly message id. Stable; use it to deduplicate.
message_id WhatsApp message id (wamid…), or null if it wasn't sent.
room_idThe conversation.
contact{ id, phone, name } of the guest.
directioninbound (guest → hotel) or outbound (hotel → guest).
sent_byguest, agent (hotel staff in Guestly), ai (Guestly AI assistant), api (a partner API key — including yours), whatsapp_app (the hotel's WhatsApp Business app), or system (campaign, broadcast or scheduled message).
sender_name Staff member, AI assistant name, or the API key's label, when known.
typetext, template, image, video, audio, document, sticker, location, contacts or reaction.
text Message text, media caption, or the template text as the guest saw it.
media{ url, filename } for media messages, otherwise null.
location{ latitude, longitude, name?, address? } for location messages.
reaction{ message_id, emoji } for reactions.
template{ name, language } for template messages.
reply_toWhatsApp id of the message this one replies to.
statusreceived (inbound), sending, sent, delivered, read or failed.
error{ code, message } from WhatsApp when status is failed.
created_atISO-8601 UTC.

10. Webhooks

Send Guestly your HTTPS endpoint. We set it up, send it a test ping and share the signing secret (whsec_…) with you. A webhook needs an active API key for the hotel; revoking the hotel's last key turns the webhook off.

Guestly then POSTs every event to that one URL:

HTTP
POST https://your-server.example.com/guestly
Content-Type: application/json
X-Guestly-Event: message.created
X-Guestly-Delivery: evt_qGlH_spKIgkHAapr
X-Guestly-Signature: t=1790545000,v1=5f2b…
JSON
{
  "id": "evt_qGlH_spKIgkHAapr",
  "event": "message.created",
  "created_at": "2026-09-28T10:14:03.512Z",
  "business_id": "66e42ceeb622697a6f788bb3",
  "data": { …message object (section 9)… }
}
eventdata
message.created A new message in any of the hotel's conversations — both directions, every sender (including messages you sent).
message.status{ id, message_id, room_id, status, error, timestamp } when an outbound message is sent, delivered, read or failed.
pingTest event.

Verify the signature

v1 is the HMAC-SHA256 of "{t}.{raw request body}" with the signing secret. Reject requests whose signature doesn't match or whose t is more than 5 minutes old.

Node.js:

JavaScript
const crypto = require('crypto');

function verifyGuestly(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const given = Buffer.from(String(parts.v1 || ''));
  return fresh && given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), given);
}

Python:

Python
import hashlib, hmac, time

def verify_guestly(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts["v1"])

Delivery

  • Answer with any 2xx within 5 seconds, then process the event asynchronously.
  • Anything else (or a timeout) is retried after 30 s, 2 min, 10 min, then every 15 min — for about 6 hours in total.
  • Delivery is at least once: deduplicate on the event id (or the message id).
  • Events can arrive out of order: sort by created_at, and treat message.status as "latest wins". If you ever miss events, catch up with GET /partner/rooms/{id}/messages?after=….

11. Quick reference

MethodPathPurpose
GET/partner/meCheck the key; hotel and WhatsApp number.
GET/partner/templatesApproved templates and the values they need.
POST/partner/messagesSend a template, text, image, video or document.
GET/partner/roomsConversations, newest activity first.
GET/partner/rooms/{id}/messagesMessages of one conversation.
POSTyour webhook URL Guestly → you: message.created, message.status, ping.

All paths are relative to https://api.guestly.ai/api/v1.

Support

For test keys on our test environment, template design or higher limits, contact the Guestly integrations team.

Language

© 2025 Guestly