Overview
Webhooks let a Talk with Guest integration receive events push-style instead of polling. When something meaningful happens inside the platform — a reservation is confirmed, a folio is charged, a guest sends a WhatsApp message, a review lands from an OTA — the platform serialises an event, signs it, and posts it to every endpoint that has subscribed to that event. Consumers acknowledge with a 2xx; anything else triggers the retry ladder documented below.
Events are the recommended pattern for anything reservation-driven. Polling GET /reservations to detect changes is legal, cheap and rate-limited generously, but it always trails the actual state of the world by whatever your polling interval is. A webhook consumer sees a new confirmed reservation within one to two seconds of the platform accepting the booking.
Available events
The catalogue below is the stable set. New event names are added on a minor version bump — never removed within a major version — so a consumer that filters on an unknown event name is future-safe.
| Event | Emitted when |
|---|---|
| reservation.created | A reservation is written to the platform (direct, OTA or manual entry) |
| reservation.updated | Stay window, room type, rate quote or guest pointer changes |
| reservation.cancelled | A reservation is cancelled by the guest, the OTA or an operator |
| reservation.checked_in | An operator or the self-check-in flow moves the reservation to checked_in |
| reservation.checked_out | Check-out completes and the folio is closed |
| guest.updated | A guest profile is created or edited (name, contact points, preferences, document data) |
| folio.charged | A charge is posted to a folio (room, ancillary, tax, adjustment) |
| folio.paid | A payment is applied to a folio |
| message.received | An inbound guest message arrives on any channel |
| message.delivered | An outbound message is confirmed delivered by the transport |
| review.received | A post-stay review lands from an OTA or a direct-review request |
| channel.sync_failed | An OTA sync attempt fails after the internal retry budget is exhausted |
Payload format
Every event is a JSON object with a stable outer envelope and an event-specific data object inside. The envelope carries the event id, the event name, the tenant, a millisecond-precision timestamp and the platform version that emitted it. The data object carries the full current shape of the resource the event is about, so a consumer does not have to make a second GET to hydrate context.
POST /your-webhook-endpoint HTTP/1.1
Host: your-service.example.com
Content-Type: application/json
X-Talkguest-Event: reservation.created
X-Talkguest-Event-Id: evt_01HYQ8XN2K9AB4CB
X-Talkguest-Timestamp: 1722355200000
X-Talkguest-Signature: t=1722355200000,v1=8f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
{
"id": "evt_01HYQ8XN2K9AB4CB",
"event": "reservation.created",
"tenant_id": "ten_01HYQ8XN2K9AB4C0",
"created_at": "2026-07-30T14:00:00.000Z",
"api_version": "v1",
"data": {
"id": "res_01HYQ8XN2K9AB4C7",
"status": "confirmed",
"channel": "booking_com",
"arrival": "2026-08-03",
"departure": "2026-08-06",
"guest_id": "gst_01HYQ8XN2K9AB4C6",
"rate_plan_id": "rp_flex_bar",
"room_type_id": "rt_dbl_seaview",
"quote": { "currency": "EUR", "total": "489.72" }
}
} Signature verification
Every webhook request carries an X-Talkguest-Signature header. The signature is HMAC-SHA256 of the concatenation of the timestamp, a dot, and the raw request body, computed with the endpoint's signing secret as the key. The secret is issued when you register the endpoint and can be rotated at any time. The header carries both the timestamp used in the signing and the signature, so a consumer can reject requests with a timestamp older than five minutes to defeat replay attacks.
import hmac, hashlib
def verify(secret: bytes, header: str, body: bytes) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
ts, sig = parts["t"], parts["v1"]
expected = hmac.new(secret, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return False
# reject anything older than 5 minutes to prevent replay
import time
return abs(time.time() * 1000 - int(ts)) < 5 * 60 * 1000 Verify the signature before you parse the body for anything other than raw bytes. If you parse first and verify second, an attacker who knows a shape you accept but not the secret can still trigger side effects in your parser. The signing secret is displayed once at endpoint creation and re-displayable only on rotation; store it in the same secrets manager you use for other integration credentials, never in code.
Retry policy
A consumer that returns any 2xx is considered to have accepted the event. Anything else — 4xx, 5xx, timeout, DNS failure, TCP reset — triggers the retry ladder. The Guest Talk platform retries up to five times with exponential backoff: after 30 seconds, 2 minutes, 10 minutes, 1 hour, and 6 hours. A consumer that never returns 2xx across all five attempts is marked failing; the endpoint stays subscribed but events stop being delivered until an operator acknowledges the failure through the UI or the API.
Retries carry a new X-Talkguest-Delivery-Attempt header incrementing from 1 to 5 so a consumer can distinguish a fresh event from a retry. The X-Talkguest-Event-Id stays constant across retries, which is what a consumer should key idempotency off — if you have already processed the event id, return 200 immediately without touching your database. Talkguest guarantees at-least-once delivery, not exactly-once; idempotency is the consumer's responsibility.
Best practices
- Verify the HMAC signature before parsing the JSON body. Reject anything that fails verification with a
401. - Return
200from the endpoint as soon as you have persisted the raw event; do the actual work in a background job. A synchronous consumer that ties up the socket for two seconds does not scale to peak. - Key idempotency off
X-Talkguest-Event-Id. Duplicates are guaranteed, not exceptional. - Log the timestamp, the event id and the raw body for at least 30 days. When you file a support ticket for a missed event this is the first thing we ask for.
- Do not filter events by string equality in production code. Filter by prefix so a new
reservation.*event does not silently break your router.
Testing locally
The recommended local testing pattern is a tunnel — ngrok, cloudflared, or the equivalent — pointed at your local endpoint, with a webhook subscription in the sandbox environment pointing at the tunnel URL. The sandbox environment emits the same events as production against synthetic data, so you can drive a full end-to-end verification without touching a real property.
Every endpoint has a Replay button in the operator UI under Settings → Integrations → Webhooks. Replay resends the last successfully-signed payload to the endpoint with a new event id but the same data object; this is the fastest way to iterate on parser code without waiting for a new event to happen naturally.
Subscribing an endpoint
POST /v1/webhook-endpoints
Content-Type: application/json
{
"url": "https://your-service.example.com/talkguest/events",
"events": ["reservation.*", "folio.charged", "message.received"],
"description": "Data warehouse ingestion",
"active": true
} The response carries the signing secret in secret; save it immediately, it is not retrievable later. Wildcards are allowed in the events array — reservation.* subscribes to every event under the reservation namespace. To unsubscribe an event without removing the endpoint, PATCH the endpoint with a smaller events array.
Next steps
Read the API reference for the shape of the resources inside each event's data object. Read the rate limits page if your consumer will call back into the API to hydrate additional context. Read the error codes page for the shape of the failure envelopes you may receive. If your integration needs webhook delivery with SLAs above the standard, contact the integrations team through the contact page or request access to Enterprise tier.