Skip to main content

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 */ }
}
FieldTypeDescription
eventstringEvent name in noun.past-tense-verb form
timestampISO-8601 UTCWhen we generated the event
dataobjectEvent-specific payload — schemas below

Headers on every delivery

HeaderPurpose
X-Voka-EventEvent name (mirrors event field — useful for fast routing)
X-Voka-TimestampUnix 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-256HMAC-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-Timestamp is more than 5 minutes from your server clock.
  • Order. Best-effort, not guaranteed. If you need strict ordering, key on data.call_id and use timestamp for 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"
}
}
Direction values

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
}
}
Field naming on transcript segments

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"
]
}'