Skip to main content

API reference

Errors

Status codes, error bodies and what to retry.

Beta. This endpoint is not final. Field names and response shapes can change before general availability.

How errors are returned

Every failed request returns a JSON body with an error object. The HTTP status code tells you the class of failure. The error.code field tells you the specific cause and is the value you should branch on in code. Status codes are stable across the beta. Individual error.code values may be added or renamed.

{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_required_field",
    "message": "Field 'phone_number' is required.",
    "param": "phone_number",
    "request_id": "req_01HX9QK3M2ZP7YV4T8"
  }
}
FieldTypeNotes
typestringBroad class. One of authentication_error, invalid_request_error, rate_limit_error, api_error.
codestringMachine-readable cause. Branch on this.
messagestringHuman-readable. Do not parse or match on it.
paramstring, nullableThe offending field, when the failure is field-specific.
request_idstringEchoes the X-Request-Id response header. Include it in support requests.

Whether param uses dotted paths for nested fields (audience.contacts.0.phone_number) is not yet settled. Do not build parsing logic on it.

Status codes

StatusMeaningRetry?
400Malformed request. Bad JSON, wrong type, unknown field.No. Fix the request.
401Missing, malformed or revoked API key.No. See authentication.
403Key is valid but not permitted for this resource or workspace.No.
404Resource does not exist, or belongs to another workspace.No.
409Conflict with current state. A deployment already running, a number already allocated.No, until the conflicting state changes.
422Syntactically valid but semantically rejected. Unreachable number, unsupported language, empty audience.No. Fix the input.
429Rate limit or concurrency limit reached.Yes, with backoff. See api-rate-limits.
500Unhandled error on Finn's side.Yes, with backoff.
502 / 503 / 504Upstream or telephony provider unavailable, or request timed out.Yes, with backoff.

A 404 on a resource you believe exists usually means the API key belongs to a different workspace. Keys are workspace-scoped, so a valid key never leaks the existence of another workspace's resources.

What to retry

Retry 429 and 5xx. Do not retry 4xx other than 429. Retrying a 400 or 422 with the same body produces the same failure and consumes rate limit budget.

Use exponential backoff with jitter. For 429, honour the Retry-After response header when present rather than your own schedule.

Retries on write endpoints can create duplicates if the first request succeeded but the response was lost. Send an Idempotency-Key header on every POST. See api-idempotency.

curl -X POST https://api.hirefinn.ai/v1/calls \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -H "Idempotency-Key: 8f14e45f-ea6d-4c1b-9d2a-3c7b1a0e5f22" \
  -H "Content-Type: application/json" \
  -d '{
    "finn_id": "finn_01HX8QK3M2",
    "to": "+14155550142"
  }'

Client example

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function finnRequest(
  path: string,
  init: RequestInit,
  maxAttempts = 4,
): Promise<Response> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(`https://api.hirefinn.ai/v1${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${process.env.FINN_API_KEY}`,
        "Content-Type": "application/json",
        ...init.headers,
      },
    });

    if (res.ok) return res;
    if (!RETRYABLE.has(res.status)) return res;
    if (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/v1"
RETRYABLE = {429, 500, 502, 503, 504}

def finn_request(method, path, max_attempts=4, **kwargs):
    headers = kwargs.pop("headers", {})
    headers["Authorization"] = f"Bearer {os.environ['FINN_API_KEY']}"

    for attempt in range(max_attempts):
        res = requests.request(method, f"{BASE}{path}", headers=headers, **kwargs)

        if res.ok or res.status_code not in RETRYABLE:
            return res
        if 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")

Errors that are not HTTP errors

A 200 on a call-creation endpoint means Finn accepted the request, not that the call connected. Calls fail later for reasons the API cannot see at request time: the number is busy, the carrier rejects it, nobody answers. Those outcomes arrive through webhooks and appear in call-outcomes, not in an HTTP status code.

Build your error handling in two layers. HTTP errors mean your request was wrong or Finn was unavailable. Call outcomes mean the request was fine and the call itself did not go the way you wanted.

Reporting a problem

Include the request_id and the full response body. Do not include the API key. If the failure is intermittent, include the timestamps of both a failing and a succeeding request. See support.