Beta. Place-a-call and retrieve-a-call are live and documented as shipped. There is no endpoint to list or cancel calls.
Overview
The Calls API places one outbound call and reads the record of a call. A call is tied to a Finn (the agent), which supplies the voice, the prompt, the language and the transfer number. You pass the Finn's ID and a destination number. Everything else comes from that Finn's saved configuration.
Base URL: https://api.hirefinn.ai/api/v1. Auth: Authorization: Bearer finn_live_.... See authentication.
The Finn must belong to the organization that issued your API key. A Finn in another organization returns 404 finn_not_found, the same response as a Finn that does not exist, so the endpoint cannot be used to discover which IDs are in use elsewhere.
To call a list of contacts, create an outbound deployment with api-deployments instead of looping this endpoint. Single calls are meant for event-driven cases such as a form submission, a CRM webhook or a support escalation.
Endpoints
| Method | Path | Rate limit (per API key) |
|---|---|---|
POST | /calls | 60 per minute |
GET | /calls/{call_uuid} | 300 per minute, shared with GET /finns, GET /finns/{finn_id} and GET /wallet |
Place a call
POST /calls
curl -X POST https://api.hirefinn.ai/api/v1/calls \
-H "Authorization: Bearer $FINN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"finn_id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
"to_number": "4155550142",
"country_code": "1"
}'
| Field | Type | Required | Notes |
|---|---|---|---|
finn_id | string | yes | UUID of the Finn to run. Must belong to your organization. |
to_number | string | yes | Destination number. Digits with an optional leading +; spaces, dashes and parentheses are allowed. If it starts with +, it is used as a full international number. |
country_code | string | yes | Calling code such as "1" or "+91". Prepended to to_number when to_number has no leading +. Send it even when to_number starts with +: without it the request returns 400 invalid_request. |
Any other field in the body, including metadata, is ignored.
Which Finn settings apply. The call uses the Finn's voice, language, begin_message, identity_text, system_prompt (added after identity_text), style_guardrails, response_guidelines and handoff_number. It does not use max_call_duration_minutes, idle_reminder_message, idle_reminder_after_seconds or end_call_on_silence_seconds: those apply only to calls from deployments, and a single call runs with Finn's defaults for them. See Call settings and which calls use them.
Caller ID. Every call placed with this endpoint comes from the same Finn-owned Indian number (+91), whatever the destination country. You can't choose or change it, and you can't place the call from a number you rented. Outside India, the person you call sees an international Indian number rather than a local one, which can lower answer rates. To show a local caller ID, rent a number in that country (api-phone-numbers) and call through an outbound deployment that uses it (api-deployments).
There is no idempotency key. A retried POST places a second real call, so retry only when you know the first request did not reach the server.
The response is 202 Accepted once Finn has accepted the call for dialing:
{
"success": true,
"data": {
"call_uuid": "b7e3c9a2-4d1f-4e8b-9a6c-2f5d8e1b3c70",
"finn_id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
"to_number": "4155550142",
"status": "initiated"
}
}
to_number echoes what you sent, trimmed. call_uuid is the ID for every later lookup.
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_request | finn_id is not a UUID, to_number is missing or not a phone number, or country_code is missing or not 1–4 digits. |
| 401 | api_key_required | No Bearer finn_live_... key in the Authorization header. |
| 401 | invalid_api_key | The key is unknown or revoked. |
| 402 | insufficient_credits | The wallet does not hold enough credits to start a call. Top up in the dashboard. |
| 404 | finn_not_found | No Finn with that ID in your organization. |
| 429 | rate_limited | More than 60 calls per minute on this key. |
| 500 | internal_error | Unexpected server error. |
| 502 | call_failed | The call couldn't be placed, or Finn didn't get a call ID back. |
Error bodies have the shape { "error": "...", "message": "..." }; internal_error has no message. See api-errors.
Retrieve a call
GET /calls/{call_uuid}
Returns any call in your organization: calls placed with POST /calls, calls dialed by deployments, and inbound calls.
curl https://api.hirefinn.ai/api/v1/calls/b7e3c9a2-4d1f-4e8b-9a6c-2f5d8e1b3c70 \
-H "Authorization: Bearer $FINN_API_KEY"
{
"success": true,
"data": {
"call_uuid": "b7e3c9a2-4d1f-4e8b-9a6c-2f5d8e1b3c70",
"finn_id": "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
"deployment_id": null,
"status": "completed",
"disconnect_reason": "hangup",
"direction": "outbound",
"from_number": "+918045550199",
"to_number": "+14155550142",
"duration_seconds": 97,
"user_sentiment": "positive",
"call_successful": true,
"created_at": "2026-09-16T09:14:02.000Z",
"updated_at": "2026-09-16T09:15:41.000Z",
"recording_url": "https://storage.example.com/recordings/b7e3c9a2-4d1f-4e8b-9a6c-2f5d8e1b3c70.wav?sig=...",
"billing": {
"credits_charged": 2,
"price_per_credit": 7,
"currency": "INR",
"estimated_cost": 14
},
"analysis": [
{
"question_name": "interested",
"question_type": "Yes/No",
"answer": "Yes",
"needs_review": false,
"reasoning": "Caller asked for a follow-up call on Tuesday.",
"extracted_at": "2026-09-16T09:16:10.000Z"
}
]
}
}
The call object
| Field | Type | Notes |
|---|---|---|
call_uuid | string | UUID of the call. |
finn_id | string or null | The Finn that handled the call. |
deployment_id | string or null | The deployment the call belongs to: the same id that GET /deployments returns, and the same value as deployment_id in the call.completed webhook. null for calls that don't belong to a deployment, such as calls placed with POST /calls. |
status | string | See Call status. |
disconnect_reason | string or null | Why the call ended. Free-form. |
direction | string or null | outbound or inbound. |
from_number | string or null | Caller number. For calls placed with POST /calls, always Finn's Indian caller ID. |
to_number | string or null | Called number, in E.164 format. |
duration_seconds | integer or null | Talk time, rounded from milliseconds. Null when no duration was recorded. |
user_sentiment | string or null | Set by post-call analysis. Null until analysis runs. |
call_successful | boolean or null | Set by post-call analysis. Null until analysis runs. |
created_at | string | ISO 8601. |
updated_at | string | ISO 8601. |
recording_url | string or null | Signed link to the recording, valid for 1 hour. Read the call again for a fresh link. Null when recording was off, the call wasn't answered, or the recording is past your retention window. |
billing | object or null | Estimated charge, below. Null if your rate could not be looked up. |
analysis | array | Post-call analysis answers, newest first. Empty until analysis completes. |
billing is an estimate computed when you read the call. It is ceil(duration_seconds / 60) credits at your organization's current price_per_credit, so a 10-second call shows one credit. It does not apply plan multipliers or unanswered-call charges, and nothing is charged by reading it. The wallet ledger is the record of what was actually debited: see GET /wallet/transactions in api-wallet.
billing field | Type | Notes |
|---|---|---|
credits_charged | integer | ceil(duration_seconds / 60), or 0 with no duration. |
price_per_credit | number | Your effective rate. |
currency | string | Currency of price_per_credit. |
estimated_cost | number | credits_charged × price_per_credit. |
Each analysis item has question_name, question_type, answer, needs_review (boolean), reasoning and extracted_at. The questions are the Finn's post_call_analysis configuration; see api-finns. Analysis runs after the call ends, so an empty array on a call that has just finished is expected.
The call record isn't created by POST /calls itself. It appears after the call ends, usually within about 5 minutes. Until then, a GET returns 404 call_not_found. Retry with a backoff, or wait for the call.completed webhook.
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_request | call_uuid is not a UUID. |
| 401 | api_key_required / invalid_api_key | Missing, unknown or revoked key. |
| 404 | call_not_found | No call with that ID in your organization, or the record has not been written yet. |
| 429 | rate_limited | More than 300 requests per minute in this key's read bucket. |
| 500 | internal_error | Unexpected server error. |
Call status
| Status | Meaning |
|---|---|
initiated | Returned by POST /calls only: Finn accepted the call for dialing. |
pending | Queued, not yet dialed. |
calling / ringing | Being dialed. |
answered | Picked up. |
completed | Ended normally. Analysis follows. |
voicemail | Reached voicemail. |
no_answer | Rang out with no pickup. |
busy | The carrier returned busy. |
failed | Could not be placed or dropped with an error. |
canceled / cancelled | Canceled before it connected. Both spellings occur. |
Status values come from the carrier and from how the call went, so this is the common set, not a closed enum. Compare case-insensitively and handle unknown values.
Rate limits
Limits are counted per API key, not per IP. Over the limit returns 429 with {"error": "rate_limited", "message": "..."}, a Retry-After header, and RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers on every response. See api-rate-limits.
TypeScript example
const res = await fetch("https://api.hirefinn.ai/api/v1/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FINN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
finn_id: "8f14e45f-ceea-467a-9f6a-1c0e5b2a77d1",
to_number: "+14155550142",
country_code: "1",
}),
});
const body = await res.json();
if (!res.ok) {
throw new Error(`${res.status} ${body.error}: ${body.message ?? ""}`);
}
console.log(body.data.call_uuid, body.data.status);
Getting results
Pull a finished call with GET /calls/{call_uuid}, or register a webhook endpoint and receive a signed call.completed event after the call ends, usually within about 5 minutes. Calls placed with POST /calls and calls nobody answered get the event too. See api-webhooks to register an endpoint, webhook-post-call for the payload and webhook-security to verify signatures.
Not available
There is no endpoint to list calls or cancel a call, and metadata is not stored or returned. For call history in bulk, use the export in the dashboard.