Webhooks
Webhooks push conversation events from Lane.Chat to a URL on your server, so you can sync conversations, trigger downstream automation, or feed analytics. You subscribe per application and choose which events you want.
Configuration
Set a webhook URL, enable webhooks, and select events for an application (from the dashboard). Optionally set a webhook secret — when present, every delivery is HMAC-signed so you can verify authenticity.
Delivery format
Deliveries are POST requests with a JSON body. Every payload has the same envelope:
{
"event": "message.received",
"app_id": 123,
"app_pub": "app_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"timestamp": 1753180800,
"data": { }
}timestampis a Unix epoch (seconds).datais event-specific.- Deliveries are queued and retried asynchronously.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | LaneChat-Webhook/1.0 |
X-Webhook-Signature | HMAC-SHA256(secret, raw_body) (only when a secret is configured) |
Verifying the signature
The signature is the hex HMAC-SHA256 of the raw request body using your webhook secret. Compute it over the exact bytes you received and compare in constant time.
Node.js
import crypto from 'node:crypto'
function verify(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader || '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
// Express (make sure you have the RAW body, not the parsed object)
app.post('/lanechat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verify(raw, req.get('X-Webhook-Signature'), process.env.LANECHAT_WEBHOOK_SECRET)) {
return res.status(401).end()
}
const event = JSON.parse(raw)
// handle event...
res.status(200).end()
})PHP
function verify(string $rawBody, ?string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return is_string($signature) && hash_equals($expected, $signature);
}
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
if (!verify($raw, $sig, getenv('LANECHAT_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);Python
import hashlib
import hmac
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
# Flask
@app.post("/lanechat/webhook")
def hook():
raw = request.get_data()
if not verify(raw, request.headers.get("X-Webhook-Signature", ""), WEBHOOK_SECRET):
return "", 401
event = request.get_json()
return "", 200Sign over the raw body
Recompute the HMAC over the exact received bytes. If your framework re-serializes the JSON before you hash it, key ordering or whitespace changes will break the comparison.
Event catalog
The events you can subscribe to:
Message events
| Event | Fires when |
|---|---|
message.received | A message is received from a customer |
message.sent | A message is sent to a customer |
message.ai_response | The AI replies |
Conversation events
| Event | Fires when |
|---|---|
chat.started | A conversation starts |
chat.ended | A conversation ends |
chat.assigned | A conversation is assigned to an agent |
chat.transferred | A conversation is transferred |
chat.queued | A conversation enters the queue |
Customer events
| Event | Fires when |
|---|---|
client.online | A customer comes online |
client.offline | A customer goes offline |
client.banned | A customer is banned |
client.unbanned | A customer is unbanned |
client.binding | A customer binds a notification channel |
Agent events
| Event | Fires when |
|---|---|
agent.online | An agent comes online |
agent.offline | An agent goes offline |
agent.busy | An agent becomes busy |
agent.available | An agent becomes available |
System events
| Event | Fires when |
|---|---|
system.error | A system error occurs |
system.notification | A system notification is emitted |
Responding
Return a 2xx status quickly to acknowledge receipt. Do heavy work asynchronously — deliveries are retried on failure, so make your handler idempotent.