Β§ 00What webhooks are
When something happens on the EasyLiveChat side β a customer messages your widget, an agent closes a conversation, an audit-relevant action occurs β we make an outbound HTTPS POST to every URL you've registered. The body is JSON and contains the full event payload, so your server can react without polling.
Think of it as the mirror image of the REST API. The API is for code you control reaching into EasyLiveChat. Webhooks are for EasyLiveChat reaching into code you control. Most integrations use both.
Each registered endpoint has its own secret. We sign every request body with HMAC-SHA256 using that secret and send the hex digest in the x-easylivechat-signature header. Your server recomputes the HMAC with the same secret and compares β match means the request really came from us, mismatch means drop it.
Β§ 01Plan availability
Webhook endpoints can be created on every paid plan and during the 14-day Free trial. There's no per-event billing β fan-out to multiple endpoints is included.
The audit.event stream is Enterprise-only, gated by the auditWebhookExport entitlement. message.created and conversation.updated fire on every plan that allows API access.
Β§ 02Configure an endpoint
Add an endpoint at https://app.livechattools.com/settings/webhooks. We'll generate a random per-endpoint secret and show it to you exactly once on creation. Treat it like a password β copy it into your server's secret store immediately.
- URL β Must be https:// and resolve to a public IP. We refuse loopback, private, and cloud-metadata addresses both at creation and at delivery time.
- Events β Either pick specific event names or subscribe to * (all events). Wildcards are convenient for prototyping but tighter event lists make your handler easier to reason about.
- Secret β Auto-generated, shown once. Used to sign every delivery. If you lose it, delete the endpoint and create a new one β there's no recovery path.
Active by default. You can revoke an endpoint at any time from the same page β deliveries stop instantly.
Β§ 03Payload shape
Every delivery is a POST with content-type: application/json. Headers identify the event and carry the signature; the body envelope is always { event, deliveredAt, data }:
POST https://your-server.example.com/webhook
content-type: application/json
user-agent: EasyLiveChat-Webhooks/1.0
x-easylivechat-event: message.created
x-easylivechat-signature: sha256=<hex>
{
"event": "message.created",
"deliveredAt": "2026-05-27T08:55:25.841Z",
"data": {
"conversationId": "cmp...",
"message": {
"id": "cmp...",
"body": "Hi!",
"senderType": "CUSTOMER",
"createdAt": "2026-05-27T08:55:25.840Z"
}
}
}event is the same value as the x-easylivechat-event header. data shape varies by event β see the event catalog below.
Β§ 04Verify the signature
Anyone who knows your endpoint URL can POST junk at it. The HMAC signature is how you tell our requests apart from spoofed ones. Recompute it with your secret and reject the request on mismatch.
Read the raw body bytes (do NOT json.parse first β re-serialising will reorder keys and break the hash). Strip the sha256= prefix from the signature header. Compute HMAC-SHA256(rawBody, secret) and timing-safely compare against the received hex.
Node.js
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.WEBHOOK_SECRET;
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const received = (req.header('x-easylivechat-signature') || '').replace(/^sha256=/, '');
const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// handle event.event, event.data...
res.status(200).send('ok');
});Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
SECRET = os.environ["WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/webhook")
def webhook():
received = request.headers.get("x-easylivechat-signature", "").removeprefix("sha256=")
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
event = request.get_json()
# handle event["event"], event["data"]...
return "ok", 200curl (debugging)
# Recompute the signature locally and compare with the header. echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
timingSafeEqual / hmac.compare_digest β always use a constant-time comparison so attackers can't probe the secret one byte at a time.
Β§ 05Event catalog
Today the platform fires four event types. We add more as features ship β subscribe to * if you want forward-compatibility.
| Event | Fires when | Data shape |
|---|---|---|
| message.created | A new message lands on a conversation, regardless of who sent it (customer inbound, agent outbound, REST API send). | { conversationId, message } |
| conversation.updated | A conversation's status (OPEN Β· SNOOZED Β· CLOSED) or its assignedAgentId changes. | { conversationId, status?, assignedAgentId? } |
| audit.event | Any audit-log row is written β agent login, role change, integration created, key revoked, etc. (Enterprise plan.) | { action, entity, entityId, actorId, metadata } |
| webhook.test | You click "Test" in Settings β Webhooks. Signed and shaped exactly like a real event. | { tenantId, test: true } |
Subscribe to * to receive every current and future event. Subscribe to specific names if you want to constrain what your handler has to know about.
Β§ 06Test the integration
The Test button in Settings β Webhooks fires a webhook.test event at your endpoint and reports the upstream HTTP status and elapsed time. Use this to validate your handler before pointing real traffic at it.
The test payload uses exactly the same body envelope, header names, and HMAC scheme as production events β if your verification passes for webhook.test, it will pass for message.created and everything else.
Β§ 07Retries & timeouts
We give your server 10 seconds to respond. Any non-2xx status or network error is a failure. Failed deliveries retry up to 5 times with exponential backoff starting at 2 seconds, so a flaky endpoint gets several chances before we give up on that event.
We do NOT guarantee exactly-once delivery β under network flapping the same event can land twice. Make your handler idempotent: dedupe on the inner data.message.id (or whatever id makes sense for that event).
Β§ 08Security checklist
- Always use https:// β plain http:// endpoints are rejected at creation.
- We block private, loopback, and cloud-metadata IPs at both create and delivery time, so a misconfigured DNS record can't be used to pivot inside your network.
- Compare signatures with a timing-safe equal (Node crypto.timingSafeEqual, Python hmac.compare_digest, Go hmac.Equal).
- Rotate by deleting the endpoint and creating a new one with the same URL. Update your secret store atomically.