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
| Method | Path | Scope | Rate limit (per API key) |
|---|---|---|---|
GET | /record_kinds | crm:read | 300 per minute, shared by all reads on this page, api-cases and api-approvals |
GET | /fields | crm:read | Read bucket |
GET | /contacts | crm:read | Read bucket |
GET | /contacts/{id} | crm:read | Read bucket |
GET | /contacts/{id}/timeline | crm:read | Read bucket |
POST | /contacts | crm:write | 120 per minute, shared by these writes and the approval decisions |
PATCH | /contacts/{id} | crm:write | Write bucket |
POST | /contacts/{id}/notes | crm:write | Write 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"
}
| Field | Type | Notes |
|---|---|---|
id | string | UUID. |
phone | string | E.164, or "" for a record without a number. |
name | string | "" when unknown. |
email | string | When known. |
stage | string | Stage key. |
leadStatus | string | new, contacted, qualified, converted or lost. Absent on records that aren't leads. |
assigneeId, assigneeName | string or null | The owner. |
tags | array | Strings. |
sources | array | How the record reached Finn, such as api, lead, csv. |
fields | object | Built-in fields. |
customFields | object | Your workspace's fields by key (see GET /fields). Sensitive values are hidden without crm:sensitive:read; see scopes. |
stats | object | Call counts: count, connected, lastContacted, lastOutcome, lastSentiment. |
kind, ref_no, substatus, substatusLabel, substatusAt | Only in workspaces with record kinds or substatuses. | |
createdAt, updatedAt | string | ISO 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"
| Query | Notes |
|---|---|
q | Search name, number, email and Case ID. |
stage | A stage key. |
tag | One tag. |
kind | A kind key, or all (default). |
page | From 1. Default 1. |
page_size | 1 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 } }'
| Field | Type | Required | Notes |
|---|---|---|---|
phone | string | Yes | At least 6 digits. |
name | string | No | |
kind | string | No | A kind key, in workspaces with kinds. |
custom_fields | object | No | Values 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 } }'
| Field | Type | Notes |
|---|---|---|
name | string | Up to 200 characters. |
tags | array | Replaces the tags. |
custom_fields | object | { "key": value } sets, { "key": null } clears. Fields you leave out are unchanged. |
stage | string | A stage key. |
substatus | string or null | Workspaces with substatuses only. |
lead_status | string | new, 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
409and 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,stageorsubstatuson a record whose kind isn't lead-centric returns400.
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"
| Query | Notes |
|---|---|
kind | Comma list of call, chat, message, operator, note, form, change, file, view. |
channel | email for email only, -email for everything else. |
limit | 1 to 100. Default 50. |
before | The 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" }'
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | Yes | Cut to 2,000 characters. |
client_id | string | No | Your 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.
| Status | error | When |
|---|---|---|
| 400 | phone_required | POST /contacts without a number of at least 6 digits. |
| 400 | unsupported_field | PATCH with a key a key can't set. |
| 400 | invalid_substatus | A substatus in a workspace without substatuses. |
| 400 | message only | Bad stage, kind, lead_status, custom field, timeline kind or channel. |
| 403 | insufficient_scope | The key lacks the scope. |
| 404 | not_found | No such record in your organization. |
| 409 | contact_exists | POST /contacts with a number already in use. |
| 409 | A stage check blocks the move. | |
| 429 | rate_limited | Over the limit. Wait Retry-After seconds. |
| 503 | schema_missing | The 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.