Documentation
Webhooks
Receive call events and verify their signature.
Webhooks tell your server what happened on a call, so you do not have to poll. Create an endpoint in the app (Connectors and API) or with POST /v1/webhook-endpoints: you get its signing secret once.
Events
| Event | When |
|---|---|
call.started | A call was answered. |
call.completed | A call ended normally, with transcript and summary. |
call.failed | A call failed or was not answered. |
ask.answered | A person answered an ask. |
ask.expired | An ask expired without an answer. |
contact.created | A contact was created. |
Every event has an id (identical across retries, use it to deduplicate), a type, an apiVersion, a createdAt and a data.object. Payload schemas are in the API reference.
Delivery
Events are POSTed as JSON. Answer with any 2xx status within 10 seconds. Failures are retried with exponential backoff for up to 24 hours. The deliveries of an endpoint, with their status, are listed by GET /v1/webhook-endpoints/{endpointId}/deliveries and in the app, where you can send a test event.
Verify the signature
Every delivery carries the header:
Allomi-Signature: t=1793908800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
t is a Unix timestamp in seconds and v1 is the hexadecimal HMAC-SHA256, with your endpoint secret as the key, of the string t + "." + raw body. To verify:
- Read the raw request body, before any JSON parsing.
- Recompute the HMAC and compare it to
v1in constant time. - Reject the event if
tis more than 5 minutes from now (replay protection).
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyAllomiSignature(secret: string, rawBody: string, header: string | null, toleranceSeconds = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=", 2) as [string, string]));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const got = parts.v1 ?? "";
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
An Express or Hono handler:
app.post("/hooks/allomi", async (c) => {
const raw = await c.req.text();
if (!verifyAllomiSignature(process.env.ALLOMI_WEBHOOK_SECRET!, raw, c.req.header("Allomi-Signature") ?? null)) {
return c.text("invalid signature", 400);
}
const event = JSON.parse(raw);
// deduplicate on event.id, then handle event.type
return c.text("ok");
});
In Python:
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Rotate the secret
POST /v1/webhook-endpoints/{endpointId}/rotate-secret returns a new secret. Update your server right after, as deliveries are signed with the new one immediately.