Webhooks

Rather than polling, register an HTTPS endpoint and we will post each event to it as it happens, signed so you can prove it came from us.

Events

FieldTypeDescription
call.completedeventA call ended and the transcript and summary are ready.
call.transferredeventThe agent handed the caller to a human.
opportunity.createdeventA property enquiry was scored against the buy box.
opportunity.updatedeventOwner, outcome or note changed.
lead.createdeventA new caller was captured.
lead.updatedeventLead status, owner or note changed.
booking.createdeventA callback or viewing was placed on the calendar.
agent.publishedeventA new agent configuration went live.

Payload

Every delivery has the same shape. data matches the corresponding API object, so the same parsing code works for both.

json
{
  "id": "evt_4c9a1f",
  "type": "opportunity.created",
  "created_at": "2026-09-09T08:46:03Z",
  "workspace_id": "ws_7Kd2",
  "data": {
    "id": "opp_31a",
    "call_id": "call_9f21",
    "address": "14 Hollybank Road, Walsall",
    "postcode": "WS3 2NX",
    "score": 88,
    "outcome": "hot"
  }
}

Verifying the signature

Each request carries X-Call-Agent-Signature and X-Call-Agent-Timestamp. The signature is an HMAC-SHA256 of {timestamp}.{raw body} using your endpoint's signing secret. Compute it over the raw bytes — parsing the JSON first will change the bytes and break the check.

ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, timestamp: string, secret: string) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false; // reject replays older than 5 minutes

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
Reject anything that fails verification with 401 and do not process it. Never log the signing secret.

Retries and duplicates

Respond 2xx within 10 seconds. Anything else is retried with exponential backoff for up to 24 hours. That means an event can arrive more than once, so treat event.id as an idempotency key: record the IDs you have processed and ignore repeats. Order is not guaranteed — compare created_at before overwriting newer state.

Acknowledge first, work afterwards. Queue the payload and return 200 immediately rather than doing slow work inside the request.

Registering an endpoint

bash
curl -X POST https://<your-api-host>/api/v1/webhooks \
  -H "Authorization: Bearer cak_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://crm.example.com/hooks/call-agent",
    "events": ["call.completed", "opportunity.created", "lead.updated"]
  }'

The response includes the signing secret once. Endpoints must be HTTPS and publicly reachable; self-signed certificates are rejected.