How errors are returned
A failed request returns an HTTP status code and a flat JSON body. error is a stable code that your code can check. message explains the problem to a person.
{
"error": "invalid_request",
"message": "finn_id must be a valid UUID"
}
| Field | Type | Notes |
|---|---|---|
error | string | Code to check in your code. The tables below list every value. |
message | string | Human-readable explanation. It can change at any time, so don't parse it. internal_error responses have no message. |
Some errors include extra fields:
| Field | On | Meaning |
|---|---|---|
details | 400 invalid_request from audience contact endpoints | Contacts that failed validation, up to 50 entries. |
deployment_id | 409 deployment_already_active, 409 audience_in_use | The live deployment that caused the conflict. |
errors | 422 deployment_rejected | The specific reasons the deployment was rejected, when Finn has them. |
required_credits, available_credits | 402 insufficient_credits on POST /phone_numbers | How many credits the rental needs and how many you have. |
transaction_id | 500 rent_failed | The wallet transaction that was refunded. |
action_url | Errors that need setup on the website (see below) | The dashboard page where you fix the problem. |
Setup that happens on the website
Compliance, billing and API key management are done in the Finn dashboard, not through the API. When a request fails because one of them isn't done, the error includes action_url pointing at the page to finish it. Retry the request once that's done.
{
"error": "compliance_required",
"message": "Compliance approval is required. Complete the compliance application in the Finn dashboard (see action_url), then retry.",
"action_url": "https://www.hirefinn.ai/dashboard/settings?tab=telephony-account"
}
error | What to do on the website | action_url |
|---|---|---|
compliance_required | Submit the compliance application under Settings → Compliance and wait for approval. Required before renting numbers, and before searching India (IN) or Exotel inventory. | https://www.hirefinn.ai/dashboard/settings?tab=telephony-account (opens the Compliance tab) |
insufficient_credits | Top up the wallet. | https://www.hirefinn.ai/dashboard/settings?tab=billing |
feature_not_allowed | Upgrade the plan to include a voice provider. | https://www.hirefinn.ai/dashboard/settings?tab=billing |
api_key_owner_required | The user who created the key left the organization or was deleted. Sign in as an active member and create a new key. | https://www.hirefinn.ai/dashboard/settings?tab=integrations |
Team members and roles are also managed only in the dashboard (Settings → Team). Show message and action_url to the person running your integration rather than retrying automatically. These errors don't go away until someone acts.
Responses don't include a request ID. If you contact support, include the method, path, time (UTC) and the full response body.
Status codes
| Status | Meaning | Retry? |
|---|---|---|
400 | The request is malformed: a missing or invalid field, an invalid UUID, or a bad query value. | No. Fix the request. |
401 | The API key is missing, malformed, unknown or revoked. A body that isn't valid JSON gets 400 instead, even without a key; see Errors outside the normal format. | No. See authentication. |
402 | The wallet balance is too low for this action. | Only after you top up the wallet. |
403 | Your plan or compliance status doesn't allow this action, or the request came from a browser origin Finn doesn't allow. | No. |
404 | The resource doesn't exist or belongs to another organization. | No. |
409 | The request conflicts with the current state, for example a deployment that's already running or a number that's in use. | Not until that state changes. |
422 | The request is valid but can't be carried out, for example an audience with no contacts uploaded. | No. |
429 | You hit the rate limit for this key and endpoint group. | Yes, after Retry-After. See api-rate-limits. |
500 | Unexpected error on Finn's side. | Reads, yes. Writes, see api-idempotency. |
502 | Finn couldn't complete the request: placing the call, starting or stopping the deployment, the carrier or the voice provider failed or didn't respond. | Reads, yes. Writes, see api-idempotency. |
A 404 for a resource you're sure exists usually means the API key belongs to a different organization.
Error codes
Every endpoint
| Status | error | Cause |
|---|---|---|
400 | invalid_request | A field, path ID or query value failed validation. message names the problem. |
401 | api_key_required | No Authorization header, or the token doesn't start with finn_live_. |
401 | invalid_api_key | The key is unknown or has been revoked. |
403 | origin_not_allowed | The request has a browser Origin header that Finn doesn't allow. The API is for server-to-server use: call it from your backend, not from a web page. |
429 | rate_limited | Too many requests for this key and endpoint group. |
500 | internal_error | Unexpected failure. No message. |
Calls
| Status | error | Endpoint | Cause |
|---|---|---|---|
404 | finn_not_found | POST /calls | No Finn with that finn_id in your organization. |
402 | insufficient_credits | POST /calls | The wallet balance is too low to place the call. |
502 | call_failed | POST /calls | The call couldn't be placed, or Finn didn't get a call_uuid back. See api-idempotency before you retry. |
404 | call_not_found | GET /calls/{call_uuid} | No call with that ID in your organization. |
Finns and voices
| Status | error | Endpoint | Cause |
|---|---|---|---|
404 | finn_not_found | GET, PATCH, DELETE /finns/{finn_id} | No Finn with that ID in your organization. |
403 | feature_not_allowed | POST /finns, PATCH /finns/{finn_id} with voice, GET /voices, GET /voices/{voice_id} | Your plan includes no voice provider. |
409 | api_key_owner_required | POST /finns | The user who created this key has been deleted or is no longer in the organization. Create a new key. |
409 | finn_not_draft | DELETE /finns/{finn_id} | Only draft Finns can be deleted. |
409 | finn_in_use | DELETE /finns/{finn_id} | The Finn is assigned a number or is used by deployments. |
400 | invalid_request | POST /finns, PATCH /finns/{finn_id} with voice | voice is not a voice your organization can use. message is voice is not one of the voices from GET /voices. |
404 | voice_not_found | GET /voices/{voice_id} | None of your organization's voice providers has a voice with that ID. |
502 | voices_unavailable | GET /voices, GET /voices/{voice_id}, POST /finns, PATCH /finns/{finn_id} with voice | The voice catalog couldn't be loaded, so the voice couldn't be listed or checked. Retry later. On GET /voices, also returned when your organization is limited to a voice provider whose voices can't be listed through the API; retrying doesn't help then. |
Deployments
| Status | error | Endpoint | Cause |
|---|---|---|---|
409 | api_key_owner_required | POST /deployments, POST /deployments/inbound | The key has no creator, or its creator is no longer in the organization. Create a new key. POST /finns returns the same error; see Finns and voices. |
404 | finn_not_found | create and inbound stop | No Finn with that ID in your organization. |
404 | phone_number_not_found | POST /deployments, POST /deployments/inbound | No usable number with that phone_number_id in your organization. |
409 | phone_number_isolated | POST /deployments, POST /deployments/inbound | The number was isolated after a spam check. |
409 | phone_number_in_use | POST /deployments, POST /deployments/inbound | The number is held by another deployment that has not ended. |
404 | audience_not_found | POST /deployments | No active audience with that ID in your organization. |
422 | audience_not_supported | POST /deployments | Dynamic audiences can't be deployed through the API. |
422 | audience_not_ready | POST /deployments | The audience has no uploaded contact list yet, or its contact list has no contacts. |
422 | schedule_not_supported | POST /deployments | Scheduled deployments aren't available yet. Leave out schedule. |
409 | deployment_already_active | POST /deployments, POST /deployments/inbound | This Finn already has a live deployment. The response includes its deployment_id. |
402 | insufficient_credits | POST /deployments | The wallet balance is too low. |
422 | deployment_rejected | POST /deployments, POST /deployments/inbound | Finn rejected the deployment. Any reasons are in errors. |
409 | conflict | POST /deployments, POST /deployments/inbound | Another conflict with the current state prevented the deployment. |
502 | deployment_failed | POST /deployments, POST /deployments/inbound | The deployment couldn't be created, or Finn didn't get a deployment ID back. |
404 | deployment_not_found | GET /deployments/{id}, both stop endpoints | No such deployment, or the Finn has no live inbound deployment. |
409 | use_inbound_stop | POST /deployments/{id}/stop | This is an inbound deployment. Use POST /deployments/inbound/{finn_id}/stop. |
409 | deployment_not_active | POST /deployments/{id}/stop | The deployment has already finished or been stopped. |
502 | stop_failed | both stop endpoints | Finn couldn't stop the deployment. |
Phone numbers
| Status | error | Endpoint | Cause |
|---|---|---|---|
404 | phone_number_not_found | GET, DELETE /phone_numbers/{id} | No number with that ID in your organization. |
409 | compliance_required | GET /phone_numbers/available | You need compliance approval to search this inventory. Applies to India (IN) inventory, to provider=exotel, and to searches without provider when your organization's default carrier is Exotel. Organizations with an approved compliance application or an existing carrier subaccount pass. |
502 | provider_unavailable | GET /phone_numbers/available, DELETE /phone_numbers/{id} | The carrier couldn't be reached. |
409 | api_key_owner_required | POST /phone_numbers | The key's creator is no longer in the organization or has no email on file. |
402 | insufficient_credits | POST /phone_numbers | The wallet balance is too low. Includes required_credits and available_credits. |
403 | compliance_required | POST /phone_numbers | You need compliance approval before you can rent numbers. |
403 | rental_unavailable | POST /phone_numbers | This organization can't rent numbers. |
400 | price_currency_mismatch | POST /phone_numbers | The number's price is in a different currency from your wallet. |
502 | price_unavailable | POST /phone_numbers | Finn couldn't confirm the rental price. Check GET /phone_numbers before you retry. |
500 | rent_failed | POST /phone_numbers | Setting up the number failed after the charge, and the charge was refunded. Includes transaction_id. |
409 | number_in_use | DELETE /phone_numbers/{id} | The number is used by a deployment that hasn't ended (as its number or in its number_pool), was ever a deployment's phone_number_id, or is assigned to a Finn or has old assignment records. |
compliance_required returns 409 from the search endpoint and 403 from the rental endpoint. Check the error code, not the status.
Audiences
| Status | error | Endpoint | Cause |
|---|---|---|---|
404 | audience_not_found | any /audiences/{audience_id} path | No active audience with that ID in your organization. |
404 | contacts_file_missing | contact list and edit endpoints | The audience's contact file no longer exists. |
409 | audience_not_editable | POST /audiences/{audience_id}/contacts, DELETE /audiences/{audience_id}/contacts/{phone} | The audience has no contact file to edit. |
409 | audience_in_use | POST /audiences/{audience_id}/contacts, DELETE /audiences/{audience_id}/contacts/{phone} | A running deployment is using the audience: one with status in_progress, active, running or live. pending and paused deployments don't block edits. The response includes its deployment_id. |
409 | audience_limit_exceeded | POST /audiences/{audience_id}/contacts | The audience would have more than 50,000 contacts. |
404 | contact_not_found | DELETE /audiences/{audience_id}/contacts/{phone} | No contact with that phone number in the audience. |
409 | audience_would_be_empty | DELETE /audiences/{audience_id}/contacts/{phone} | You can't remove the last contact. Archive the audience instead. |
Errors outside the normal format
A few failures happen before your request reaches an endpoint, so their bodies use a different shape. Check the status code for these.
The body is parsed before the API key is checked. A body that isn't valid JSON gets 400 even when the key is missing or wrong, and it doesn't count toward your rate limits.
| Status | When | Body |
|---|---|---|
400 | The body isn't valid JSON | {"success": false, "error": "<parser message>"}. Here error is a description, not a code. |
413 | The body is larger than 10 MB | {"success": false, "error": "request entity too large"} |
404 | Valid key, but the path doesn't exist | {"success": false, "message": "Route /api/v1/... not found"} |
What to retry
| Response | Do this |
|---|---|
429 | Wait for the number of seconds in Retry-After, then retry. Nothing was done, so retrying is safe. |
500 or 502 on a GET | Retry with exponential backoff and jitter. |
500 or 502 on a write, a timeout, or a dropped connection | The write may have happened. Check first. See api-idempotency. |
402 | Top up the wallet, then retry. |
Any other 4xx | Don't retry. The same request gets the same error. |
There's no Idempotency-Key header. Retrying a write after an unclear result can do the work twice, for example placing two calls. api-idempotency shows how to check before you retry each write.
Client example
This client retries reads only. Writes go back to the caller so it can decide.
const BASE = "https://api.hirefinn.ai/api/v1";
const RETRYABLE = new Set([429, 500, 502]);
export async function finnGet(path: string, maxAttempts = 4): Promise<Response> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${process.env.FINN_API_KEY}` },
});
if (!RETRYABLE.has(res.status) || attempt === maxAttempts - 1) return res;
const retryAfter = res.headers.get("Retry-After");
const waitMs = retryAfter
? Number(retryAfter) * 1000
: 2 ** attempt * 500 + Math.random() * 250;
await new Promise((r) => setTimeout(r, waitMs));
}
throw new Error("unreachable");
}
import os, time, random, requests
BASE = "https://api.hirefinn.ai/api/v1"
RETRYABLE = {429, 500, 502}
def finn_get(path, max_attempts=4, **kwargs):
headers = {"Authorization": f"Bearer {os.environ['FINN_API_KEY']}"}
for attempt in range(max_attempts):
res = requests.get(f"{BASE}{path}", headers=headers, timeout=30, **kwargs)
if res.status_code not in RETRYABLE or attempt == max_attempts - 1:
return res
retry_after = res.headers.get("Retry-After")
wait = float(retry_after) if retry_after else 2**attempt * 0.5 + random.random() * 0.25
time.sleep(wait)
raise RuntimeError("unreachable")
Failures that aren't HTTP errors
202 from POST /calls means Finn accepted the call and started dialing. It doesn't mean anyone answered. Calls can still fail after that: the line is busy, the carrier rejects the call, or nobody picks up. Those outcomes appear in the call record (GET /calls/{call_uuid}) and in the call.completed webhook. They never come back as an HTTP error.
So handle errors in two layers. An HTTP error means your request was wrong or Finn couldn't process it. A call outcome means the request worked but the call didn't go the way you wanted.
Reporting a problem
Send Finn support the method, path, time (UTC) and the full response body. Never include the API key. If the failure is intermittent, include the times of one failing request and one successful one.