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.
Call flows, tools, the knowledge base and variables are configured in the dashboard. They are not readable or writable through this API.
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 |
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?" }
],
"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. |
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. |
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 in the dashboard. A call flow runs instead of the system prompt. | Used, added after identity_text. |
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.
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": [],
"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?" }
],
"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.
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 | 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. |
Error bodies have the shape { "error": "...", "message": "..." }; internal_error has no message. See api-errors.