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

EventWhen
call.startedA call was answered.
call.completedA call ended normally, with transcript and summary.
call.failedA call failed or was not answered.
ask.answeredA person answered an ask.
ask.expiredAn ask expired without an answer.
contact.createdA 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:

  1. Read the raw request body, before any JSON parsing.
  2. Recompute the HMAC and compare it to v1 in constant time.
  3. Reject the event if t is 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.