Webhooks and signature verification
Events, payload, retries, how to verify the signature in Node.js and Python, and how to rotate the secret without downtime.
Set up an endpoint
In the dashboard, open Developers, then Webhooks, add your URL and choose the events. The page shows the secret used to sign deliveries. INFYNIA calls your URL with a POST request and a JSON body.
Events
Nineteen events exist: call.started, call.ended, call.failed, call.transcript_ready, call.recording_ready, call.analysis_ready, call.custom_event, agent.created, agent.updated, agent.status_changed, agent.alert, contact.created, contact.updated, contact.deleted, knowledge.document_ready, knowledge.document_failed, campaign.started, campaign.completed and campaign.cancelled. The schema of each one is in the webhooks section of the reference.
{
"id": "evt_call_123_call.ended",
"eventType": "call.ended",
"timestamp": "2026-01-15T10:30:00.000Z",
"organizationId": "org_123",
"version": "1.0",
"livemode": true,
"data": { "callId": "call_123", "agentId": "agent_123" }
}idis unique per event and identical on every retry: use it to deduplicate.livemodeisfalsefor activity of test keys and for test events.- Deliveries are at least once: the same event can arrive twice.
Answer and retries
- Answer with any
2xxstatus as soon as you have stored the event. Any other answer, a timeout or a network error counts as a failure. - A failed delivery is retried up to 6 times: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours then 12 hours (7 attempts in total).
- After 20 consecutive failed attempts, the webhook is disabled automatically and the owners and admins are notified by e-mail. Re-enable it in the dashboard once your endpoint is fixed.
- You can replay a delivery from the delivery log of the dashboard.
Verify the signature
Each delivery carries a signature in the X-Webhook-Signature header: the HMAC-SHA256, in hexadecimal, of the raw request body, computed with your secret. Always verify it, using the body exactly as received (before any JSON parsing), and compare in constant time.
# Normal case
X-Webhook-Signature: sha256=<hex>
# During a secret rotation (24 hours): new secret first, then the old one
X-Webhook-Signature: v1=<hex signed with the new secret>,v1=<hex signed with the old secret>During a secret rotation the header holds two signatures, separated by a comma. Accept the delivery when one of them matches one of your secrets.
import crypto from "node:crypto";
import express from "express";
// During a rotation, put the new secret first and the old one second.
const SECRETS = [process.env.INFYNIA_WEBHOOK_SECRET, process.env.INFYNIA_WEBHOOK_SECRET_PREVIOUS].filter(Boolean);
function isValid(rawBody, header) {
const received = (header ?? "")
.split(",")
.map((part) => part.trim().replace(/^(sha256|v1)=/, ""))
.filter(Boolean);
return SECRETS.some((secret) => {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return received.some(
(sig) =>
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
);
});
}
const app = express();
// The signature covers the raw body: do not parse it as JSON before verifying.
app.post("/infynia", express.raw({ type: "application/json" }), (req, res) => {
if (!isValid(req.body, req.get("x-webhook-signature"))) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// Answer fast, process after. Deduplicate on event.id.
res.sendStatus(200);
});The signature does not include a timestamp. To protect against replays, store the id of the events you processed and ignore duplicates.
Rotate the secret without downtime
- In
Developers,Webhooks, rotate the secret. The new secret is shown once. - For 24 hours, deliveries carry both signatures:
v1=<new>,v1=<old>. Your receiver, written as above, accepts both as long as you configure both secrets. - Deploy the new secret in your receiver, then remove the old one after the 24 hours.
A webhook without rotation in progress keeps the simple format sha256=<hex>, so existing receivers keep working.