Skip to main content

API reference

Finns

Create, read, update and delete voice agents.

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

Overview

A Finn is a voice agent: its instructions, a voice, a language, call settings, an optional transfer number and the questions post-call analysis answers. The Finns API creates, reads, updates and deletes Finns in the organization that owns your API key.

A Finn speaks with the voice in its voice field. Pick one from api-voices: Finn puts the Finn on that voice's provider for you. Speech-to-text and the language model are set by Finn for your organization and are not part of the API.

A Finn created through the API is a draft. It does not take or place calls until you deploy it with api-deployments; deploying sets its status to live, and stopping the deployment sets it back to draft. You can also place a single call with any Finn, draft or live, through api-calls.

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

A Finn's call flow, including the built-in End Call, Call Transfer and Cal.com tools, is read and written through the call_flow field. See Call flow. Knowledge base documents are added and listed with the knowledge base endpoints. Variables are configured in the dashboard only.

Endpoints

MethodPathRate limit (per API key)
GET/finns300 per minute, shared with GET /finns/{finn_id}, GET /calls/{call_uuid} and GET /wallet
GET/finns/{finn_id}Same bucket as above
POST/finns60 per minute, shared by all three write endpoints
PATCH/finns/{finn_id}Same write bucket
DELETE/finns/{finn_id}Same write bucket
POST/finns/{finn_id}/knowledge_base10 per minute
GET/finns/{finn_id}/knowledge_baseSame bucket as GET /finns

The Finn object

Every endpoint that returns a Finn (list, retrieve, create and update) returns the same full object.

{
  "id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
  "name": "Renewal reminders",
  "status": "draft",
  "voice": "21m00Tcm4TlvDq8ikWAM",
  "language": "English",
  "language_group": "global",
  "time_zone": "America/New_York",
  "use_case": "Remind customers about their plan renewal",
  "identity_text": "You are Maya, calling from Acme about the customer's plan renewal.",
  "style_guardrails": "Never quote a price that is not in the prompt.",
  "response_guidelines": "Keep each answer under two sentences.",
  "begin_message": "Hi {{ name }}, this is Maya from Acme. Do you have a minute to talk about your renewal?",
  "system_prompt": "Confirm the customer still wants the plan. If they do, confirm the renewal date. If they want to cancel, offer a transfer to the account team.",
  "max_call_duration_minutes": 5,
  "idle_reminder_message": "Are you still there?",
  "idle_reminder_after_seconds": 10,
  "end_call_on_silence_seconds": 30,
  "handoff_number": { "country_code": "+1", "phone_number": "4155550100" },
  "post_call_analysis": [
    { "id": 1759742062000, "name": "Wants to renew", "type": "Yes/No", "description": "Did the customer confirm the renewal?" }
  ],
  "call_flow": null,
  "created_at": "2026-09-30T09:14:22.000Z",
  "updated_at": "2026-09-30T09:14:22.000Z"
}
FieldTypeNotes
idstringUUID.
namestringDisplay name. Not required to be unique.
statusstringdraft or live. Set by deployments, not writable.
voicestringVoice ID from api-voices.
languagestring or nullLanguage label, for example English.
language_groupstringSet from the voice's provider. Not writable.
time_zonestring or nullIANA time zone, for example Asia/Kolkata.
use_casestring or nullShort description of what the Finn is for.
identity_textstring or nullWho the Finn is.
style_guardrailsstring or nullTone and boundaries.
response_guidelinesstring or nullHow to phrase answers.
begin_messagestring or nullFirst line the Finn speaks on every call.
system_promptstring or nullThe Finn's main instructions: objective, flow and rules. The same text as System prompt in the dashboard editor.
max_call_duration_minutesinteger or nullHard limit on call length. null means Finn's default limit of about 8 minutes.
idle_reminder_messagestring or nullWhat the Finn says when the caller goes quiet.
idle_reminder_after_secondsinteger or nullSeconds of silence before the Finn says the idle reminder.
end_call_on_silence_secondsinteger or nullSeconds of silence after which the Finn hangs up.
handoff_numberobject or nullTransfer number { "country_code": "+91", "phone_number": "9876543210" }.
post_call_analysisarrayQuestions answered after each call. Empty array when there are none. See Post-call analysis questions.
call_flowobject or null{ "enabled": true, "canvas_workflow": { ... } }, or null when the Finn has no call flow. Tool api_key values are returned as "[redacted]". See Call flow.
created_atstringISO 8601.
updated_atstringISO 8601.

Writable fields

POST and PATCH accept only these keys. Any other key returns 400 invalid_request with Unknown field(s): ....

FieldTypeRequired on createRules
namestringYes1 to 100 characters. Can't be null or empty.
voicestringYesA voice_id from api-voices. Up to 200 characters. Can't be null or empty. See Choosing a voice.
languagestringNoUp to 50 characters. Defaults to English.
time_zonestringNoA valid IANA time zone.
use_casestringNoUp to 500 characters.
identity_textstringNoUp to 20,000 characters.
style_guardrailsstringNoUp to 20,000 characters.
response_guidelinesstringNoUp to 20,000 characters.
begin_messagestringNoUp to 1,000 characters. Defaults to Hello, this is a call from Finn.
system_promptstringNoUp to 100,000 characters.
max_call_duration_minutesintegerNoOne of 1, 3, 5, 10, 15, 30, 60.
idle_reminder_messagestringNoUp to 1,000 characters.
idle_reminder_after_secondsintegerNo5 to 600.
end_call_on_silence_secondsintegerNoOne of 15, 20, 30, 45, 60, 90, 120.
handoff_numberobjectNo{ "country_code": "+91", "phone_number": "9876543210" }. country_code is 1 to 4 digits with an optional +; phone_number is 4 to 15 digits without the country code (spaces, dashes and parentheses are stripped).
post_call_analysisarrayNoUp to 50 question objects, each with a non-empty name.
call_flowobjectNo{ "enabled": true, "canvas_workflow": { ... } }. enabled defaults to true. See Call flow.

Strings are trimmed. Send null to clear any field except name and voice; null on post_call_analysis clears the list. On the text fields (language, use_case, identity_text, style_guardrails, response_guidelines, begin_message, system_prompt, idle_reminder_message) an empty string also clears the value. A cleared call setting goes back to Finn's default.

Values that break a rule return 400 invalid_request and message names the field, for example max_call_duration_minutes must be one of 1, 3, 5, 10, 15, 30, 60 or null or idle_reminder_after_seconds must be an integer from 5 to 600 or null.

Choosing a voice

voice must be a voice your organization can use: one returned by GET /voices, or one GET /voices/{voice_id} finds. Finn checks it on every POST, and on every PATCH that sends voice:

ResultResponse
The voice is foundThe Finn is saved and switched to that voice's provider.
No provider your organization can use has that voice400 invalid_request, voice is not one of the voices from GET /voices
The voice catalog couldn't be loaded to check it502 voices_unavailable. Retry later.
Your plan includes no voice provider403 feature_not_allowed, with an action_url to upgrade

If your organization is limited to a voice provider whose voices can't be listed through the API, voice is saved without this check. See api-voices.

Call settings and which calls use them

system_prompt, max_call_duration_minutes, idle_reminder_message, idle_reminder_after_seconds and end_call_on_silence_seconds are the same settings as in the dashboard editor. Calls placed with POST /calls don't use all of them:

FieldCalls from deployments (outbound and inbound)Calls placed with POST /calls
system_promptUsed, unless the Finn has a call flow turned on. A call flow runs instead of the system prompt.Added after identity_text. A call flow that is turned on runs, as on deployment calls.
max_call_duration_minutesUsed. null means about 8 minutes.Not used.
idle_reminder_messageUsed.Not used.
idle_reminder_after_secondsUsed.Not used.
end_call_on_silence_secondsUsed.Not used.

When a setting is null, Finn's default applies. max_call_duration_minutes is a hard stop: when it's reached the call ends without a closing line. See agents-conversation for how the reminder and silence settings behave on a call.

Post-call analysis questions

Each item in post_call_analysis is normalized before it is stored:

KeyRequiredNotes
nameYesThe question, also used as question_name in call analysis results.
typeNoYes/No, Selector, Number or Text. Common aliases are accepted (boolean and yes/no become Yes/No; select, dropdown and choice become Selector; integer and numeric become Number). Anything else, or no type, becomes Text.
descriptionNoGuidance for the analyzer. Defaults to an empty string.
choicesNoArray of strings, kept for Selector questions.
formatExamplesNoArray of strings, or one comma-separated string.
idNoNumber. Generated when missing.

Answers appear in the analysis array of api-calls and of the call.completed webhook.

Call flow

call_flow holds the same call flow the dashboard's canvas edits. When enabled is true and the flow has at least one step, it runs on every call the Finn handles: deployment calls and calls from POST /calls. It runs instead of system_prompt. See agents-tasks.

canvas_workflow has this shape:

KeyTypeRules
entry_nodestringThe id of the first step. Required when there are steps.
nodesarrayThe steps. Each has a unique id and a type.
toolsobjectTool definitions keyed by tool name. Every tool a step uses must be defined here.
settingsobjectOptional. For example { "endCallReasons": ["caller is not interested"] }.
version, max_repeatsOptional. Default to "1.0" and 10.

Step types:

typeKeys
llmA Conversation step. prompt (string), tools (names from tools the step can call), transitions ([{ "to": "<step id>", "when": "<condition>" }], where when: null means always), steer_back (optional string).
toolA terminal step that runs one tool, for example { "id": "bye", "type": "tool", "tool": "end_call" }.

Built-in tools are defined with "builtin": true and their canonical name:

NameConfig
end_callNone. Optional ack: the goodbye line.
call_transfer_to_humanconfig.call_transfer_number: an E.164 number such as +919876543210. Required. Optional ack: the line before the transfer.
check_calendar_slot_availability, book_calendar_slotconfig.api_key: a Cal.com API key, and config.event_type_id: the numeric Cal.com event type. Both required. Optional config.timezone (IANA) and filler.
press_digitNone.

Custom tools ("builtin": false) are accepted in the shape the dashboard saves. See tools-custom.

A call flow that breaks a rule returns 400 invalid_request with message naming the problem, for example call_flow.canvas_workflow.tools.call_transfer_to_human.config.call_transfer_number must be an E.164 number such as "+919876543210".

Rules that match the dashboard:

  • "call_flow": null, or a canvas_workflow with no nodes, removes the call flow. The Finn goes back to system_prompt.
  • "enabled": false keeps the flow saved but turns it off.
  • Responses replace every tool config.api_key with "[redacted]". If you send "[redacted]" back in a PATCH, the stored key for that tool is kept. That way you can edit a flow you read from the API without sending the key again. On a Finn with no stored key for that tool, "[redacted]" returns 400.

A flow that books a demo, transfers on request and otherwise ends the call:

curl -X PATCH https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1 \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "call_flow": {
      "enabled": true,
      "canvas_workflow": {
        "entry_node": "pitch",
        "nodes": [
          {
            "id": "pitch",
            "type": "llm",
            "prompt": "Offer a 20-minute demo. Check the calendar, offer two slots, collect name and email, then book.",
            "tools": ["check_calendar_slot_availability", "book_calendar_slot"],
            "transitions": [
              { "to": "transfer", "when": "the caller asks for a person" },
              { "to": "bye", "when": "the demo is booked or the caller is not interested" }
            ]
          },
          { "id": "transfer", "type": "tool", "tool": "call_transfer_to_human" },
          { "id": "bye", "type": "tool", "tool": "end_call" }
        ],
        "tools": {
          "check_calendar_slot_availability": { "builtin": true, "config": { "api_key": "cal_live_...", "event_type_id": "123456", "timezone": "America/New_York" } },
          "book_calendar_slot": { "builtin": true, "config": { "api_key": "cal_live_...", "event_type_id": "123456", "timezone": "America/New_York" } },
          "call_transfer_to_human": { "builtin": true, "ack": "Connecting you to the team now.", "config": { "call_transfer_number": "+14155550100" } },
          "end_call": { "builtin": true, "ack": "Thanks for your time. Goodbye!" }
        }
      }
    }
  }'

List Finns

GET /finns

Returns Finns in your organization, newest first, as full Finn objects.

Query parameterTypeDefaultNotes
limitinteger501 to 200. Out-of-range values are clamped.
offsetinteger0Number of Finns to skip.
curl "https://api.hirefinn.ai/api/v1/finns?limit=20&offset=0" \
  -H "Authorization: Bearer $FINN_API_KEY"
{
  "success": true,
  "data": [
    {
      "id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
      "name": "Renewal reminders",
      "status": "draft",
      "voice": "21m00Tcm4TlvDq8ikWAM",
      "language": "English",
      "language_group": "global",
      "time_zone": "America/New_York",
      "use_case": "Remind customers about their plan renewal",
      "identity_text": "You are Maya, calling from Acme about the customer's plan renewal.",
      "style_guardrails": null,
      "response_guidelines": null,
      "begin_message": "Hi {{ name }}, this is Maya from Acme. Do you have a minute to talk about your renewal?",
      "system_prompt": "Confirm the customer still wants the plan. If they do, confirm the renewal date.",
      "max_call_duration_minutes": 5,
      "idle_reminder_message": null,
      "idle_reminder_after_seconds": null,
      "end_call_on_silence_seconds": null,
      "handoff_number": null,
      "post_call_analysis": [],
      "call_flow": null,
      "created_at": "2026-09-30T09:14:22.000Z",
      "updated_at": "2026-09-30T09:14:22.000Z"
    }
  ],
  "limit": 20,
  "offset": 0
}

The response has no total count. Keep requesting with a larger offset until a page returns fewer than limit items. See api-pagination.

Retrieve a Finn

GET /finns/{finn_id}

curl https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1 \
  -H "Authorization: Bearer $FINN_API_KEY"

Returns { "success": true, "data": { ... } } with one full Finn object.

Create a Finn

POST /finns

Only name and voice are required.

curl -X POST https://api.hirefinn.ai/api/v1/finns \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renewal reminders",
    "voice": "21m00Tcm4TlvDq8ikWAM",
    "time_zone": "America/New_York",
    "use_case": "Remind customers about their plan renewal",
    "identity_text": "You are Maya, calling from Acme about the customer'\''s plan renewal.",
    "begin_message": "Hi {{ name }}, this is Maya from Acme. Do you have a minute to talk about your renewal?",
    "system_prompt": "Confirm the customer still wants the plan. If they do, confirm the renewal date. If they want to cancel, offer a transfer to the account team.",
    "max_call_duration_minutes": 5,
    "end_call_on_silence_seconds": 30,
    "handoff_number": { "country_code": "+1", "phone_number": "4155550100" },
    "post_call_analysis": [
      { "name": "Wants to renew", "type": "Yes/No", "description": "Did the customer confirm the renewal?" }
    ]
  }'

Response 201 Created with the full Finn object:

{
  "success": true,
  "data": {
    "id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
    "name": "Renewal reminders",
    "status": "draft",
    "voice": "21m00Tcm4TlvDq8ikWAM",
    "language": "English",
    "language_group": "global",
    "time_zone": "America/New_York",
    "use_case": "Remind customers about their plan renewal",
    "identity_text": "You are Maya, calling from Acme about the customer's plan renewal.",
    "style_guardrails": null,
    "response_guidelines": null,
    "begin_message": "Hi {{ name }}, this is Maya from Acme. Do you have a minute to talk about your renewal?",
    "system_prompt": "Confirm the customer still wants the plan. If they do, confirm the renewal date. If they want to cancel, offer a transfer to the account team.",
    "max_call_duration_minutes": 5,
    "idle_reminder_message": null,
    "idle_reminder_after_seconds": null,
    "end_call_on_silence_seconds": 30,
    "handoff_number": { "country_code": "+1", "phone_number": "4155550100" },
    "post_call_analysis": [
      { "id": 1759742062000, "name": "Wants to renew", "type": "Yes/No", "description": "Did the customer confirm the renewal?" }
    ],
    "call_flow": null,
    "created_at": "2026-09-30T09:14:22.000Z",
    "updated_at": "2026-09-30T09:14:22.000Z"
  }
}

{{ name }} in begin_message and system_prompt is filled from each contact's audience row on deployment calls. See agents-variables.

The Finn is created with status draft and owned by the user who created the API key. If that user has since been deleted or is no longer a member of your organization, the create returns 409 api_key_owner_required; create a new key from an active account.

There is no idempotency key. A retried create makes a second Finn.

Update a Finn

PATCH /finns/{finn_id}

Send only the fields to change; at least one is required. Fields you don't send keep their current values, including settings made in the dashboard that the API doesn't expose.

curl -X PATCH https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1 \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice": "a0e99841-438c-4a64-b679-ae501e7d6091",
    "max_call_duration_minutes": null,
    "idle_reminder_message": "Take your time. I just need the renewal date.",
    "idle_reminder_after_seconds": 8
  }'

Returns 200 with the full Finn object, as on create.

Setting voice checks it as described in Choosing a voice and moves the Finn onto that voice's provider, the same as picking a voice in the dashboard.

Live Finns can be edited. The change applies to the next call placed with the Finn, including the next calls dialed by a running deployment. Calls already in progress are not affected. There is no version history in the API, so keep your own copy of text you overwrite.

Delete a Finn

DELETE /finns/{finn_id}

curl -X DELETE https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1 \
  -H "Authorization: Bearer $FINN_API_KEY"
{
  "success": true,
  "data": { "id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1", "deleted": true }
}

Deletion is permanent and only allowed when:

  • the Finn is a draft, and
  • the Finn has never had a deployment (stopped deployments count), and
  • no phone number is assigned to it.

Otherwise the request returns 409. In practice, a Finn that has been deployed cannot be deleted through the API.

Knowledge base

The Finn searches these documents during its calls. The upload path is the same as the dashboard's Knowledge base tab. See agents-knowledge-base.

Add a document

POST /finns/{finn_id}/knowledge_base

Send exactly one source:

SourceBodyNotes
TextJSON { "text": "...", "name": "Refund policy" }Up to 200,000 characters. Stored as a DOCX document. name is optional, up to 200 characters.
WebsiteJSON { "url": "https://example.com/pricing" }One public page, loaded in a browser and stored as a PDF. Links aren't followed. name isn't accepted.
Filemultipart/form-data with a file field, and optionally namePDF, DOCX or TXT, up to 20 MB. TXT is stored as DOCX.
curl -X POST https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1/knowledge_base \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Refund policy", "text": "Refunds are issued within 14 days of purchase. After 14 days we offer store credit." }'
curl -X POST https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1/knowledge_base \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -F "[email protected]"

Response 201 Created:

{
  "success": true,
  "data": { "id": "0b6f4c1e-3a8d-4f5e-9c2b-7d1a6e4f8b30", "name": "Refund policy.docx", "type": "docx", "created_at": "2026-10-17T09:14:22.000Z" }
}

A website import can take up to a minute. Set your client timeout to match.

List documents

GET /finns/{finn_id}/knowledge_base

curl https://api.hirefinn.ai/api/v1/finns/8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1/knowledge_base \
  -H "Authorization: Bearer $FINN_API_KEY"

Returns { "success": true, "data": [ ... ] }, newest first, with each document in the shape above. Documents can't be deleted through the API. Remove them in the dashboard.

Errors

StatuserrorEndpointWhen
400invalid_requestallfinn_id is not a UUID; the body is not a JSON object; an unknown field; a value fails the rules above; a required field is missing on create; an empty PATCH; voice is not a voice your organization can use. message names the problem.
401api_key_requiredallNo Bearer finn_live_... key.
401invalid_api_keyallKey unknown or revoked.
403feature_not_allowedPOST, PATCH with voiceYour plan includes no voice provider. Upgrade at action_url.
404finn_not_foundGET one, PATCH, DELETE, knowledge baseNo Finn with that ID in your organization.
409api_key_owner_requiredPOSTThe user who created the API key has been deleted or is no longer a member of your organization.
409finn_not_draftDELETEThe Finn is live.
409finn_in_useDELETEThe Finn has deployments or an assigned number.
429rate_limitedallOver the bucket's per-minute limit.
500internal_errorallUnexpected server error.
502voices_unavailablePOST, PATCH with voiceThe voice couldn't be checked because the voice catalog couldn't be loaded. Nothing was saved; retry later.
502website_import_failedPOST knowledge base with urlThe page couldn't be loaded.
502knowledge_base_upload_failedPOST knowledge baseThe document couldn't be processed. Nothing was saved.
503knowledge_base_unavailablePOST knowledge baseAdding documents is temporarily disabled. Retry later.

Error bodies have the shape { "error": "...", "message": "..." }; internal_error has no message. See api-errors.