Webhooks
ByoTalk sends signed HTTP callbacks to your endpoints. Create endpoints in the dashboard (Environment → Webhooks) or with the API. The signing secret is shown once, when you create or rotate it.
Webhooks
| Method & path | Caller |
|---|---|
POST/GET/PATCH/DELETE /v1/webhooks[/{id}] |
Server, dashboard |
POST /v1/webhooks/{id}/rotate-secret |
Server, dashboard |
POST /v1/webhooks/{id}/test |
Server, dashboard |
GET /v1/webhooks/{id}/deliveries |
Server, dashboard |
POST /v1/webhooks/{id}/deliveries/{deliveryId}/replay |
Server, dashboard |
// POST /v1/webhooks
{ "url": "https://api.example.com/chat-webhooks",
"events": ["message.created", "message.deleted", "member.added", "sink.failing"] }
// 201 (secret shown once)
{ "id": "wh_01J9...", "url": "...", "events": [...], "status": "enabled", "secret": "whsec_MfKQ9r8G..." }
Envelope and headers
POST /chat-webhooks HTTP/1.1
Content-Type: application/json
webhook-id: evt_01J9ZQ...
webhook-timestamp: 1791100000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Signature = base64(HMAC-SHA256(secret_bytes, ${webhook-id}.${webhook-timestamp}.${rawBody})), per Standard Webhooks. Reject timestamps more than 5 minutes off.
{
"id": "evt_01J9ZQ...",
"type": "message.created",
"createdAt": "2026-10-04T10:06:00.120Z",
"env": "env_01J9...",
"data": { ... }
}
Catalogue
| Event | data |
Notes |
|---|---|---|
message.created |
{conversationId, seq, message} |
Full message incl. text |
message.updated |
{conversationId, seq, message} |
seq = event seq; message.seq = original |
message.deleted |
{conversationId, seq, messageId, hard} |
No content |
conversation.created |
{conversation} |
|
conversation.updated |
{conversation, changes} |
|
conversation.deleted |
{conversationId} |
|
member.added |
{conversationId, seq, userIds, actorId} |
|
member.removed |
{conversationId, seq, userId, actorId, reason: removed|left} |
|
message.read |
{conversationId, userId, lastReadSeq} |
Coalesced per member per 10 s |
user.deleted |
{userId, messages: anonymize|delete} |
|
notification.needed |
{conversationId, messageId, recipientIds} |
Webhook-only push mode |
sink.degraded |
{lagSeconds} |
Lag > 5 min |
sink.failing |
{errorClass, since} |
No success > 1 h |
sink.recovered |
{lagSeconds} |
|
call.started |
{call} |
When the call starts ringing; see calls |
call.ended |
{call} |
With endReason and durationSeconds |
Delivery rules
- Success = any 2xx within 10 s. Redirects are not followed.
- Retries: 0, 10 s, 1 min, 10 min, 1 h, 3 h, 6 h, 12 h, 24 h, 24 h (+ jitter).
- Not ordered. Use
data.seqper conversation; dedup bywebhook-id. - Endpoint disabled after 72 h with no success; owner emailed; replay available for 72 h of payloads.
Verifying signatures
Use the official Standard Webhooks library for your language (libraries) or our server SDK. Always verify against the raw request body, before JSON parsing.
// Node / Express — with byotalk/server
app.post("/chat-webhooks", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = chatServer.webhooks.verify(req.body, req.headers); // throws on bad signature or > 5 min skew
handle(event); // dedup on event.id
res.sendStatus(200);
} catch {
res.sendStatus(401);
}
});
// Node — without our SDK (what verify() does)
import crypto from "node:crypto";
function verify(rawBody: Buffer, headers: Record<string, string>, secret: string) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error("stale");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); // base64-decode the part after whsec_
const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
const ok = headers["webhook-signature"].split(" ").some((sig) => {
const value = sig.split(",")[1] ?? "";
return value.length === expected.length && crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
});
if (!ok) throw new Error("bad signature");
return JSON.parse(rawBody.toString("utf8"));
}
Python, Go, PHP, Ruby and others: use the Standard Webhooks library for that language with the same whsec_... secret.