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
| Method | Path | Rate limit (per API key) |
|---|---|---|
GET | /finns | 300 per minute, shared with GET /finns/{finn_id}, GET /calls/{call_uuid} and GET /wallet |
GET | /finns/{finn_id} | Same bucket as above |
POST | /finns | 60 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_base | 10 per minute |
GET | /finns/{finn_id}/knowledge_base | Same 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"
}
| Field | Type | Notes |
|---|---|---|
id | string | UUID. |
name | string | Display name. Not required to be unique. |
status | string | draft or live. Set by deployments, not writable. |
voice | string | Voice ID from api-voices. |
language | string or null | Language label, for example English. |
language_group | string | Set from the voice's provider. Not writable. |
time_zone | string or null | IANA time zone, for example Asia/Kolkata. |
use_case | string or null | Short description of what the Finn is for. |
identity_text | string or null | Who the Finn is. |
style_guardrails | string or null | Tone and boundaries. |
response_guidelines | string or null | How to phrase answers. |
begin_message | string or null | First line the Finn speaks on every call. |
system_prompt | string or null | The Finn's main instructions: objective, flow and rules. The same text as System prompt in the dashboard editor. |
max_call_duration_minutes | integer or null | Hard limit on call length. null means Finn's default limit of about 8 minutes. |
idle_reminder_message | string or null | What the Finn says when the caller goes quiet. |
idle_reminder_after_seconds | integer or null | Seconds of silence before the Finn says the idle reminder. |
end_call_on_silence_seconds | integer or null | Seconds of silence after which the Finn hangs up. |
handoff_number | object or null | Transfer number { "country_code": "+91", "phone_number": "9876543210" }. |
post_call_analysis | array | Questions answered after each call. Empty array when there are none. See Post-call analysis questions. |
call_flow | object 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_at | string | ISO 8601. |
updated_at | string | ISO 8601. |
Writable fields
POST and PATCH accept only these keys. Any other key returns 400 invalid_request with Unknown field(s): ....
| Field | Type | Required on create | Rules |
|---|---|---|---|
name | string | Yes | 1 to 100 characters. Can't be null or empty. |
voice | string | Yes | A voice_id from api-voices. Up to 200 characters. Can't be null or empty. See Choosing a voice. |
language | string | No | Up to 50 characters. Defaults to English. |
time_zone | string | No | A valid IANA time zone. |
use_case | string | No | Up to 500 characters. |
identity_text | string | No | Up to 20,000 characters. |
style_guardrails | string | No | Up to 20,000 characters. |
response_guidelines | string | No | Up to 20,000 characters. |
begin_message | string | No | Up to 1,000 characters. Defaults to Hello, this is a call from Finn. |
system_prompt | string | No | Up to 100,000 characters. |
max_call_duration_minutes | integer | No | One of 1, 3, 5, 10, 15, 30, 60. |
idle_reminder_message | string | No | Up to 1,000 characters. |
idle_reminder_after_seconds | integer | No | 5 to 600. |
end_call_on_silence_seconds | integer | No | One of 15, 20, 30, 45, 60, 90, 120. |
handoff_number | object | No | { "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_analysis | array | No | Up to 50 question objects, each with a non-empty name. |
call_flow | object | No | { "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:
| Result | Response |
|---|---|
| The voice is found | The Finn is saved and switched to that voice's provider. |
| No provider your organization can use has that voice | 400 invalid_request, voice is not one of the voices from GET /voices |
| The voice catalog couldn't be loaded to check it | 502 voices_unavailable. Retry later. |
| Your plan includes no voice provider | 403 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:
| Field | Calls from deployments (outbound and inbound) | Calls placed with POST /calls |
|---|---|---|
system_prompt | Used, 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_minutes | Used. null means about 8 minutes. | Not used. |
idle_reminder_message | Used. | Not used. |
idle_reminder_after_seconds | Used. | Not used. |
end_call_on_silence_seconds | Used. | 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:
| Key | Required | Notes |
|---|---|---|
name | Yes | The question, also used as question_name in call analysis results. |
type | No | Yes/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. |
description | No | Guidance for the analyzer. Defaults to an empty string. |
choices | No | Array of strings, kept for Selector questions. |
formatExamples | No | Array of strings, or one comma-separated string. |
id | No | Number. 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:
| Key | Type | Rules |
|---|---|---|
entry_node | string | The id of the first step. Required when there are steps. |
nodes | array | The steps. Each has a unique id and a type. |
tools | object | Tool definitions keyed by tool name. Every tool a step uses must be defined here. |
settings | object | Optional. For example { "endCallReasons": ["caller is not interested"] }. |
version, max_repeats | Optional. Default to "1.0" and 10. |
Step types:
type | Keys |
|---|---|
llm | A 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). |
tool | A 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:
| Name | Config |
|---|---|
end_call | None. Optional ack: the goodbye line. |
call_transfer_to_human | config.call_transfer_number: an E.164 number such as +919876543210. Required. Optional ack: the line before the transfer. |
check_calendar_slot_availability, book_calendar_slot | config.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_digit | None. |
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 acanvas_workflowwith nonodes, removes the call flow. The Finn goes back tosystem_prompt."enabled": falsekeeps the flow saved but turns it off.- Responses replace every tool
config.api_keywith"[redacted]". If you send"[redacted]"back in aPATCH, 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]"returns400.
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 parameter | Type | Default | Notes |
|---|---|---|---|
limit | integer | 50 | 1 to 200. Out-of-range values are clamped. |
offset | integer | 0 | Number 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:
| Source | Body | Notes |
|---|---|---|
| Text | JSON { "text": "...", "name": "Refund policy" } | Up to 200,000 characters. Stored as a DOCX document. name is optional, up to 200 characters. |
| Website | JSON { "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. |
| File | multipart/form-data with a file field, and optionally name | PDF, 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
| Status | error | Endpoint | When |
|---|---|---|---|
| 400 | invalid_request | all | finn_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. |
| 401 | api_key_required | all | No Bearer finn_live_... key. |
| 401 | invalid_api_key | all | Key unknown or revoked. |
| 403 | feature_not_allowed | POST, PATCH with voice | Your plan includes no voice provider. Upgrade at action_url. |
| 404 | finn_not_found | GET one, PATCH, DELETE, knowledge base | No Finn with that ID in your organization. |
| 409 | api_key_owner_required | POST | The user who created the API key has been deleted or is no longer a member of your organization. |
| 409 | finn_not_draft | DELETE | The Finn is live. |
| 409 | finn_in_use | DELETE | The Finn has deployments or an assigned number. |
| 429 | rate_limited | all | Over the bucket's per-minute limit. |
| 500 | internal_error | all | Unexpected server error. |
| 502 | voices_unavailable | POST, PATCH with voice | The voice couldn't be checked because the voice catalog couldn't be loaded. Nothing was saved; retry later. |
| 502 | website_import_failed | POST knowledge base with url | The page couldn't be loaded. |
| 502 | knowledge_base_upload_failed | POST knowledge base | The document couldn't be processed. Nothing was saved. |
| 503 | knowledge_base_unavailable | POST knowledge base | Adding documents is temporarily disabled. Retry later. |
Error bodies have the shape { "error": "...", "message": "..." }; internal_error has no message. See api-errors.