Webhooks API reference
Lodgestory can notify your systems the moment something happens in your workspace — a message arrives, a ticket changes hands, a campaign finishes, a call ends. You register an HTTPS endpoint, pick the events you care about, and Lodgestory POSTs a signed JSON notification to it for every matching event.
This page is the developer contract: the envelope every delivery shares, the headers and signature scheme, retry behaviour, the complete event catalogue, and the exact payload of every event type. For the product walkthrough (creating endpoints in the UI, delivery log, test and replay), see the Webhooks settings guide.
At a glance
| Transport | HTTPS POST with a JSON body |
| Format | One envelope for every event; payload version 2026-05-01 |
| Authenticity | HMAC-SHA256 signature on every delivery |
| Events | 20 subscribable event types (plus a synthetic webhook.test) |
| Reliability | At-least-once; up to 6 attempts with increasing delays; 10-second timeout per attempt |
| Endpoints | Up to 3 per organisation, each with its own secret, event selection, and up to 30 custom headers |
| History | Delivery log retained 30 days, with on-demand replay |
The envelope
Every delivery — regardless of event type — is a POST with this JSON body:
{
"id": "0c6532a2-8e2a-4b6e-9d5e-1f9b1a7e4c11",
"event": "message.received",
"api_version": "2026-05-01",
"occurred_at": "2026-08-02T10:14:02.000Z",
"organisation_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"object": "message",
"message": {
"…": "the full object — see the per-event reference below"
}
}
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique delivery ID. Automatic retries of a failed delivery reuse the same id; a manual Redeliver mints a new one. Use it to deduplicate. |
event | string | The event type, e.g. ticket.assigned. Always matches the X-Webhook-Event header. |
api_version | string | Payload schema version, currently 2026-05-01. It only changes if the shape changes incompatibly. |
occurred_at | string (ISO 8601) | When the underlying event happened (not when the delivery was attempted). |
organisation_id | string (UUID) | The organisation the event belongs to. |
data.object | string | The kind of record in the payload: one of message, message_status, template, ticket, contact, chat_state, campaign, channel, call, goal_milestone, member, or test. |
data.<object> | object | The full record, under a key named after data.object (e.g. data.ticket). A point-in-time snapshot taken when the event fired. |
data.previous_attributes | object | Only on change-type events (see the catalogue below) — the value(s) before the change. |
Delivery semantics
- At-least-once. The same delivery can occasionally arrive twice (for example, if your endpoint responds slowly and a retry overlaps). Make your handler idempotent using
id/X-Webhook-Id. - No ordering guarantee. Deliveries are made in parallel and retried independently, so events can arrive out of order. Sequence with
occurred_atand the timestamps inside each object, not arrival order. - Snapshots, not live reads. The payload reflects the record at the moment the event fired. If you need the current state later, read it from the API.
- Per-endpoint filtering. An endpoint only receives the event types it subscribes to.
Delivery headers
| Header | Description |
|---|---|
Content-Type | Always application/json. |
X-Webhook-Id | The delivery ID (same as id in the body). Idempotency key. |
X-Webhook-Event | The event type (same as event in the body). Lets you route before parsing. |
X-Webhook-Timestamp | Unix time in seconds when the delivery was signed. Part of the signature input. |
X-Webhook-Signature | sha256=<hex HMAC> — see Verifying the signature. |
If you configured custom headers on the endpoint (for example, an Authorization header for your own gateway), they are sent on every delivery in addition to the above.
Verifying the signature
Every delivery is signed with the endpoint's signing secret (shown when you create the endpoint; view or rotate it any time). The signature is an HMAC-SHA256 over the timestamp header and the raw request body, joined by a dot:
signature = "sha256=" + hex( HMAC_SHA256( secret, "<X-Webhook-Timestamp>.<raw body>" ) )
To verify: recompute it from the exact bytes you received and compare against X-Webhook-Signature using a constant-time comparison. Always use the raw, unparsed body — re-serializing the JSON changes the bytes and breaks the check.
Node.js
import crypto from 'crypto';
function isValidDelivery(req, secret) {
const timestamp = req.headers['x-webhook-timestamp'];
const signature = req.headers['x-webhook-signature']; // "sha256=…"
const expected =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.rawBody}`) // the exact bytes received
.digest('hex');
return (
signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}Python
import hashlib, hmac
def is_valid_delivery(headers, raw_body: bytes, secret: str) -> bool:
timestamp = headers["X-Webhook-Timestamp"]
signature = headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Replay protection. The timestamp is part of the signed input, so an attacker can't re-sign an old body with a fresh time. Reject deliveries whose X-Webhook-Timestamp is older than a few minutes (5 minutes is a sensible tolerance — remember that retries of a failed delivery are re-signed with a fresh timestamp on each attempt).
Rotation. After rotating a secret, deliveries are signed with the new secret immediately. If you need zero-gap rotation, verify against both secrets for a short window on your side.
Responding, retries, and automatic pause
- Respond with any
2xxstatus to acknowledge. Anything else — including a timeout — counts as a failure. - Each attempt waits up to 10 seconds for your response. Acknowledge fast and process asynchronously; the response body is ignored (only the first 1,000 bytes are kept for the delivery log).
- Failed deliveries are retried automatically: up to 6 attempts with roughly doubling delays, spread over about 3 minutes.
- If an endpoint keeps failing — 15 consecutive events exhausting all their retries — it is paused automatically and stops receiving traffic. Fix your endpoint, then re-enable it (re-enabling resets the failure count). Any successful delivery also resets the count.
- Every attempt is recorded in the delivery log (kept for 30 days) with status, response code, and attempt count. You can redeliver any logged delivery on demand; the replay carries the original payload with a fresh delivery ID.
Chat identifiers — read this once
Two different chat identifiers appear across payloads, and mixing them up is the most common integration bug:
chat_idonticket,chat_state,call, andgoal_milestonepayloads is the conversation ID (a UUID) — the same ID the Lodgestory API uses for chat endpoints.chat_idonmessagepayloads is the channel-level thread ID (for example[email protected]on WhatsApp) — the ID of the thread on the channel itself.goal_milestonecarries both:chat_id(conversation UUID) andchannel_chat_id(channel thread ID), so you can join goal events to either stream.
Supported events
| Event | Payload object | Fires when | previous_attributes |
|---|---|---|---|
message.sent | message | An outbound message is sent on any channel — by an agent, a bot, a campaign, or the API. | — |
message.received | message | An inbound message arrives on any channel. | — |
message.status_updated | message_status | A sent message's delivery status changes (SENT / DELIVERED / READ / FAILED), on channels that report receipts (WhatsApp Official). | — |
template.created | template | A WhatsApp template is created and submitted for review. | — |
template.status_updated | template | WhatsApp's review status for a template changes (approved, rejected, paused…). | — |
ticket.created | ticket | A ticket is filed — manually, by a bot journey, or via the API. | — |
ticket.status_changed | ticket | A ticket is resolved or reopened. | status |
ticket.assigned | ticket | A ticket's assignee changes (including unassignment when a member is removed). | assigned_to_user_id |
contact.created | contact | A contact is created — from a new conversation, the create-contact action, or a web-chat visitor identifying themselves. Bulk imports don't fire per-record events. | — |
chat.state_changed | chat_state | A conversation moves between workflow lifecycle stages (including a workflow restart). | state_id, state_name |
campaign.created | campaign | A campaign is created. | — |
campaign.completed | campaign | A campaign finishes: COMPLETED, or FAILED when no message went out. Cancelled campaigns don't fire it. | status |
channel.connected | channel | A channel is connected (or recovers, for WhatsApp Official health). | — |
channel.disconnected | channel | A channel is disconnected or removed. | — |
call.completed | call | A call on a Lodgestory Voice number ends after being answered. | — |
call.missed | call | A call on a Lodgestory Voice number ends unanswered. | — |
call.recording_available | call | A call's recording becomes available for playback/download. | — |
call.transcript_available | call | A call's transcript becomes available. | — |
goal.milestone_reached | goal_milestone | A live bot-journey session passes a Goal milestone. Builder test-runs never fire it. | — |
member.removed | member | A team member is removed from the organisation. | — |
webhook.test | test | Only when you press Send test (or call the test API) — never subscribable, delivered regardless of the endpoint's event selection. | — |
Call events fire for calls on Lodgestory Voice numbers. Calls carried by third-party telephony providers connected to your workspace don't produce webhooks.
Event payloads
Each section below documents one data.object type: the events that use it, every field, and a full example. All field names are snake_case; timestamps are ISO 8601 strings; any field can be null when the underlying value is absent.
message — message.sent, message.received
message — message.sent, message.receivedOne payload per message, on every channel. direction always matches the event: inbound for message.received, outbound for message.sent. Internal system entries (join notes, automated markers) never fire message events. Campaign sends fire one message.sent per recipient.
| Field | Type | Description |
|---|---|---|
id | string | Message ID. For WhatsApp this is the provider message ID (wamid.…); other channels use their own ID format. |
chat_id | string | Channel-level thread ID (see Chat identifiers). |
channel_id | string (UUID) | The connected channel the message flowed through. |
channel_type | string | whatsapp, instagram, messenger, email, or webwidget. |
direction | string | inbound or outbound. |
type | string | Message type — text, image, audio, video, document, location, sticker, contacts, interactive, button, and template sends. |
text | string | Text/caption content, if any. |
media_uri | string | Media location for media messages, if any. |
status | string | Latest known delivery status (SENT, DELIVERED, READ, FAILED); usually null on inbound messages. |
from_name | string | Display name of the sender (the customer on inbound). |
from_number | string | Sender's number/address on the channel. |
reply_to_message_id | string | ID of the message this one replies to, if it's a reply. |
agent_id | string (UUID) | The team member who sent it, for agent-sent outbound messages. |
timestamp | string | When the message was recorded. |
{
"id": "wamid.HBgMOTE5OTk5OTk5OTk5FQIAEhgg…",
"chat_id": "[email protected]",
"channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
"channel_type": "whatsapp",
"direction": "inbound",
"type": "text",
"text": "Is breakfast included with the deluxe room?",
"media_uri": null,
"status": null,
"from_name": "John Doe",
"from_number": "+919999999999",
"reply_to_message_id": null,
"agent_id": null,
"timestamp": "2026-08-02T10:14:02.000Z"
}message_status — message.status_updated
message_status — message.status_updatedDelivery-receipt updates for messages you sent, on channels that report them (WhatsApp Official today). Expect one event per status transition — a read message typically produces SENT → DELIVERED → READ. Failures (FAILED) include messages the provider accepted but could not deliver.
| Field | Type | Description |
|---|---|---|
message_id | string | The message whose status changed — matches message.id from the original message.sent. |
chat_id | string | May be null on status updates — correlate using message_id. |
channel_id | string (UUID) | The channel the message was sent on. |
channel_type | string | Channel type (see message). |
status | string | SENT, DELIVERED, READ, or FAILED. |
timestamp | string | When the provider reported the status. |
{
"message_id": "wamid.HBgMOTE5OTk5OTk5OTk5FQIAEhgg…",
"chat_id": null,
"channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
"channel_type": "whatsapp",
"status": "READ",
"timestamp": "2026-08-02T10:15:40.000Z"
}template — template.created, template.status_updated
template — template.created, template.status_updatedWhatsApp template lifecycle. template.created fires when the template is submitted (status is typically the initial review state); template.status_updated fires whenever WhatsApp's review status changes.
| Field | Type | Description |
|---|---|---|
id | string | The template's ID with WhatsApp. |
name | string | Template name. |
language | string | Language code, e.g. en, en_US. |
category | string | MARKETING, UTILITY, or AUTHENTICATION. |
status | string | Review status as reported by WhatsApp — e.g. PENDING, APPROVED, REJECTED, PAUSED. |
rejected_reason | string | Rejection reason (and recommendation, when provided) for rejected templates. |
quality_score | string | WhatsApp quality rating, when reported. |
{
"id": "1189312045678901",
"name": "booking_confirmation",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"rejected_reason": null,
"quality_score": null
}ticket — ticket.created, ticket.status_changed, ticket.assigned
ticket — ticket.created, ticket.status_changed, ticket.assignedThe full ticket record. ticket.status_changed fires only on a genuine open ⇄ resolved transition, with previous_attributes.status. ticket.assigned fires whenever the assignee changes, with previous_attributes.assigned_to_user_id (the previous assignee, or null if it was unassigned). When a member is removed from the organisation, each of their open tickets fires ticket.assigned with assigned_to_user_id: null.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Ticket ID. |
ticket_number | number | Human-friendly per-organisation ticket number (#42). |
chat_id | string (UUID) | The conversation the ticket belongs to, if chat-linked. |
call_log_id | string (UUID) | The call the ticket was filed from, if call-linked. |
customer_phone | string | Customer phone captured on the ticket. |
customer_email | string | Customer email captured on the ticket. |
status | string | open or resolved. |
resolved | boolean | Same fact as status, as a boolean. |
priority | string | LOW_PRIORITY, MEDIUM_PRIORITY, HIGH_PRIORITY, SOS, or NO_ACTION_REQUIRED. |
assigned_to_user_id | string (UUID) | Current assignee. |
created_by_user_id | string (UUID) | Who filed it (null for bot/API-created tickets). |
issue | object | The issue categories selected when the ticket was filed — mirrors your Ticket Workflow taxonomy for the channel. |
remarks | string | Free-text remarks trail. |
created_at | string | Creation time. |
updated_at | string | Last update time. |
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"ticket_number": 42,
"chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"call_log_id": null,
"customer_phone": "+919999999999",
"customer_email": null,
"status": "resolved",
"resolved": true,
"priority": "HIGH_PRIORITY",
"assigned_to_user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"created_by_user_id": "77fa1329-5f0b-4c2d-9e3a-1b2c3d4e5f60",
"issue": { "Room Service": ["Late delivery"] },
"remarks": "Guest called twice; comped breakfast.",
"created_at": "2026-08-01T09:00:00.000Z",
"updated_at": "2026-08-02T11:02:00.000Z"
}With, on ticket.status_changed:
"previous_attributes": { "status": "open" }contact — contact.created
contact — contact.created| Field | Type | Description |
|---|---|---|
id | string (UUID) | Contact ID. |
name | string | Full display name. |
first_name | string | First name. |
last_name | string | Last name. |
phone | string | Phone number (without country code when stored separately). |
country_code | string | Country dialling code, e.g. +91. |
email | string | Email address. |
created_at | string | Creation time. |
{
"id": "b7e23ec2-9054-4c3f-a1de-2f4b8c6d0e12",
"name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"phone": "9999999999",
"country_code": "+91",
"email": null,
"created_at": "2026-08-02T10:14:05.000Z"
}chat_state — chat.state_changed
chat_state — chat.state_changedFires when a conversation moves between workflow lifecycle stages, including moving into a final stage and restarting a finished workflow. previous_attributes carries the stage it moved from.
| Field | Type | Description |
|---|---|---|
chat_id | string (UUID) | The conversation. |
state_machine_id | string (UUID) | The workflow (lifecycle-stage set) in play. |
state_machine_name | string | Its name. |
state_id | string | ID of the stage the conversation is now in. |
state_name | string | Name of that stage, e.g. Awaiting Guest. |
is_final | boolean | Whether the new stage is a final stage. |
responsible_user_id | string (UUID) | Team member responsible for the transition, when recorded. |
remarks | string | Transition remarks, when provided. |
changed_at | string | When the transition happened. |
{
"chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"state_machine_id": "0d47ac10-58cc-4372-a567-0e02b2c3d479",
"state_machine_name": "Guest journey",
"state_id": "node-3",
"state_name": "Awaiting Guest",
"is_final": false,
"responsible_user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"remarks": "Waiting on guest confirmation",
"changed_at": "2026-08-02T12:30:00.000Z"
}With:
"previous_attributes": { "state_id": "node-2", "state_name": "In Progress" }campaign — campaign.created, campaign.completed
campaign — campaign.created, campaign.completedcampaign.created fires when the campaign is created (large file-based campaigns report SCHEDULED while recipients are still being prepared). campaign.completed fires exactly once when sending finishes: status is COMPLETED, or FAILED when not a single message went out. A cancelled campaign never fires campaign.completed. The counters are final on the completion event.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Campaign ID. |
name | string | Campaign name. |
status | string | DRAFT, SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, or FAILED. |
channel_id | string (UUID) | The channel the campaign sends on. |
total_messages | number | Total recipients. |
completed_messages | number | Sends that succeeded so far. |
failed_messages | number | Sends that failed so far. |
created_at | string | Creation time. |
{
"id": "5f7d8e9a-0b1c-2d3e-4f50-617283940a5b",
"name": "August offers",
"status": "COMPLETED",
"channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
"total_messages": 1200,
"completed_messages": 1187,
"failed_messages": 13,
"created_at": "2026-08-01T08:00:00.000Z"
}channel — channel.connected, channel.disconnected
channel — channel.connected, channel.disconnectedA connected messaging channel changed availability. The payload never includes channel credentials or tokens.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Channel ID — matches channel_id on message/campaign payloads. |
channel_type | string | whatsapp, instagram, messenger, email, or webwidget. |
name | string | The channel's identity on its platform (e.g. the WhatsApp business number ID). |
phone_number | string | The channel's phone number, for phone-based channels. |
connected | boolean | true on channel.connected, false on channel.disconnected. |
{
"id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
"channel_type": "whatsapp",
"name": "104857623456789",
"phone_number": "+919888877766",
"connected": true
}call — call.completed, call.missed, call.recording_available, call.transcript_available
call — call.completed, call.missed, call.recording_available, call.transcript_availableAll four events share this payload — a snapshot of the call record. On call.recording_available the payload always has recording_available: true; on call.transcript_available it always has has_transcript: true. Recording/transcript events can arrive minutes after call.completed for the same call (id ties them together). Fires for Lodgestory Voice numbers only.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Call ID — matches call_log_id on tickets filed from calls. |
direction | string | INBOUND or OUTBOUND. |
status | string | Final call status — COMPLETED for answered calls; NO_ANSWER, CANCELLED, BUSY, or FAILED for missed ones. |
caller_number | string | The calling party. |
called_number | string | The number dialled. |
caller_id | string | The caller ID presented. |
hangup_cause | string | Telephony hangup cause, e.g. NORMAL_CLEARING, NO_ANSWER, USER_BUSY. |
agent_user_id | string (UUID) | The team member who handled the call; null on missed calls. |
missed_agents | array | On missed calls: the agents that were rung, in ring order — each { "user_id", "name", "number" } (user_id/name are null when the rung extension maps to no team member). null when not applicable. |
chat_id | string (UUID) | The conversation the call is linked to, when matched to one. |
duration_total_sec | number | Total seconds from initiation to hangup. |
duration_talk_sec | number | Seconds of talk time (0 for missed calls). |
duration_ring_sec | number | Seconds of ringing. |
recording_available | boolean | Whether a recording is ready. |
has_transcript | boolean | Whether a transcript is ready. |
custom_identifier | string | Your own reference, if one was attached when placing the call via the API. |
initiated_at | string | When the call started. |
answered_at | string | When it was answered (null if never). |
ended_at | string | When it ended. |
{
"id": "0b1f2e3d-4c5b-6a79-8897-a6b5c4d3e2f1",
"direction": "INBOUND",
"status": "NO_ANSWER",
"caller_number": "+919999999999",
"called_number": "+918888877766",
"caller_id": "+919999999999",
"hangup_cause": "NO_ANSWER",
"agent_user_id": null,
"missed_agents": [
{ "user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "name": "Asha Rao", "number": "1001" },
{ "user_id": null, "name": null, "number": "1004" }
],
"chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"duration_total_sec": 32,
"duration_talk_sec": 0,
"duration_ring_sec": 28,
"recording_available": false,
"has_transcript": false,
"custom_identifier": null,
"initiated_at": "2026-08-02T14:05:10.000Z",
"answered_at": null,
"ended_at": "2026-08-02T14:05:42.000Z"
}goal_milestone — goal.milestone_reached
goal_milestone — goal.milestone_reachedA live bot-journey conversation passed a Goal milestone. Carries the tracker values captured up to that point — ready to pipe conversions into your analytics or CRM. Fires for live conversations only; builder test-runs are excluded.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Milestone event ID (each occurrence is its own event record). |
goal_id | string (UUID) | The goal. |
goal_name | string | Its name, e.g. Bookings. |
milestone_id | string (UUID) | The milestone reached. |
milestone_name | string | Its name, e.g. Booking confirmed. |
journey_id | string | The bot journey the session was running. |
chat_id | string (UUID) | Conversation ID (see Chat identifiers). |
channel_chat_id | string | Channel-level thread ID — joins to message.chat_id. |
chat_name | string | Conversation display name. |
channel_id | string (UUID) | The channel the session ran on. |
tracker_values | object | Tracker name → captured value, as of this milestone. {} when none. |
recorded_at | string | When the milestone was recorded. |
{
"id": "9e8d7c6b-5a49-3827-1605-f4e3d2c1b0a9",
"goal_id": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"goal_name": "Bookings",
"milestone_id": "aa11bb22-cc33-dd44-ee55-ff6677889900",
"milestone_name": "Booking confirmed",
"journey_id": "journey-42",
"chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"channel_chat_id": "[email protected]",
"chat_name": "John Doe",
"channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
"tracker_values": { "Order_ID": "ORD-1042", "Room_Type": "Deluxe" },
"recorded_at": "2026-08-02T09:15:00.000Z"
}member — member.removed
member — member.removedA team member was removed from the organisation. Their open chats and tickets are unassigned as part of removal — the counts tell you how many, and each unassigned ticket also fires its own ticket.assigned event (new assignee null), so you can reconcile either way.
| Field | Type | Description |
|---|---|---|
user_id | string (UUID) | The removed member. |
email | string | Their email. |
first_name | string | First name. |
last_name | string | Last name. |
organisation_id | string (UUID) | The organisation they were removed from. |
removed_roles | array | The roles they held at removal. |
unassigned_chats | number | Open chats that lost their assignee. |
unassigned_tickets | number | Open tickets that lost their assignee. |
removed_at | string | When the removal happened. |
{
"user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"email": "[email protected]",
"first_name": "Asha",
"last_name": "Rao",
"organisation_id": "550e8400-e29b-41d4-a716-446655440000",
"removed_roles": ["CRM_USER"],
"unassigned_chats": 4,
"unassigned_tickets": 2,
"removed_at": "2026-08-02T16:20:00.000Z"
}test — webhook.test
test — webhook.testSent only when you trigger Send test on an endpoint. It is delivered, signed, retried, and logged exactly like a real event — use it to verify your signature check end to end. It ignores the endpoint's event selection and cannot be subscribed to.
{
"id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"event": "webhook.test",
"api_version": "2026-05-01",
"occurred_at": "2026-08-02T17:00:00.000Z",
"organisation_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"object": "test",
"test": { "message": "This is a test webhook from your CRM." }
}
}Managing endpoints via the API
Everything the Settings → Webhooks UI does is available under /api/wp-crm/webhooks, authenticated with a signed-in admin session and scoped by the organisationId query parameter on every call.
| Method & path | What it does |
|---|---|
POST /api/wp-crm/webhooks | Create an endpoint. Returns the record including its generated signing secret. |
GET /api/wp-crm/webhooks | List the organisation's endpoints. |
PUT /api/wp-crm/webhooks/:id | Update URL, events, description, headers, or isActive (re-enabling resets the failure counter). |
DELETE /api/wp-crm/webhooks/:id | Delete an endpoint. |
POST /api/wp-crm/webhooks/:id/rotate-secret | Generate and return a new signing secret. |
GET /api/wp-crm/webhooks/:id/deliveries?limit=50 | Recent deliveries for the endpoint (default 50, max 200). |
POST /api/wp-crm/webhooks/:id/test | Send a webhook.test delivery. |
POST /api/wp-crm/webhooks/deliveries/:deliveryId/redeliver | Replay a logged delivery (fresh delivery ID, original payload). |
Create/update body fields
| Field | Type | Notes |
|---|---|---|
url | string | The receiving endpoint. Must be a valid URL and must not point at the Lodgestory API. Use HTTPS in production. |
events | string[] | Any set of the subscribable events listed above. |
description | string | Optional label shown in the UI. |
headers | object | Optional custom headers sent with every delivery — up to 30; names must be valid HTTP header names (≤ 256 chars); values ≤ 2,048 chars, no line breaks. |
isActive | boolean | Update only — enable/disable the endpoint. |
Limits
- 3 endpoints per organisation.
- 30 custom headers per endpoint (name ≤ 256 chars, value ≤ 2,048 chars, no line breaks).
- Endpoint URLs cannot target the Lodgestory API.
- Delivery log retention: 30 days (replay is available within that window).
- Response bodies are truncated to 1,000 bytes in the delivery log.
Related pages
- Webhooks settings guide — the product walkthrough: creating endpoints, delivery log, test and replay.
- Accounts & sign-in — authenticating against the Lodgestory public API for follow-up reads.
- AI assistants (MCP) — another way to act on Lodgestory data from your own tools.
Updated about 1 hour ago
