Envelope
Every Finn webhook delivery uses the same outer shape. Only data varies by event type.
{
"id": "evt_2H4abc",
"type": "call.completed",
"created_at": "2026-05-22T14:32:14.892Z",
"org_id": "org_8fac17c5",
"data": { }
}
| Field | Type | Notes |
|---|---|---|
id | string | Unique per event. Use it as your dedupe key. Retries reuse the same id. |
type | string | One of the events below. Match on this before touching data. |
created_at | ISO-8601 | When Finn generated the event, not when it was delivered. |
org_id | string | Your organization. |
data | object | Event-specific body. |
Delivery order is best-effort. A call.completed can land before its call.started if the first delivery attempt failed and was retried. Sort by created_at and write handlers that tolerate arriving out of order. See webhook-retries for the retry schedule and webhook-security for signature verification.
Event catalog
| Event | Fires when |
|---|---|
call.started | The carrier reports the call picked up. |
call.completed | The call ended, for any reason, including missed, voicemail, busy, and failed. Delivered within 5 seconds of the call ending. |
call.analyzed | Post-call analysis finished extracting your configured fields. Separate from call.completed because extraction runs after the call record is closed. |
call.transferred | A warm transfer to a human was executed. |
deployment.completed | A campaign finished its audience. |
wallet.low_balance | Your wallet dropped below the threshold configured in the dashboard. |
You subscribe to events per webhook endpoint. A subscription with "events": ["call.completed"] receives nothing else, including call.analyzed. Subscribe to both if you need the extracted fields.
call.completed
This is the event most integrations are built on. Full payload example and the surrounding handler code live in webhooks. The data fields:
| Field | Type | Notes |
|---|---|---|
call_id | string | Globally unique. Stable across call.started, call.completed, call.analyzed. |
deployment_id | string / null | Source campaign. Null for one-off test calls. |
finn_id | string | The agent that handled the call. |
phone_number_id | string | The Finn number involved. |
from / to | E.164 | Pre-translation. WhatsApp calls carry wa_id here instead. |
call_type | enum | inbound | outbound |
channel | enum | pstn | whatsapp | sip |
started_at | ISO-8601 | When Finn began dialing or received the call. |
answered_at | ISO-8601 / null | Null when the call never picked up. |
ended_at | ISO-8601 | |
duration_seconds | int | Billable duration, answered_at to ended_at. |
ring_seconds | int | |
outcome.label | string | Your outcome taxonomy, defined in the prompt. |
outcome.confidence | float 0–1 | Model confidence in the label. |
outcome.extracted_fields | object | Free-form, shaped by your prompt design. |
sentiment | enum | positive | neutral | negative |
csat_score | number / null | |
call_status | enum | completed | no_answer | busy | failed | voicemail |
hangup_party | enum | caller | agent | system |
recording_url | string | Expires after 30 days by default. Mirror to your own storage for archive. |
recording_duration_seconds | int | |
transcript_url | string | JSON transcript. |
credits_charged | float | Wallet debit for this call. |
currency | string | |
monetary_value | float | |
audience_id | string / null | |
audience_row | object / null | The full CSV row dialed. Null for inbound calls. |
metadata | object | Whatever your inbound webhook returned in metadata, echoed back. |
What breaks
| Symptom | Cause |
|---|---|
outcome.label is missing or generic | The agent prompt does not define an outcome taxonomy. Nothing to classify against. |
audience_row is null on calls you expected to have it | The call was inbound. Branch on call_type before reading it. |
duration_seconds is 0 but you were billed a connection | The call rang and was never answered. Check call_status before treating duration as a business signal. |
Extracted fields absent from call.completed | Analysis had not finished when the event fired. They arrive on call.analyzed. |
Fetching recording_url returns 403 | The 30 day window elapsed. |
| Handler runs twice on the same call | You are deduping on call_id rather than id, or not deduping at all. |
Payloads not documented here
The data shape for call.started, call.transferred, deployment.completed, and wallet.low_balance is not yet settled in this reference. Do not infer them from call.completed. To see a real payload for any of them, subscribe an endpoint and read the delivery in Settings → Integrations → Webhooks → Logs in the dashboard, which shows the full request body Finn sent. There is no API endpoint that returns example payloads.
Related: webhooks, webhook-security, webhook-retries, post-call-analysis, call-outcomes, api-webhooks.