Skip to main content

Server integration

Event reference

The events Finn sends and when they fire.

Payload shape

Every delivery has the same top-level shape.

{
  "event": "call.completed",
  "created_at": "2026-09-18T11:14:09.000Z",
  "data": { }
}
FieldTypeNotes
eventstringOne of the events below. Check it before you read data.
created_atISO 8601When Finn built the payload, not when it was delivered.
dataobjectDetails for the event.

Every event except call.completed also has event_id, occurred_at and org_id at the top level. call.completed has no event ID: to remove duplicates, use event plus data.call_uuid as the key. For every other event, use event_id. The event name also arrives in the X-Finn-Event header. See webhook-security to verify signatures and webhook-retries for the delivery schedule.

Event catalog

EventFires when
call.completedA call in your organization has ended, answered or not. For an answered call on a Finn with post-call analysis fields, Finn waits for the analysis first. Each endpoint gets it at most once per call.
contact.createdA new contact was added: in Contacts, as a lead (a form, the leads webhook, added by hand), or by a call or message from a number Finn didn't know. Adding a deleted contact again counts.
contact.assignedA contact got a new owner, or lost its owner. Someone in the app changed it, or an assignment rule, a rep calling an unowned contact or a Sales AI hand-off set it.
contact.stage_changedA contact moved to another stage. Someone changed it, or a call or message moved it (a stage hint or an opt-out).
lead.createdA contact came in as a lead: added by hand, through the leads webhook, from a Sales AI hand-off, or a new contact from a Contacts form. A contact you already had can come in as a lead again, and each such arrival is one event. A form submitted by a contact you already had isn't one.
lead.status_changedA lead's status changed: someone changed it, a first outbound call or message moved a New lead to Contacted, or a won or lost stage moved it.
contact.updatedA contact's name, tags, email, stage, owner, lead status, substatus, close reason or custom fields changed, in the app or through PATCH /api/v1/contacts/{id}. One event per edit.
contact.substatus_changedA lead's substatus changed, in workspaces with substatuses.
contact.section_row_addedA row was added to a contact's section field (a list such as visits or policies), in workspaces with record kinds.
contact.link_changedA link field now points the contact at another record (a doctor, an agent), or at none, in workspaces with record kinds.
document.status_changedA contact's document changed status: missing, received, verified or rejected.
case.createdA Finn Operator case opened.
case.completedA Finn Operator case reached a final status.
approval.requestedA Finn Operator case asked a person for an approval.

You pick the events when you add an endpoint. call.completed is picked by default. An endpoint gets only the events picked when it was added, so an older endpoint doesn't get the newer events.

A Finn's Data webhook URL and Inbound webhook URL can also receive a test request, post_call.test or inbound_call.test, when someone clicks Initialize next to the URL in Deployment settings. It isn't a real event and has a different body, with no data. Return 2xx and ignore it. See webhooks.

call.completed

The event isn't sent the moment the call hangs up. Calls that weren't answered, and calls on a Finn with no analysis fields, are usually sent within about 5 minutes. Answered calls on a Finn with analysis fields are sent as soon as the analysis is saved, usually within about 5 minutes, and after about an hour at the latest with whatever analysis exists. webhooks has the details. Most integrations are built on this event. webhook-post-call has the full payload and handler code. Summary of the data fields:

FieldTypeNotes
call_uuidUUIDThe call. Use it with GET /api/v1/calls/{call_uuid}.
finn_idUUID or nullThe Finn that handled the call.
deployment_idUUID or nullThe deployment the call belongs to: the same id that GET /api/v1/deployments returns. null for calls that don't belong to a deployment, such as calls placed with POST /api/v1/calls.
statusstring or nullThe call's final status.
disconnect_reasonstring or nullWhy the call ended.
directionstring or nullinbound or outbound.
from_number, to_numberstring or nullThe two phone numbers on the call. to_number is in E.164 format.
duration_secondsinteger or nullCall length in whole seconds.
user_sentimentstring or nullOverall sentiment of the person called.
call_successfulboolean or nullWhether the call met its goal, according to analysis.
created_at, updated_atISO 8601 or nullWhen the call record was created and last updated.
analysisarrayOne entry per analysis field, newest first. Empty if no fields are set up. Can also be empty for a call that wasn't answered, or when analysis hadn't arrived after about an hour.

status, disconnect_reason and user_sentiment can take new values at any time. Treat them as strings, not a fixed list.

Contact and lead events

contact.created, contact.assigned, contact.stage_changed, lead.created and lead.status_changed are sent usually within a minute of the change. Use them to keep a CRM, a spreadsheet or a Zapier or Make scenario in step with Finn's Contacts.

{
  "event": "contact.stage_changed",
  "event_id": "crm:stage:6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b:1761563649000:resolved",
  "created_at": "2026-10-27T11:14:11.000Z",
  "occurred_at": "2026-10-27T11:14:09.000Z",
  "org_id": "0b7c6a1e-2f3d-4e5a-8b9c-1d2e3f4a5b6c",
  "data": {
    "contact": {
      "id": "6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b",
      "name": "Ravi Rao",
      "phones": ["+919800000091"],
      "emails": ["[email protected]"],
      "owner": { "id": "a3f9c2d1-5b6e-4c7d-8e9f-0a1b2c3d4e5f", "name": "Asha" },
      "stage": "resolved",
      "stage_kind": "won",
      "lead_status": "converted",
      "lead_source": "Website form",
      "sources": ["lead"],
      "tags": ["expo"],
      "fields": { "company": "Acme" },
      "custom_fields": { "budget": 12000 },
      "created_at": "2026-10-20T09:02:44.000Z",
      "updated_at": "2026-10-27T11:14:09.000Z"
    },
    "change": {
      "from": "follow_up",
      "to": "resolved",
      "by": "person",
      "user": { "id": "a3f9c2d1-5b6e-4c7d-8e9f-0a1b2c3d4e5f", "name": "Asha" }
    }
  }
}
FieldTypeNotes
event_idstringThe same for every delivery of one change. Use it to remove duplicates: an event is sent at least once, and rarely twice.
occurred_atISO 8601When the change happened.
org_idUUIDYour organization.
data.contactobjectThe contact as it is when the event is sent, not when the change happened. A contact merged into another since is sent as the contact it went into.
data.changeobjectcontact.assigned, contact.stage_changed and lead.status_changed only. See below.
data.leadobjectlead.created only: via (how it arrived, such as manual, webhook or form) and source (the lead source, or null).

data.contact fields:

FieldTypeNotes
idUUIDThe contact.
namestring or null
phonesarray of stringsE.164. The main number first, then any others. Empty for a contact with no phone.
emailsarray of stringsLowercase. Empty when there's none.
ownerobject or null{ "id", "name" } of the team member who owns the contact.
stagestringThe stage key, such as new or resolved. Your workspace can add its own.
stage_kindstringopen, won or lost.
lead_statusstring or nullnew, contacted, qualified, converted or lost. null for a contact that isn't a lead.
lead_sourcestring or nullWhere the lead came from.
sourcesarray of stringsHow the contact reached Finn, such as lead, manual or csv.
tagsarray of strings
fieldsobjectThe contact's fields, such as company.
custom_fieldsobjectYour workspace's custom fields, by key.
created_at, updated_atISO 8601

data.change fields:

FieldTypeNotes
from, tostring, object or nullThe stage key or lead status before and after. For contact.assigned, the owner before and after as { "id", "name" }, or null for no owner.
bystringperson when someone in the app made the change, auto when a rule, a call, a message or an agent did.
userobject or null{ "id", "name" } of the person, when by is person.

stage, lead_status, sources, via and by can take new values. Treat them as strings, not a fixed list.

How they're sent:

  • In order per endpoint. An endpoint gets its events other than call.completed one at a time, in the order they happened, so contact.created arrives before the lead.created of the same lead. An event that fails all its attempts doesn't hold up the next.
  • Same signature, attempts and headers as call.completed. Up to 3 attempts, 1 and 2 seconds apart. If all 3 fail, Finn doesn't send that event again. The Deliveries list shows each one.
  • At least once. If Finn can't record your 2xx, it sends the event again later with the same event_id. Remove duplicates on event_id.
  • Not for list uploads. Contacts added by a CSV or list upload, or by an audience import, don't produce contact.created or lead.created. You already have that file. Later changes to them do produce events, including contact.assigned when an assignment rule gives them an owner.
  • Not for deleted contacts. An event whose contact was deleted before it was sent isn't sent. Its delivery shows failed with the error contact_deleted.
  • Not after a day. An event that couldn't be sent within 24 hours is dropped (expired).
  • No backfill. A new endpoint gets the changes made from about 30 seconds after you add it.

More contact events

These have the same body as the contact and lead events above, with data.contact plus one more part.

contact.updated adds data.changed_fields: the sorted keys of what changed, never the values. Keys are name, tags, email, stage, owner, lead_status, substatus, close_reason and custom_fields.<key>. Read the new values in data.contact. An edit that changed nothing sends no event. A stage or owner change also sends contact.stage_changed or contact.assigned; pick only the events you need.

{
  "event": "contact.updated",
  "event_id": "crm:updated:6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b:1762164902000",
  "created_at": "2026-11-03T10:15:04.000Z",
  "occurred_at": "2026-11-03T10:15:02.000Z",
  "org_id": "0b7c6a1e-2f3d-4e5a-8b9c-1d2e3f4a5b6c",
  "data": {
    "contact": { "id": "6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b", "name": "Ravi Rao", "stage": "qualified", "custom_fields": { "budget": 15000 } },
    "changed_fields": ["custom_fields.budget", "stage", "tags"]
  }
}

(data.contact shortened here; it has every field in the table above.)

contact.substatus_changed adds data.substatus: { "stage", "from", "to", "by", "via" }. In workspaces with substatuses, contact.stage_changed also carries data.substatus as { "from", "to" }. Sent for leads only.

contact.section_row_added adds data.section_row: { "field_key", "row_id", "data", "supersedes_id", "meta" }. supersedes_id is the row this one corrects, or null.

contact.link_changed adds data.link: { "field_key", "from_contact_id", "old_to", "new_to" }. old_to and new_to are record IDs or null.

document.status_changed adds data.document:

FieldTypeNotes
typeobject{ "id", "key", "name" } of the document type.
from, tostring or nullmissing, received, verified or rejected. from is null for a new document.
viastringuser, rule, link, form, ai or import.
reasonstringWhen given, such as for a rejection.
file_id, versionThe file, when there is one.
requested_countintegerHow many times the document was asked for.

File names and file contents are never sent.

In workspaces with record kinds, data.contact also has kind, ref_no, substatus and substatus_label. lead.created, lead.status_changed and contact.substatus_changed are sent only for leads.

Finn Operator events

case.created, case.completed and approval.requested follow the same shape and delivery rules. data.contact is the case's contact, or null when the case has none or it was deleted: these events are sent either way.

{
  "event": "approval.requested",
  "event_id": "approval_requested:5d2e8f1a-3b4c-4d6e-9f0a-1b2c3d4e5f6a",
  "created_at": "2026-11-03T09:00:03.000Z",
  "occurred_at": "2026-11-03T09:00:00.000Z",
  "org_id": "0b7c6a1e-2f3d-4e5a-8b9c-1d2e3f4a5b6c",
  "data": {
    "contact": null,
    "approval": {
      "id": "5d2e8f1a-3b4c-4d6e-9f0a-1b2c3d4e5f6a",
      "case_id": "3c9b1f7e-2a4d-4e6b-8c1d-5f7a9b0c2d3e",
      "agent": { "id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e", "name": "Renewals" },
      "proposal_summary": "Email the renewal quote",
      "due_at": "2026-11-04T09:00:00.000Z",
      "link": "https://www.hirefinn.ai/..."
    }
  }
}
Eventevent_idExtra data
case.createdcase_created:<case_id>data.case: { "id", "agent": { "id", "name" } or null, "status", "outcome_key", "title" }.
case.completedcase_completed:<case_id>data.case, same shape, with the final status and outcome_key.
approval.requestedapproval_requested:<approval_id>data.approval: { "id", "case_id", "agent", "proposal_summary", "due_at", "link" }.

Each is sent once per case or approval. To act on an approval, read it with GET /api/v1/approvals/{id} and decide it with an approvals:write key; see api-approvals.

Events that don't exist

Some integrations expect these events. Finn doesn't send them:

EventWhat to use instead
call.startedYour 202 response from POST /api/v1/calls
call.analyzedcall.completed waits for analysis on answered calls, so it already includes analysis
call.transferredNot available
deployment.completedPoll GET /api/v1/deployments/{id}
approval.decidedPoll GET /api/v1/approvals/{id}, or wait for case.completed
contact.deletedNot available
wallet.low_balancePoll GET /api/v1/wallet and read low_balance

What breaks

SymptomCause
A handler expecting type or id at the top level throwsThe fields are event and created_at. There's no top-level id; every event but call.completed has event_id.
A handler built for call.completed throws on another eventCheck event (or the X-Finn-Event header) before you read data, and answer events you don't handle with 200.
A case event has no contactdata.contact is null when the case has none or it was deleted.
A contact change never produces an eventThe endpoint doesn't list that event, the contact isn't a lead (for lead.* and contact.substatus_changed), the contact came from a list upload, the contact was deleted before the event was sent, or all 3 attempts failed.
A call never produces an eventAll 3 delivery attempts failed, or the call ended before you added the endpoint. Older calls aren't backfilled. Check GET /api/v1/calls/{call_uuid}.
analysis is emptyThe Finn has no analysis fields set up, the call wasn't answered, or analysis hadn't arrived after about an hour. GET /api/v1/calls/{call_uuid} returns analysis that arrives later.
Fields are nullThe call record wasn't available when the payload was built. Read the call with GET /api/v1/calls/{call_uuid}.

Related: webhooks, webhook-post-call, webhook-security, webhook-retries, api-webhooks.