Webhook Events
Events deliver to the URL you registered via POST /webhook-subscriptions. Every delivery is signed (see HMAC verification) and uses a stable JSON envelope.
Envelope
Every event has the same top-level shape:
{
"event": "call.completed",
"timestamp": "2026-05-10T12:03:35.000Z",
"data": { /* event-specific payload */ }
}
| Field | Type | Description |
|---|---|---|
event | string | Event name in noun.past-tense-verb form |
timestamp | ISO-8601 UTC | When we generated the event |
data | object | Event-specific payload — schemas below |
Headers on every delivery
| Header | Purpose |
|---|---|
X-Voka-Event | Event name (mirrors event field — useful for fast routing) |
X-Voka-Timestamp | Unix timestamp in seconds (10-digit integer string). Note: this is a different format from the timestamp field inside the JSON body (which is ISO-8601). The HMAC is computed against this header value. |
X-Voka-Signature-256 | HMAC-SHA256 hex over ${X-Voka-Timestamp}.${rawBody} |
Delivery semantics
- At-least-once. We retry transient failures; idempotency is your responsibility.
- Auto-disable. A subscription with 10 consecutive delivery failures is auto-disabled. Re-enable from the dashboard.
- Replay protection. Reject deliveries where
X-Voka-Timestampis more than 5 minutes from your server clock. - Order. Best-effort, not guaranteed. If you need strict ordering, key on
data.call_idand usetimestampfor sequencing.
call.completed
Fires after a call ends and all post-processing (duration, cost, quality scoring, billing) is complete.
{
"event": "call.completed",
"timestamp": "2026-05-10T12:03:35.000Z",
"data": {
"call_id": "550e8400-e29b-41d4-a716-446655440001",
"direction": "incoming",
"from": "+12025551111",
"to": "+12025552222",
"duration_seconds": 205,
"cost": 0.0421,
"status": "completed",
"started_at": "2026-05-10T12:00:00Z",
"ended_at": "2026-05-10T12:03:30Z",
"conversation_id": "conv_abc123",
"assistant_id": "550e8400-e29b-41d4-a716-446655440099",
"disconnect_reason": "normal_clearing"
}
}
Webhook payloads use the legacy values incoming / outgoing. The REST API at /api/v1/calls translates these to the friendlier inbound / outbound for input and output. Wrapper authors normalize once at their boundary.
call.insight.received
Fires when AI insights for a call have been processed. One insight group per call; insights[] holds individual results.
{
"event": "call.insight.received",
"timestamp": "2026-05-10T12:04:00.000Z",
"data": {
"call_id": "550e8400-e29b-41d4-a716-446655440001",
"assistant_id": "550e8400-e29b-41d4-a716-446655440099",
"from": "+12025551111",
"to": "+12025552222",
"duration_seconds": 205,
"started_at": "2026-05-10T12:00:00Z",
"insights": [
{ "name": "appointment_booked", "result": true },
{ "name": "topic", "result": "scheduling" },
{ "name": "sentiment", "result": "positive" }
]
}
}
The result type varies by insight kind. Treat it as unknown and branch on name.
call.transcript.ready
Fires once the full transcript is ready and cached on our side. Typically within 30 seconds of call.completed. If processing is slow, the queue retries with exponential backoff for up to ~2 hours.
{
"event": "call.transcript.ready",
"timestamp": "2026-05-10T12:04:10.000Z",
"data": {
"call_id": "550e8400-e29b-41d4-a716-446655440001",
"conversation_id": "conv_abc123",
"assistant_id": "550e8400-e29b-41d4-a716-446655440099",
"duration_seconds": 205,
"language": "en",
"transcript": {
"format": "segments",
"segments": [
{ "role": "assistant", "text": "Hi, this is Acme Dental.", "ts": "2026-05-10T12:00:00Z" },
{ "role": "user", "text": "Hi, I'd like to book a cleaning.", "ts": "2026-05-10T12:00:04Z" }
],
"plain_text": "Assistant: Hi, this is Acme Dental.\nUser: Hi, I'd like to book a cleaning."
},
"truncated": false
}
}
The webhook uses role / ts / plain_text. The REST API at /api/v1/calls/{id}/transcript uses speaker / timestamp / full_text. Same data, two field names — historical reasons. Pick one in your wrapper.
Subscription-gated. Transcript processing only runs when you have an active call.transcript.ready subscription, so subscribing late costs nothing — you'll just miss historical events (use GET /calls/{id}/transcript to backfill).
SMS events (coming soon)
sms.received and sms.sent ship in v1.1 once toll-free messaging verification clears. Schemas will appear here when they go live.
Sample subscription
Sample setup for receiving all three call events:
curl https://voice.vokaai.com/api/v1/webhook-subscriptions \
-H "Authorization: Bearer voka_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/voka",
"events": [
"call.completed",
"call.insight.received",
"call.transcript.ready"
]
}'