Webhooks
Akol sends HTTP POST notifications to your endpoint when call events happen. Use webhooks to sync calls into your CRM, trigger automations, or update your own analytics.
Configuring a webhook
Go to Dashboard → Integrations → Webhooks and:
- Click Add Webhook
- Paste your HTTPS endpoint URL
- Pick the events you want to subscribe to
- Save — Akol generates a signing secret. Copy it now (it’s hashed server-side after this).
Your endpoint must:
- Accept
POSTwithContent-Type: application/json - Return
2xxwithin 30 seconds (we don’t wait longer) - Use HTTPS — plain
http://URLs are rejected at save time - Not resolve to a private/internal IP (SSRF protection)
Event types
| Event | When it fires |
|---|---|
call.started | A call connects (inbound or outbound) |
call.ended | A call ends. Includes transcript, summary, sentiment. |
call.transferred | The agent transferred the call to a human |
call.voicemail | Outbound call hit voicemail |
appointment.scheduled | Agent booked an appointment via a function call |
message.received | Inbound SMS to one of your numbers |
lead.created | New lead captured during a call |
Payload shape
{
"event": "call.ended",
"timestamp": "2026-04-18T14:23:11.000Z",
"businessId": "biz_xxxxxxxx",
"webhookId": "whk_xxxxxxxx",
"data": {
"callId": "call_xxxxxxxx",
"campaignId": "cmp_xxxxxxxx",
"direction": "inbound",
"fromNumber": "+15551234567",
"toNumber": "+15557654321",
"agentId": "agt_xxxxxxxx",
"startedAt": "2026-04-18T14:21:42.000Z",
"endedAt": "2026-04-18T14:23:10.000Z",
"duration": 88,
"outcome": "completed",
"summary": "Caller asked about hours and booked a Saturday appointment.",
"sentiment": "positive",
"transcript": [
{ "speaker": "agent", "text": "Thanks for calling — how can I help?", "timestamp": 0 },
{ "speaker": "caller", "text": "What time do you close today?", "timestamp": 2400 },
...
]
}
}The data shape varies by event. See Endpoints → Webhooks for the
exact schema per event type.
Verifying signatures
Every request includes:
X-Webhook-Signature: t=1745000000,v1=<hex-signature>
X-Webhook-Event: call.ended
X-Webhook-Delivery: del_xxxxxxxx
User-Agent: Akol-Voice-Webhooks/1.0Verify with HMAC-SHA256 using your signing secret:
import crypto from "node:crypto";
function verifyAkolWebhook(rawBody, header, secret) {
// header = "t=<timestamp>,v1=<signature>"
const parts = Object.fromEntries(
header.split(",").map(p => p.trim().split("="))
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1, "hex"),
Buffer.from(expected, "hex"),
);
}Important: verify against the raw request body (the bytes you received), NOT a re-serialized JSON object. JSON whitespace differences will break the signature check.
Also reject anything with a timestamp more than 5 minutes from the current time — that prevents replay attacks if a request is intercepted.
Retries
If your endpoint returns non-2xx (or times out):
- Burst retries — 3 attempts with 1s, 5s, 15s backoff, total ~21s.
- Persistent retries — if those exhaust, the delivery moves to a Redis- backed queue with another 3 attempts, 5s/10s/20s backoff up to 1 hour.
After ~1 hour of total retries, the delivery is dropped and a failure record is logged. You can replay it from Dashboard → Webhooks → Logs.
Best practices
- Respond fast. Acknowledge with
200immediately, then enqueue the payload for async processing in your own infrastructure. Don’t do heavy work in the webhook handler. - Be idempotent. We may retry, and network gremlins can occasionally
cause duplicate deliveries. Use the
deliveryID (ordata.callIdfor call events) as your idempotency key. - Whitelist our IPs if your firewall requires it — see the IP allowlist in the dashboard footer (subject to change with notice).
- Test locally with
ngrokor similar — the dashboard’s “Send test event” button lets you fire any event type at your endpoint with a synthetic payload.