Overview
Outbound webhooks let Chatlane notify your systems the moment something happens in an inbox. Each webhook is an HTTPS URL plus a set of subscribed events; whenever a subscribed event occurs, Chatlane sends an HTTP POST with a JSON payload describing what happened.
Webhooks are configured per inbox by team admins under Inbox Settings → Webhooks. Each webhook has:
- URL — the HTTPS endpoint Chatlane will POST to.
- Secret (optional) — used to sign each delivery so you can verify it came from Chatlane. Stored encrypted.
- Subscribed events — the events this endpoint should receive (see Events below).
- Active — inactive webhooks receive nothing.
Delivery
- Deliveries are
POSTrequests withContent-Type: application/jsonand the event payload as the raw body. - Your endpoint should respond with a
2xxstatus within 10 seconds. Slow endpoints should acknowledge immediately and process asynchronously. - If the request cannot be completed (connection failure, timeout), delivery is retried up to 3 times, backing off 30 seconds, 2 minutes, then 5 minutes. Non-2xx responses are logged but not retried.
- Deliveries may occasionally arrive out of order or more than once — use the IDs in the payload to deduplicate if needed.
Verifying signatures
When a webhook has a secret, every delivery includes an X-Chatlane-Signature header: the HMAC-SHA256 hex digest of the raw request body, keyed with your secret. Compute the same digest and compare using a constant-time comparison.
PHP
$signature = hash_hmac('sha256', $request->getContent(), $secret);
if (! hash_equals($signature, $request->header('X-Chatlane-Signature'))) {
abort(401);
}
Node.js
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(req.headers['x-chatlane-signature'] ?? ''))) {
return res.status(401).end();
}
Common payload fields
Every event payload includes:
| Field | Type | Description |
|---|---|---|
event |
string | The event name, e.g. message.created. |
inbox_id |
integer | The inbox the event occurred in. |
conversation |
object | { "id": 123, "status": "active" } — the conversation involved. |
contact |
object | null | { "id": 5, "email": "[email protected]" } — the conversation's contact, or null for entity ("order") conversations. |
order_external_id |
string | null | The external ID of the entity record (e.g. an order number) for entity-inbox conversations; null for regular contact conversations. |
Events
message.created
Fires when a new message is added to a conversation. Activity entries never fire this event; internal notes only fire it when recorded via the API with record_only, and record_only transactional emails are excluded.
{
"event": "message.created",
"from_contact": true,
"inbox_id": 12,
"conversation": { "id": 345, "status": "active" },
"contact": { "id": 5, "email": "[email protected]" },
"recipient": null,
"order_external_id": null,
"message": {
"id": 6789,
"type": "web",
"created_at": "2026-07-22T13:45:00+00:00"
}
}
| Field | Type | Description |
|---|---|---|
from_contact |
boolean | true when the message was sent by the contact (inbound); false for outbound/team messages. |
contact |
object | null | The counterpart contact: the sender for inbound messages, the addressed recipient for outbound ones. |
recipient |
object | null | The explicitly addressed peer contact for direct replies; null when the message is addressed to the support team. |
message.type |
string | Channel type, e.g. web, email, sms, whatsapp, note. |
conversation.read
Fires when a conversation that had unread activity is marked as read by a team member.
{
"event": "conversation.read",
"inbox_id": 12,
"conversation": { "id": 345, "status": "active" },
"contact": null,
"order_external_id": "ORD-1042"
}
conversation.created
Fires when a new conversation is started in the inbox — by an inbound message, the widget, or a team member.
{
"event": "conversation.created",
"inbox_id": 12,
"conversation": { "id": 346, "status": "active" },
"contact": { "id": 5, "email": "[email protected]" },
"order_external_id": null
}
conversation.status.updated
Fires when a conversation's status changes (e.g. it is closed, reopened, or marked pending).
{
"event": "conversation.status.updated",
"inbox_id": 12,
"conversation": { "id": 345, "status": "closed" },
"contact": { "id": 5, "email": "[email protected]" },
"order_external_id": null,
"old_status": "active",
"new_status": "closed"
}
| Field | Type | Description |
|---|---|---|
old_status |
string | Status before the change: active, pending, or closed. |
new_status |
string | Status after the change. Matches conversation.status. |
conversation.assigned
Fires when a conversation's assignee changes, including when it becomes unassigned.
{
"event": "conversation.assigned",
"inbox_id": 12,
"conversation": { "id": 345, "status": "active" },
"contact": { "id": 5, "email": "[email protected]" },
"order_external_id": null,
"assignee": {
"id": 3,
"name": "Alex Doe",
"email": "[email protected]"
}
}
| Field | Type | Description |
|---|---|---|
assignee |
object | null | The team member now assigned, or null when the conversation was unassigned. |
message.delivery_status.updated
Fires when a message's delivery status changes — typically as outbound messages progress through provider callbacks. This event can be high-volume; subscribe only if you need per-message delivery tracking.
{
"event": "message.delivery_status.updated",
"inbox_id": 12,
"conversation": { "id": 345, "status": "active" },
"contact": { "id": 5, "email": "[email protected]" },
"order_external_id": null,
"message": {
"id": 6789,
"type": "whatsapp",
"delivery_status": "delivered"
}
}
| Field | Type | Description |
|---|---|---|
message.delivery_status |
string | The new status, e.g. sent, delivered, read, failed. |
Backward compatibility
Webhooks created before event subscriptions existed are pinned to message.created and conversation.read. Payloads are additive: new fields may be added over time, so parse leniently and ignore unknown fields and unknown event names.