Skip to main content

API reference

CRM (member tokens)

Search, query, edit, follow-ups, forms, documents and imports, acting as one person.

Beta. These endpoints are switched on per organization. While they are off for yours, each one answers as an unknown route (404). Ask Finn support to turn them on.

Overview

The CRM API does what a person does in the Finn CRM: search and filter records, read one record in full, edit fields, keep follow-ups, fill forms, handle documents and import a list. It is the API behind the MCP server's CRM tools.

It acts as one person. Call it with that person's token: a personal MCP key (finn_mcp_...) or an AI app's OAuth access token (finn_oat_...). Each request sees only the records that person can see. An org API key gets 403 user_token_required; org keys use the Records endpoints (/contacts) instead.

Base URL: https://api.hirefinn.ai/api/v1. Auth: Authorization: Bearer finn_mcp_.... If the person belongs to more than one organization, send Finn-Org with the organization's ID. See authentication and scopes.

Responses are the CRM's own objects, with camelCase keys. Errors use the shape on api-errors. A CRM check that refuses a request keeps its own error.code, for example invalid_spec or contact_exists.

Setup and schema

MethodPathScopeWhat it does
GET/crm/setupcrm:readStages, close reasons, fields, record kinds and workspace settings.
GET/crm/filter_schemacrm:readThe fields and operators a query spec may use.

Read both once and cache them. The stage and field keys in every other call come from here.

Finding records

MethodPathScopeWhat it does
POST/crm/records/searchcrm:readFree-text search. Body: q, optional limit.
POST/crm/records/querycrm:readRecords that match a filter spec, sorted by sort and dir.
POST/crm/records/query/countcrm:readHow many records match a spec.
POST/crm/records/query/stage_countscrm:readPer-stage counts for a spec.
GET/crm/duplicatescrm:readGroups of records that look like the same person or company.
POST/crm/askcrm:readA question in plain words (text), answered from the records this person can see.

POST /crm/records/query pages with a cursor. Put limit and cursor in the query string. Pass next_cursor back as cursor with the same body. It is null on the last page. total_count is the number of matching records. See pagination.

One record

MethodPathScopeWhat it does
GET/crm/records/{id}crm:readThe record with its fields, stage and links.
GET/crm/records/{id}/timelinecrm:readNotes, calls, messages, stage moves and other activity, newest first.
GET/crm/records/{id}/stage_gatecrm:readThe checks the record needs before it can move to stage to, and which are done.
POST/crm/recordscrm:writeCreate a record. phone is required. A number the organization already has gets 409.
PATCH/crm/records/{id}crm:writeEdit name, custom fields, tags and links. clearFields empties fields.
POST/crm/records/{id}/notescrm:writeAdd a comment to the timeline. mentions are user IDs to notify.
PUT/crm/records/{id}/stage_checks/{stage}/{check}crm:writeTick ("state": "done") or clear (null) a stage gate check. Checks can't be waived here.
POST/crm/records/{id}/ref_nocrm:writeGive the record its next reference number, if it has none. No body.

Sensitive field values are hidden unless the token also has crm:sensitive:read. To refuse an edit when someone else changed a field first, send the values you expect in expectCustomFields.

PATCH does not move the stage. Use POST /crm/records/{id}/stage_moves, below.

Files and documents

MethodPathScopeWhat it does
GET/crm/records/{id}/filescrm:readFiles on the record: names, types and dates. No download links.
POST/crm/records/{id}/filescrm:docs:writeAttach files (multipart field file), optionally as document type doc_type. Not held.
GET/crm/records/{id}/documentscrm:readThe record's document checklist and each document's status.
POST/crm/records/{id}/documents/{type}/statuscrm:writeMove a checklist document to status to, with an optional reason, note and file.
POST/crm/records/{id}/doc_draftscrm:writeDraft a document for the record from a template (template_id).

Forms

MethodPathScopeWhat it does
GET/crm/formscrm:readThe organization's forms.
GET/crm/records/{id}/forms_checklistcrm:readThe forms this record needs, and which are done.
GET/crm/records/{id}/submissionscrm:read + crm:sensitive:readSubmitted forms with their answers.
GET/crm/records/{id}/form_draftscrm:read + crm:sensitive:readUnsubmitted drafts with their answers.
PUT/crm/records/{id}/forms/{form_id}/draftcrm:write + crm:sensitive:readSave answers to the draft. version refuses the save if the draft changed meanwhile.

Follow-ups and work

MethodPathScopeWhat it does
GET/crm/follow_upscrm:readFollow-ups, soonest due first. Filters: view, status, due, assignee, contact, tz. Cursor pages.
GET/crm/follow_ups/statscrm:readOpen, overdue and due-today counts.
POST/crm/records/{id}/follow_upscrm:writeCreate a follow-up. title is required.
PATCH/crm/follow_ups/{id}crm:writeComplete, snooze, reschedule or reassign. expectDueAt refuses the change if the due date moved.
GET/crm/my_workcrm:readThis person's queue: their records and follow-ups that need action.

Views and reports

MethodPathScopeWhat it does
GET/crm/viewscrm:readSaved views (filters and columns) this person can use.
POST/crm/viewscrm:writeSave a personal view.
GET/crm/summarycrm:readTotals across the pipeline.
GET/crm/stage_countscrm:readHow many records are in each stage.
GET/crm/speedcrm:readResponse times for new leads, for a period.

Writes held for approval

These writes reach outside the CRM or change a lot at once. Finn holds each one for the person to approve. The answer is 202 with "status": "pending_approval" and an approval_id. Nothing runs until the person approves. Once approved, it runs exactly as sent. See approvals.

MethodPathScopeWhat it does
POST/crm/records/{id}/stage_movescrm:stageMove the record to stage. Send closeReason and substatus when it closes. Every stage gate still applies.
POST/crm/records/{id}/forms/{form_id}/submissionscrm:stageSubmit a form as staff, with JSON answers. No files.
POST/crm/records/{id}/forms/{form_id}/linkscrm:shareA link someone outside the organization fills the form with. expires_in_days: 1 to 30.
POST/crm/records/{id}/upload_linkscrm:shareA link someone outside the organization uploads documents with. expires_in_days: 1 to 30.
POST/crm/importscrm:importImport up to 500 rows. A dry run (dry_run, the default) runs at once and writes nothing. A real run is held.
POST/crm/imports/columnscrm:importSuggest which CRM field each CSV header fills. Runs at once.

Holds need the organization's API approvals switched on. With them off, a held write gets 403 approval_unavailable.

Example: move a record to another stage

Check the gate first, so you know the move can pass:

curl "https://api.hirefinn.ai/api/v1/crm/records/5b0e8d2a-7c1f-4e3b-9a6d-2f4c8b1e0a73/stage_gate?to=qualified" \
  -H "Authorization: Bearer $FINN_MCP_KEY"

Then ask for the move:

curl -X POST https://api.hirefinn.ai/api/v1/crm/records/5b0e8d2a-7c1f-4e3b-9a6d-2f4c8b1e0a73/stage_moves \
  -H "Authorization: Bearer $FINN_MCP_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: move-5b0e8d2a-qualified" \
  -d '{ "stage": "qualified" }'

The move is held for the person to approve. The answer (some fields left out):

{
  "status": "pending_approval",
  "approval_id": "3d1f9b2c-6a4e-4f0b-8c7d-1e2f3a4b5c6d",
  "approve_url": "https://www.hirefinn.ai/approvals/holds/3d1f9b2c-6a4e-4f0b-8c7d-1e2f3a4b5c6d",
  "expires_at": "2026-11-08T09:00:00.000Z",
  "action": "contact.stage_move"
}

Show approve_url to the person, or poll GET /approvals/{id} until it is decided. A retry with the same Idempotency-Key returns the same hold.

Rate limits

Reads and writes have separate per-key limits. The current numbers are on rate limits.