Skip to main content

API reference

Records

CRM records: list, read, create, update, timeline and notes.

Beta. Every endpoint on this page is live and documented as shipped. Shapes can still change before general availability.

Overview

Records are the people and things in Finn's CRM: leads, clients and, in workspaces with record kinds, other kinds such as companies or properties. The Records API reads them, creates them, updates a fixed set of fields and adds notes.

Base URL: https://api.hirefinn.ai/api/v1. Auth: Authorization: Bearer finn_live_.... See authentication and scopes.

These endpoints return the object itself, without the { "success": true, "data": ... } envelope of the voice endpoints. Record keys are camelCase (the dashboard's shape); webhooks send records in snake_case.

Every write is recorded in the record's audit log and timeline as made by API key: <key name>.

Endpoints

MethodPathScopeRate limit (per API key)
GET/record_kindscrm:read300 per minute, shared by all reads on this page, api-cases and api-approvals
GET/fieldscrm:readRead bucket
GET/contactscrm:readRead bucket
GET/contacts/{id}crm:readRead bucket
GET/contacts/{id}/timelinecrm:readRead bucket
POST/contactscrm:write120 per minute, shared by these writes and the approval decisions
PATCH/contacts/{id}crm:writeWrite bucket
POST/contacts/{id}/notescrm:writeWrite bucket

{id} is a record ID or the record's phone number.

The record object

{
  "id": "6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b",
  "phone": "+919800000091",
  "phoneKey": "919800000091",
  "name": "Ravi Rao",
  "fields": { "company": "Acme" },
  "sources": ["api"],
  "stage": "new",
  "stageManual": false,
  "assigneeId": null,
  "assigneeName": null,
  "tags": ["expo"],
  "customFields": { "budget": 12000, "pan": { "redacted": true, "set": true, "hint": "••••4821" } },
  "leadStatus": "new",
  "stats": { "count": 0, "connected": 0 },
  "createdAt": "2026-11-03T09:02:44.000Z",
  "updatedAt": "2026-11-03T09:02:44.000Z"
}
FieldTypeNotes
idstringUUID.
phonestringE.164, or "" for a record without a number.
namestring"" when unknown.
emailstringWhen known.
stagestringStage key.
leadStatusstringnew, contacted, qualified, converted or lost. Absent on records that aren't leads.
assigneeId, assigneeNamestring or nullThe owner.
tagsarrayStrings.
sourcesarrayHow the record reached Finn, such as api, lead, csv.
fieldsobjectBuilt-in fields.
customFieldsobjectYour workspace's fields by key (see GET /fields). Sensitive values are hidden without crm:sensitive:read; see scopes.
statsobjectCall counts: count, connected, lastContacted, lastOutcome, lastSentiment.
kind, ref_no, substatus, substatusLabel, substatusAtOnly in workspaces with record kinds or substatuses.
createdAt, updatedAtstringISO 8601.

Records carry a few more display fields (AI temperature, enrichments, and in lists pendingSuggestions and aiScore). Treat unknown keys as optional.

List record kinds

curl https://api.hirefinn.ai/api/v1/record_kinds \
  -H "Authorization: Bearer $FINN_API_KEY"
{ "enabled": true, "kinds": [{ "key": "client", "label": "Client", "label_plural": "Clients", "lead_centric": true, "is_primary": true }] }

enabled is false in a workspace without kinds; kinds then holds only the implicit client kind. Only records of a lead_centric kind have lead status, stages and substatuses.

List fields

curl https://api.hirefinn.ai/api/v1/fields \
  -H "Authorization: Bearer $FINN_API_KEY"

Returns { "fields": [...] }. Each field has key, label, type, options, position, archived, and where they apply default_timezone (datetime fields), config and kinds. Use key in custom_fields.

List records

curl "https://api.hirefinn.ai/api/v1/contacts?stage=new&page_size=100" \
  -H "Authorization: Bearer $FINN_API_KEY"
QueryNotes
qSearch name, number, email and Case ID.
stageA stage key.
tagOne tag.
kindA kind key, or all (default).
pageFrom 1. Default 1.
page_size1 to 100. Default 50.
{ "contacts": [ ... ], "totalCount": 241, "page": 1, "pageSize": 100, "totalPages": 3 }

Every record in your organization is listed, whoever owns it.

Retrieve a record

curl https://api.hirefinn.ai/api/v1/contacts/+919800000091 \
  -H "Authorization: Bearer $FINN_API_KEY"

Returns { "contact": { ... } }, or 404 not_found.

Create a record

curl -X POST https://api.hirefinn.ai/api/v1/contacts \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+919800000091", "name": "Ravi Rao", "custom_fields": { "budget": 12000 } }'
FieldTypeRequiredNotes
phonestringYesAt least 6 digits.
namestringNo
kindstringNoA kind key, in workspaces with kinds.
custom_fieldsobjectNoValues by field key.

Returns 201 with { "contact": { ... } }. The record's source is api.

If a record already has this number, you get 409:

{ "error": "contact_exists", "message": "A record with this number already exists. Change it with PATCH /v1/contacts/{id}.", "contact_id": "6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b" }

So a retry after a timeout is safe: it either creates the record or tells you which one exists.

Update a record

curl -X PATCH https://api.hirefinn.ai/api/v1/contacts/6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stage": "qualified", "tags": ["expo", "hot"], "custom_fields": { "budget": 15000, "old_note": null } }'
FieldTypeNotes
namestringUp to 200 characters.
tagsarrayReplaces the tags.
custom_fieldsobject{ "key": value } sets, { "key": null } clears. Fields you leave out are unchanged.
stagestringA stage key.
substatusstring or nullWorkspaces with substatuses only.
lead_statusstringnew, contacted, qualified, converted or lost.

Any other key returns 400 unsupported_field with the keys in fields. Owner, links and files are changed in Finn.

Returns { "contact": { ... } } after the change.

  • Stage checks. If the move crosses a stage check (a field a person must fill, an approval), you get 409 and the record stays where it is. A key can't override a stage check; a person must clear it in Finn.
  • Required fields are not enforced on API writes.
  • Records that aren't leads. Setting lead_status, stage or substatus on a record whose kind isn't lead-centric returns 400.

Timeline

curl "https://api.hirefinn.ai/api/v1/contacts/6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b/timeline?kind=call,note&limit=20" \
  -H "Authorization: Bearer $FINN_API_KEY"
QueryNotes
kindComma list of call, chat, message, operator, note, form, change, file, view.
channelemail for email only, -email for everything else.
limit1 to 100. Default 50.
beforeThe next value of the previous page.
{ "items": [ { "id": "a:...", "kind": "note", "type": "comment", "at": "2026-11-03T10:15:02.000Z", "text": "Asked for a callback after 6 pm.", "author": "API key: Sales agent" } ], "next": null, "counts": { "all": 1, "call": 0, "chat": 0, "message": 0, "email": 0, "operator": 0, "note": 1, "form": 0, "change": 0 } }

Newest first. next is null on the last page. counts covers the whole timeline, not just this page. Items carry more fields depending on kind (direction, channel, result, links, ...).

Add a note

curl -X POST https://api.hirefinn.ai/api/v1/contacts/6f1c2a0e-8d1b-4b7a-9f3e-2c4d5e6f7a8b/notes \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Asked for a callback after 6 pm.", "client_id": "run-4821-note-1" }'
FieldTypeRequiredNotes
textstringYesCut to 2,000 characters.
client_idstringNoYour ID for this note, up to 100 characters. Sending the same client_id again returns the first note instead of adding another, so retries are safe.

Returns 201 with { "note": { "id", "type": "comment", "text", "at", "author" } }.

Errors

Errors are { "error": "<code>", "message": "..." }. See api-errors.

StatuserrorWhen
400phone_requiredPOST /contacts without a number of at least 6 digits.
400unsupported_fieldPATCH with a key a key can't set.
400invalid_substatusA substatus in a workspace without substatuses.
400message onlyBad stage, kind, lead_status, custom field, timeline kind or channel.
403insufficient_scopeThe key lacks the scope.
404not_foundNo such record in your organization.
409contact_existsPOST /contacts with a number already in use.
409A stage check blocks the move.
429rate_limitedOver the limit. Wait Retry-After seconds.
503schema_missingThe environment is missing a migration. Tell us.

Webhooks

Record changes send contact.created, contact.updated, contact.stage_changed, contact.assigned, lead.status_changed and others to your webhook endpoints. See webhook-events.