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

TransportHTTPS POST with a JSON body
FormatOne envelope for every event; payload version 2026-05-01
AuthenticityHMAC-SHA256 signature on every subscription delivery (and on automation API calls when you set a signing secret)
Events28 subscribable event types (plus a synthetic webhook.test)
ReliabilityAt-least-once; up to 6 attempts with increasing delays; 10-second timeout per attempt
EndpointsUp to 3 per organisation, each with its own secret, event selection, and up to 30 custom headers
HistoryDelivery 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"
    }
  }
}
FieldTypeDescription
idstring (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.
eventstringThe event type, e.g. ticket.assigned. Always matches the X-Webhook-Event header.
api_versionstringPayload schema version, currently 2026-05-01. It only changes if the shape changes incompatibly.
occurred_atstring (ISO 8601)When the underlying event happened (not when the delivery was attempted).
organisation_idstring (UUID)The organisation the event belongs to.
data.objectstringThe kind of record in the payload: one of message, message_status, template, ticket, contact, chat_resolution, chat_assignment, chat_team_assignment, chat_state, chat_workflow, flow_response, survey_response, campaign, channel, group, call, goal_milestone, member, or test.
data.<object>objectThe 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_attributesobjectOnly 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_at and 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

HeaderDescription
Content-TypeAlways application/json.
X-Webhook-IdThe delivery ID (same as id in the body). Idempotency key.
X-Webhook-EventThe event type (same as event in the body). Lets you route before parsing.
X-Webhook-TimestampUnix time in seconds when the delivery was signed. Part of the signature input.
X-Webhook-Signaturesha256=<hex HMAC> — see Verifying the signature. Always present on subscription deliveries; on automation API calls it is present only when the step has a signing secret.

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 2xx status 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.
  • Automatic pause and redelivery apply to endpoints (subscriptions) only. Automation API calls use the same retry schedule and the same 10-second timeout, but a failing target never pauses anything and cannot be redelivered from the webhooks screen — re-run the automation instead.

Chat identifiers — read this once

Two different chat identifiers appear across payloads, and mixing them up is the most common integration bug:

  • chat_id on ticket, chat_state, call, and goal_milestone payloads is the conversation ID (a UUID) — the same ID the Lodgestory API uses for chat endpoints.
  • chat_id on message payloads is the channel-level thread ID (for example [email protected] on WhatsApp) — the ID of the thread on the channel itself.
  • goal_milestone carries both: chat_id (conversation UUID) and channel_chat_id (channel thread ID), so you can join goal events to either stream.

Supported events

EventPayload objectFires whenprevious_attributes
message.sentmessageAn outbound message is sent on any channel — by an agent, a bot, a campaign, or the API.
message.receivedmessageAn inbound message arrives on any channel.
message.status_updatedmessage_statusA sent message's delivery status changes (SENT / DELIVERED / READ / FAILED), on channels that report receipts (WhatsApp Official).
template.createdtemplateA WhatsApp template is created and submitted for review.
template.status_updatedtemplateWhatsApp's review status for a template changes (approved, rejected, paused…).
template.quality_updatedtemplateWhatsApp's quality rating for an approved template changes (GREEN / YELLOW / RED / UNKNOWN).qualityScore
ticket.createdticketA ticket is filed — manually, by a bot journey, or via the API.
ticket.status_changedticketA ticket is resolved or reopened.status
ticket.assignedticketA ticket's assignee changes (including unassignment when a member is removed).assigned_to_user_id
contact.createdcontactA contact is created — from a new conversation, the create-contact action, a web-chat visitor identifying themselves, or automatically when an advisor captures a disposition on a chat that has no linked contact (origin tells you which). Bulk imports don't fire per-record events.
chat.resolvedchat_resolutionA conversation is resolved (closed) — by an agent, or by a workflow finishing when closing rules say so — or reopens: an admin or assignee reopens it, a new customer message auto-reopens it, or its workflow is restarted. via says which.resolved
chat.assignedchat_assignmentA conversation's primary assignee is set or changed — a bot journey hands it to a human, someone assigns it manually, or an automation assigns it. Secondary-assignee additions and removals don't fire it.assignee_user_id
chat.team_assignedchat_team_assignmentOne or more teams are newly attached to a conversation — the routing moment, which can happen with no individual owner selected (nobody online, or everyone at their open-chat cap — the chat then waits in the team's queue). Re-transfers to already-attached teams and team removals don't fire it.
chat.workflow_startedchat_workflowA workflow (lifecycle-stage set) is attached to a conversation — the moment it enters the workflow.
chat.state_changedchat_stateA conversation moves between workflow lifecycle stages (including a workflow restart).state_id, state_name
campaign.createdcampaignA campaign is created.
campaign.completedcampaignA campaign finishes: COMPLETED, or FAILED when no message went out. Cancelled campaigns don't fire it.status
flow.response_receivedflow_responseA customer completes a WhatsApp Flow — the submitted answers, correlated to the chat and (when applicable) the campaign or bot-journey send.
channel.connectedchannelA channel is connected (or recovers, for WhatsApp Official health).
channel.disconnectedchannelA channel is disconnected or removed.
group.createdgroupA WhatsApp group materialises on one of your WhatsApp Official channels. Group creation via the API is asynchronous — this event delivers the group's chat_id, correlated to your create call by request_id.
call.completedcallA call on a Lodgestory Voice number ends after being answered.
call.missedcallA call on a Lodgestory Voice number ends unanswered.
call.recording_availablecallA call's recording becomes available for playback/download.
call.transcript_availablecallA call's transcript becomes available.
survey.response_submittedsurvey_responseA survey response is submitted (or a partial response is finalised). Carries the primary score and sentiment band.
goal.milestone_reachedgoal_milestoneA live bot-journey session passes a Goal milestone. Builder test-runs never fire it.
member.removedmemberA team member is removed from the organisation.
webhook.testtestOnly 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.

messagemessage.sent, message.received

One 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.

FieldTypeDescription
idstringMessage ID. For WhatsApp this is the provider message ID (wamid.…); other channels use their own ID format.
chat_idstringChannel-level thread ID (see Chat identifiers).
channel_idstring (UUID)The connected channel the message flowed through.
channel_typestringwhatsapp, instagram, messenger, email, or webwidget.
directionstringinbound or outbound.
typestringMessage type — text, image, audio, video, document, location, sticker, contacts, interactive, button, and template sends.
textstringText/caption content, if any.
media_uristringMedia location for media messages, if any.
statusstringLatest known delivery status (SENT, DELIVERED, READ, FAILED); usually null on inbound messages.
from_namestringDisplay name of the sender (the customer on inbound).
from_numberstringSender's number/address on the channel.
reply_to_message_idstringID of the message this one replies to, if it's a reply.
agent_idstring (UUID)The team member who sent it, for agent-sent outbound messages.
timestampstringWhen 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_statusmessage.status_updated

Delivery-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 SENTDELIVEREDREAD. Failures (FAILED) include messages the provider accepted but could not deliver.

FieldTypeDescription
message_idstringThe message whose status changed — matches message.id from the original message.sent.
chat_idstringMay be null on status updates — correlate using message_id.
channel_idstring (UUID)The channel the message was sent on.
channel_typestringChannel type (see message).
statusstringSENT, DELIVERED, READ, or FAILED.
timestampstringWhen 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"
}

templatetemplate.created, template.status_updated, template.quality_updated

WhatsApp 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; template.quality_updated fires when WhatsApp's quality rating for an approved template moves (GREEN / YELLOW / RED / UNKNOWN), with the prior score in previous_attributes.qualityScore.

FieldTypeDescription
idstringThe template's ID with WhatsApp.
namestringTemplate name.
languagestringLanguage code, e.g. en, en_US.
categorystringMARKETING, UTILITY, or AUTHENTICATION.
statusstringReview status as reported by WhatsApp — e.g. PENDING, APPROVED, REJECTED, PAUSED.
rejected_reasonstringRejection reason (and recommendation, when provided) for rejected templates.
quality_scorestringWhatsApp quality rating, when reported.
{
  "id": "1189312045678901",
  "name": "booking_confirmation",
  "language": "en",
  "category": "UTILITY",
  "status": "APPROVED",
  "rejected_reason": null,
  "quality_score": null
}

ticketticket.created, ticket.status_changed, ticket.assigned

The 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.

FieldTypeDescription
idstring (UUID)Ticket ID.
ticket_numbernumberHuman-friendly per-organisation ticket number (#42).
chat_idstring (UUID)The conversation the ticket belongs to, if chat-linked.
call_log_idstring (UUID)The call the ticket was filed from, if call-linked.
customer_phonestringCustomer phone captured on the ticket.
customer_emailstringCustomer email captured on the ticket.
statusstringopen or resolved.
resolvedbooleanSame fact as status, as a boolean.
prioritystringLOW_PRIORITY, MEDIUM_PRIORITY, HIGH_PRIORITY, SOS, or NO_ACTION_REQUIRED.
assigned_to_user_idstring (UUID)Current assignee.
created_by_user_idstring (UUID)Who filed it (null for bot/API-created tickets).
issueobjectThe issue categories selected when the ticket was filed — mirrors your Ticket Workflow taxonomy for the channel.
remarksstringFree-text remarks trail.
created_atstringCreation time.
updated_atstringLast 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" }

contactcontact.created

FieldTypeDescription
idstring (UUID)Contact ID.
namestringFull display name.
first_namestringFirst name.
last_namestringLast name.
phonestringPhone number (without country code when stored separately).
country_codestringCountry dialling code, e.g. +91.
emailstringEmail address.
originstringHow the contact came to exist. MANUAL — created by a person or your systems: the CRM contact dialog, the API, a new conversation, or a web-chat visitor identifying themselves. DISPOSITION — created and linked automatically because an advisor captured a disposition on a chat that had no contact yet (email chats take the address, WhatsApp 1:1 chats take the number).
created_atstringCreation time.
{
  "id": "b7e23ec2-9054-4c3f-a1de-2f4b8c6d0e12",
  "name": "John Doe",
  "first_name": "John",
  "last_name": "Doe",
  "phone": "9999999999",
  "country_code": "+91",
  "email": null,
  "origin": "MANUAL",
  "created_at": "2026-08-02T10:14:05.000Z"
}

Treat origin: "DISPOSITION" contacts as already mid-conversation, not as new leads: a welcome sequence, a "thanks for reaching out" message, or a lead-scoring hook keyed on contact.created should skip them. The field is stamped on the event, not stored on the contact.

chat_resolutionchat.resolved

Fires when a conversation is resolved (closed), and again when it reopens. Resolution comes from an agent's explicit Resolve & Close, or from a workflow finishing when the organisation's Chat closing switch (or the workflow's own closing rule) says finishing also resolves the chat. Reopening comes from an admin or assignee flipping it back, a new customer message auto-reopening it, or the chat's workflow being restarted. resolved tells you the direction, via tells you the cause, and previous_attributes carries the prior flag.

FieldTypeDescription
chat_idstring (UUID)The conversation.
channel_chat_idstringThe channel-level chat identifier (see Chat identifiers).
channel_idstring (UUID)The connected channel the conversation lives on.
resolvedbooleanThe new state: true = resolved, false = reopened.
resolved_atstringWhen it was resolved; null on reopen.
resolved_by_user_idstring (UUID)The agent who resolved it; null on reopen.
viastringWhat caused the change. AGENT — the explicit Resolve & Close / Reopen action. INBOUND_MESSAGE — a new customer message auto-reopened the chat (always resolved: false). WORKFLOW — finishing a workflow resolved the chat under the resolve-on-workflow-finish rules (resolved_by_user_id is the agent who finished it), or restarting that workflow reopened it.
{
  "chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "channel_chat_id": "919812345678",
  "channel_id": "3f8e2a10-9b7c-4d2e-8f01-5a6b7c8d9e0f",
  "resolved": true,
  "resolved_at": "2026-08-02T12:30:00.000Z",
  "resolved_by_user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
  "via": "AGENT"
}

With:

"previous_attributes": { "resolved": false }

A workflow-driven resolution carries the same shape with "via": "WORKFLOW"; the finishing move itself arrives separately as chat.state_changed with is_final: true. Two cases produce no chat.resolved even though a workflow finished: another workflow on the chat is still active (the chat stays open), or the agent unticked Also mark chat as resolved on that close.

chat_assignmentchat.assigned

Fires when a conversation's primary assignee is set or changed — the moment a human takes ownership: a bot journey hands the chat to an agent or team, someone assigns it manually in the CRM, or an automation's assign-chat action runs. It does not fire for secondary-assignee additions, for deassignments, or for the administrative primary re-election that runs when a team member is removed from the workspace.

FieldTypeDescription
chat_idstring (UUID)The conversation.
channel_chat_idstringThe channel-level chat identifier (see Chat identifiers).
channel_idstring (UUID)The connected channel the conversation lives on.
chat_namestringThe conversation's display name.
assignee_user_idstring (UUID)The new primary assignee.
assignee_namestringTheir display name.
assignee_emailstringTheir email.
previous_assignee_user_idstring (UUID)The prior primary; null when the chat had none.
assigned_by_user_idstring (UUID)Who made the assignment — an agent, or the service account when a bot journey or automation did it.
{
  "chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "channel_chat_id": "919812345678",
  "channel_id": "3f8e2a10-9b7c-4d2e-8f01-5a6b7c8d9e0f",
  "chat_name": "Asha Patel",
  "assignee_user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
  "assignee_name": "Priya Sharma",
  "assignee_email": "[email protected]",
  "previous_assignee_user_id": null,
  "assigned_by_user_id": "5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f"
}

With:

"previous_attributes": { "assignee_user_id": null }

chat_team_assignmentchat.team_assigned

Fires when one or more teams are newly attached to a conversation — the routing moment. It fires even when no individual owner gets selected (an off-hours transfer where nobody on the team is available attaches the team but picks nobody — chat.assigned then never fires, and this event is the only signal). Only the newly attached teams are reported; teams already on the conversation are not repeated.

It is also the signal for capacity overflow. Organisations can cap each advisor's open chats; when every eligible member of the routed team is at their cap (or offline), the team is attached, nobody is picked, and the chat waits in the team's queue with no owner. chat.assigned follows only when someone claims it, an admin assigns it, or an automation pulls it — so a chat.team_assigned with no chat.assigned after a few minutes is the "chat is queueing" condition worth alerting on.

FieldTypeDescription
chat_idstring (UUID)The conversation.
channel_chat_idstringThe channel-level chat identifier (see Chat identifiers).
channel_idstring (UUID)The connected channel the conversation lives on.
chat_namestringThe conversation's display name.
team_idsarray of string (UUID)The newly attached teams.
team_namesarray of stringTheir names, same order.
assigned_by_user_idstring (UUID)Who routed it — an agent, or the service account when a bot journey did it.
{
  "chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "channel_chat_id": "919812345678",
  "channel_id": "3f8e2a10-9b7c-4d2e-8f01-5a6b7c8d9e0f",
  "chat_name": "Asha Patel",
  "team_ids": ["7b2e4c66-1a2b-4c3d-9e0f-a1b2c3d4e5f6"],
  "team_names": ["Reservations"],
  "assigned_by_user_id": "5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f"
}

chat_workflowchat.workflow_started

Fires when a workflow (lifecycle-stage set) is attached to a conversation — the moment it enters the workflow. Stage transitions and completion then flow as chat.state_changed.

FieldTypeDescription
chat_idstring (UUID)The conversation.
state_machine_idstring (UUID)The workflow attached.
state_machine_namestringIts name.
state_idstringThe initial stage's ID.
state_namestringThe initial stage's name.
is_finalbooleanAlways false — a workflow cannot start already closed.
attached_by_user_idstring (UUID)Who attached it, when a person did.
triggerstringAUTO (rule-attached) or MANUAL.
attached_atstringWhen the workflow was attached.
{
  "chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "state_machine_id": "0d47ac10-58cc-4372-a567-0e02b2c3d479",
  "state_machine_name": "Guest journey",
  "state_id": "node-1",
  "state_name": "New",
  "is_final": false,
  "attached_by_user_id": null,
  "trigger": "AUTO",
  "attached_at": "2026-08-02T12:30:00.000Z"
}

chat_statechat.state_changed

Fires 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.

FieldTypeDescription
chat_idstring (UUID)The conversation.
state_machine_idstring (UUID)The workflow (lifecycle-stage set) in play.
state_machine_namestringIts name.
state_idstringID of the stage the conversation is now in.
state_namestringName of that stage, e.g. Awaiting Guest.
is_finalbooleanWhether the new stage is a final stage.
responsible_user_idstring (UUID)Team member responsible for the transition, when recorded.
remarksstringTransition remarks, when provided.
changed_atstringWhen 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" }

campaigncampaign.created, campaign.completed

campaign.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.

FieldTypeDescription
idstring (UUID)Campaign ID.
namestringCampaign name.
statusstringDRAFT, SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, or FAILED.
channel_idstring (UUID)The channel the campaign sends on.
total_messagesnumberTotal recipients.
completed_messagesnumberSends that succeeded so far.
failed_messagesnumberSends that failed so far.
created_atstringCreation 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"
}

flow_responseflow.response_received

A customer completed a WhatsApp Flow. Every flow send carries a unique token minted by Lodgestory, so each response arrives already correlated: source says what sent the flow, and campaign_id is set for campaign sends. response is the answers object exactly as the flow's submit defined it (field names come from the flow's components); photo/document answers reference uploaded media ids.

FieldTypeDescription
idstring (UUID)The flow-token record's ID.
flow_idstringThe flow's ID with WhatsApp.
chat_idstringThe conversation (WhatsApp chat id) the response came from.
channel_idstring (UUID)The channel the flow was sent on.
sourcestringWhat sent the flow — journey, campaign, chat_template, or test_send.
campaign_idstring (UUID)The campaign, for campaign sends; otherwise null.
responseobjectThe submitted answers as key-value pairs.
sent_atstringWhen the flow message was sent.
responded_atstringWhen the customer submitted.
{
  "id": "9a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
  "flow_id": "662739091060588",
  "chat_id": "919888877766",
  "channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
  "source": "journey",
  "campaign_id": null,
  "response": {
    "check_in": "2026-09-12",
    "party_size": "4",
    "room_preference": "sea_view"
  },
  "sent_at": "2026-08-15T09:12:44.000Z",
  "responded_at": "2026-08-15T09:14:02.000Z"
}

channelchannel.connected, channel.disconnected

A connected messaging channel changed availability. The payload never includes channel credentials or tokens.

FieldTypeDescription
idstring (UUID)Channel ID — matches channel_id on message/campaign payloads.
channel_typestringwhatsapp, instagram, messenger, email, or webwidget.
namestringThe channel's identity on its platform (e.g. the WhatsApp business number ID).
phone_numberstringThe channel's phone number, for phone-based channels.
connectedbooleantrue on channel.connected, false on channel.disconnected.
{
  "id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
  "channel_type": "whatsapp",
  "name": "104857623456789",
  "phone_number": "+919888877766",
  "connected": true
}

groupgroup.created

A WhatsApp group appeared on one of your WhatsApp Official channels. Group creation via POST /api/wp-crm/groups/:orgId is asynchronous — the create call returns only a requestId; this event completes the loop by delivering the group's chat_id. Match on request_id to correlate. Groups created outside the API (e.g. from the connected phone) also fire it, with request_id: null.

The chat_id is a normal conversation ID: it works with the messaging, chats, and chat-assignment APIs, so you can immediately message the group and assign agents to it.

FieldTypeDescription
idstring (UUID)Group record ID (same value as chat_id, present for consistency with other *.created payloads).
chat_idstring (UUID)The group's conversation ID — use it with send-message, chats, and chat-assignments.
group_idstringWhatsApp's opaque group ID.
channel_idstring (UUID)The WhatsApp Official channel the group lives on.
subjectstringGroup subject/name.
descriptionstringGroup description, when set.
invite_linkstringShareable invite link, when already known.
join_approval_modebooleantrue when invite-link joiners need admin approval.
request_idstringThe requestId your create call returned; null for groups created outside the API.
created_atstring (ISO 8601)When the group was created.
{
  "id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "chat_id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "group_id": "120363043968812345",
  "channel_id": "3f6f6c53-9d2a-4a0e-8f2b-6b7e5a1c2d3e",
  "subject": "Villa Amara — Guest Group",
  "description": "Concierge support for your stay, 24×7.",
  "invite_link": "https://chat.whatsapp.com/JZk1aB2cD3e",
  "join_approval_mode": true,
  "request_id": "1234567890",
  "created_at": "2026-08-15T09:12:40.000Z"
}

callcall.completed, call.missed, call.recording_available, call.transcript_available

All 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.

FieldTypeDescription
idstring (UUID)Call ID — matches call_log_id on tickets filed from calls.
directionstringINBOUND or OUTBOUND.
statusstringFinal call status — COMPLETED for answered calls; NO_ANSWER, CANCELLED, BUSY, or FAILED for missed ones.
caller_numberstringThe calling party.
called_numberstringThe number dialled.
caller_idstringThe caller ID presented.
hangup_causestringTelephony hangup cause, e.g. NORMAL_CLEARING, NO_ANSWER, USER_BUSY.
agent_user_idstring (UUID)The team member who handled the call; null on missed calls.
missed_agentsarrayOn 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_idstring (UUID)The conversation the call is linked to, when matched to one.
duration_total_secnumberTotal seconds from initiation to hangup.
duration_talk_secnumberSeconds of talk time (0 for missed calls).
duration_ring_secnumberSeconds of ringing.
recording_availablebooleanWhether a recording is ready.
has_transcriptbooleanWhether a transcript is ready.
custom_identifierstringYour own reference, if one was attached when placing the call via the API.
initiated_atstringWhen the call started.
answered_atstringWhen it was answered (null if never).
ended_atstringWhen 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"
}

survey_responsesurvey.response_submitted

Fires when a survey response is submitted (or a partial response is finalised). Sensitive request metadata (IPs, tokens, user agents) is never included.

FieldTypeDescription
idstring (UUID)The response.
form_id / form_name / form_slugstringThe survey form it answers.
version_numbernumberThe published form version answered.
statusstringResponse status (e.g. COMPLETED, PARTIAL).
sourcestringHow it arrived (invite link, public link, in-chat…).
primary_scorenumberThe form's designated score question — what score-based automations key on.
primary_score_typestringThe score question's type (stars, NPS, thumbs…).
score_normalisednumberThe score mapped onto a 0–1 scale.
sentiment_bandstringDETRACTOR, PASSIVE, or PROMOTER.
invite_id / contact_idstring (UUID)The invite and contact, when known.
external_customer_refstringYour reference from a partner-API invite, when provided.
interaction_id / interaction_type / interaction_atstringThe call/chat/ticket the survey followed, when linked.
channel / agent_user_id / team_id / queue_idstringAttribution of the interaction, when known.
trigger_eventstringWhat sent the survey (e.g. an automation's trigger).
metadataobjectCustom metadata from a partner-API invite.
started_at / responded_atstringTiming of the response.
time_to_complete_secondsnumberHow long it took.
contact_consentbooleanWhether the respondent consented to be contacted.
consent_name / consent_email / consent_phonestringContact details they left, when consent was given.
answersarrayEvery answer. Each entry carries node_id, question_type, question_text, scale_min, scale_max, numeric_value, text_value, comment_text, selected_option_ids, selected_option_labels — the value fields not applicable to the question type are null.
{
  "id": "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9",
  "form_id": "0d47ac10-58cc-4372-a567-0e02b2c3d479",
  "form_name": "Post-stay CSAT",
  "form_slug": "post-stay-csat",
  "version_number": 3,
  "status": "COMPLETED",
  "source": "INVITE",
  "primary_score": 5,
  "primary_score_type": "STARS",
  "score_normalised": 1,
  "sentiment_band": "PROMOTER",
  "invite_id": "7b2e4c66-1a2b-4c3d-9e0f-a1b2c3d4e5f6",
  "contact_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "external_customer_ref": null,
  "interaction_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
  "interaction_type": "CALL",
  "interaction_at": "2026-08-02T12:00:00.000Z",
  "channel": "WHATSAPP_OFFICIAL",
  "agent_user_id": "5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f",
  "team_id": null,
  "queue_id": null,
  "trigger_event": "call.completed",
  "metadata": null,
  "started_at": "2026-08-02T12:31:00.000Z",
  "responded_at": "2026-08-02T12:33:10.000Z",
  "time_to_complete_seconds": 130,
  "contact_consent": true,
  "consent_name": "Asha Patel",
  "consent_email": null,
  "consent_phone": "+919812345678",
  "answers": [
    {
      "node_id": "q-rating",
      "question_type": "STARS",
      "question_text": "How was your stay?",
      "scale_min": 1,
      "scale_max": 5,
      "numeric_value": 5,
      "text_value": null,
      "comment_text": "Lovely villa!",
      "selected_option_ids": null,
      "selected_option_labels": null
    }
  ]
}

goal_milestonegoal.milestone_reached

A 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.

FieldTypeDescription
idstring (UUID)Milestone event ID (each occurrence is its own event record).
goal_idstring (UUID)The goal.
goal_namestringIts name, e.g. Bookings.
milestone_idstring (UUID)The milestone reached.
milestone_namestringIts name, e.g. Booking confirmed.
journey_idstringThe bot journey the session was running.
chat_idstring (UUID)Conversation ID (see Chat identifiers).
channel_chat_idstringChannel-level thread ID — joins to message.chat_id.
chat_namestringConversation display name.
channel_idstring (UUID)The channel the session ran on.
tracker_valuesobjectTracker name → captured value, as of this milestone. {} when none.
recorded_atstringWhen 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"
}

membermember.removed

A 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.

FieldTypeDescription
user_idstring (UUID)The removed member.
emailstringTheir email.
first_namestringFirst name.
last_namestringLast name.
organisation_idstring (UUID)The organisation they were removed from.
removed_rolesarrayThe roles they held at removal.
unassigned_chatsnumberOpen chats that lost their assignee.
unassigned_ticketsnumberOpen tickets that lost their assignee.
removed_atstringWhen 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"
}

testwebhook.test

Sent 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." }
  }
}

Automation API calls

An automation's Call your API action posts to an endpoint you choose, through this same delivery pipeline — signed, retried, and recorded in the delivery log. It is not a subscription: nobody subscribes to automation.api_call, and it is delivered only to the URL configured on that step. See the Automations guide for how to set one up.

The envelope carries three things a subscription delivery can't: which automation fired, and the run's live context — the chat/contact/agent/ticket/call/survey values re-read at execution time, after any waits — alongside the trigger record itself in the usual data shape.

Headers. The standard delivery headers apply (X-Webhook-Id, X-Webhook-Timestamp, and X-Webhook-Signature when a signing secret is set — verified exactly as above), plus X-Webhook-Event: automation.api_call and any custom headers configured on the step. Three extra identity headers ride along:

HeaderMeaning
X-Automation-Rule-IdThe automation that fired.
X-Automation-Run-IdThis enrolment — one run per trigger occurrence.
X-Automation-Step-IdWhich step in that run made the call.

Idempotency. Delivery is at-least-once (retries, and a step replay after an interrupted run). Deduplicate on run_id + step_id, which are stable across every attempt — not on id, which is per-delivery.

FieldTypeDescription
eventstringAlways automation.api_call.
automation.rule_id / rule_namestringThe automation that fired.
automation.run_idstring (UUID)The enrolment. Half of the idempotency key.
automation.step_idstring (UUID)The step that called. The other half.
automation.trigger_eventstringThe event that started the run, e.g. chat.assigned.
dataobjectThe trigger's record in the same shape a subscription delivery of trigger_event would carry (data.object names the key). For a scheduled automation — which has no trigger record — data.object is "trigger".
context.chat / contact / agent / ticket / call / surveyobjectThe run's evaluated context at execution time. Groups the trigger doesn't populate are present but empty.
{
  "id": "b21c8e14-9f3a-4d77-a0c2-6e5b1f2d3c4a",
  "event": "automation.api_call",
  "api_version": "2026-05-01",
  "occurred_at": "2026-08-14T09:15:00.000Z",
  "organisation_id": "550e8400-e29b-41d4-a716-446655440000",
  "automation": {
    "rule_id": "9d3d5405-b33e-4cd5-9b1e-106a3cb8e116",
    "rule_name": "Unanswered handover watchdog",
    "run_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "step_id": "8cfff0de-7cc4-4395-909a-5a7a312747d5",
    "trigger_event": "chat.assigned"
  },
  "data": {
    "object": "chat_assignment",
    "chat_assignment": {
      "chat_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
      "assignee_user_id": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
      "previous_assignee_user_id": null
    }
  },
  "context": {
    "chat": {
      "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
      "name": "Asha Patel",
      "primaryAssigneeUserId": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
      "agentRepliedSinceTrigger": false,
      "resolved": false
    },
    "contact": { "e164": "+919812345678", "fullName": "Asha Patel" },
    "agent": { "userId": "8a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "fullName": "Priya Sharma" },
    "ticket": {}, "call": {}, "survey": {}
  }
}

Endpoint requirements. The URL must be http(s) and resolve to a public address — loopback, private, link-local and reserved ranges are refused at connection time, so an internal service cannot be reached even via DNS that resolves privately. Redirects are followed at most three times, each re-checked. Respond 2xx to acknowledge; anything else is retried with the standard backoff.


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 & pathWhat it does
POST /api/wp-crm/webhooksCreate an endpoint. Returns the record including its generated signing secret.
GET /api/wp-crm/webhooksList the organisation's endpoints.
PUT /api/wp-crm/webhooks/:idUpdate URL, events, description, headers, or isActive (re-enabling resets the failure counter).
DELETE /api/wp-crm/webhooks/:idDelete an endpoint.
POST /api/wp-crm/webhooks/:id/rotate-secretGenerate and return a new signing secret.
GET /api/wp-crm/webhooks/:id/deliveries?limit=50Recent deliveries for the endpoint (default 50, max 200).
POST /api/wp-crm/webhooks/:id/testSend a webhook.test delivery.
POST /api/wp-crm/webhooks/deliveries/:deliveryId/redeliverReplay a logged delivery (fresh delivery ID, original payload).

Create/update body fields

FieldTypeNotes
urlstringThe receiving endpoint. Must be a valid URL and must not point at the Lodgestory API. Use HTTPS in production.
eventsstring[]Any set of the subscribable events listed above.
descriptionstringOptional label shown in the UI.
headersobjectOptional 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.
isActivebooleanUpdate 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


Did this page help you?