Webhooks: real-time call events to your endpoint
Receive call.started, call.ended, call.transcript and call.error events as they happen — with retries, signatures, and the patterns that survive production.
Updated May 6, 2026
Webhooks are the way to react to call events without polling. The platform
posts JSON to your URL, you do the work, you reply with 2xx.
Setup
Set the webhook URL on the agent:
curl -X PATCH https://api.call2me.app/v1/agents/agent_abc123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://your-server.com/webhooks/call2me"}'
Or in the dashboard: Agents → Edit → Webhooks.
Event types
| Event | When it fires | Use it for |
|---|---|---|
call.started | Audio session opens | Mark the call as live in your CRM |
call.transcript | Each completed utterance | Live transcript UI, sentiment scoring |
call.ended | Call finishes (any reason) | Save the recording URL, run extraction |
call.error | Something failed mid-call | Alert ops, surface to the user |
call.transfer | Agent transferred to a human | Update queue state |
Payload shape
Every event has the same envelope:
{
"type": "call.ended",
"call_id": "call_xyz789",
"agent_id": "agent_abc123",
"timestamp": "2026-05-06T14:23:11Z",
"data": {
"duration_ms": 134000,
"status": "completed",
"transcript_url": "https://api.call2me.app/v1/calls/call_xyz789/transcript",
"recording_url": "https://api.call2me.app/v1/calls/call_xyz789/recording.mp3"
}
}
Event-specific fields go inside data. The envelope (type, call_id,
agent_id, timestamp) is stable.
Verifying the signature
Each request carries X-Call2Me-Signature: sha256=<hex>. Compute the
expected signature on the raw body using your webhook secret (visible in
the dashboard once at create time):
import hmac, hashlib
def verify(body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
received = header.replace("sha256=", "")
return hmac.compare_digest(expected, received)
Always use a constant-time comparison (hmac.compare_digest in Python,
crypto.timingSafeEqual in Node).
Retry behavior
If your endpoint returns 5xx or times out (>10s), the platform retries with exponential backoff:
- 1m, 5m, 30m, 2h, 6h, 24h
After 24h of failures the event lands in the dead-letter queue. You can view and replay from Settings → Webhooks → Failed.
4xx responses are NOT retried — they signal a permanent rejection.
Idempotency
Each event has a stable event_id field. Webhooks may be delivered more
than once (rare, but possible during retries that succeeded after a
network blip). Dedupe on event_id:
if not redis.set(f"event:{event_id}", "1", nx=True, ex=86400):
return # already processed
What's next
- Calls — the underlying call object webhooks reference
- Post-Call Actions — declarative alternative to webhooks
- Errors & rate limits — what HTTP codes mean
Frequently asked
Q.How do I configure a webhook URL?
Set webhook_url on the agent via PATCH /v1/agents/{id}. Or, in the dashboard: Agents → Edit → Webhooks tab.
Q.What events does Call2Me send?
call.started, call.ended, call.transcript (per utterance), call.error, and call.transfer. Each event includes a call_id you can use to correlate.
Q.What if my server is down when an event fires?
Webhooks retry on 5xx and timeouts with exponential backoff for up to ~24 hours. Persistent failures land in the dead-letter queue you can replay from the dashboard.
Q.How do I verify the request really came from Call2Me?
Each webhook request carries an HMAC signature in the X-Call2Me-Signature header. Compute SHA-256 over the raw body using your webhook secret and compare in constant time.