Skip to main content

API reference

Rate limits

Limits, headers and backoff.

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

How limits are applied

Rate limits are enforced per API key, against the workspace that key belongs to. Two keys in the same workspace draw from the same pool. A key that is revoked and reissued keeps the same pool.

Limits are counted in requests per minute using a rolling window. The window advances continuously, so a burst of requests spread over a minute is not equivalent to the same burst fired in one second. Requests that fail authentication still count against the limit.

Concurrency of live calls is a separate control and is not governed by these limits. See concurrency for how simultaneous call capacity works.

Limit tiers

The exact per-endpoint numbers are not yet settled and are not published here. Read the response headers rather than hardcoding a number, because the value assigned to your workspace can change without a version bump.

Endpoint groups are rate limited independently. A campaign launch that saturates the write limit on api-calls does not consume the read limit on api-call-logs.

GroupEndpointsCharacteristic
ReadGET on finns, deployments, calls, audiences, voicesHighest limit
WritePOST, PATCH, DELETE on the same resourcesLower limit
Call originationPOST /v1/calls and campaign dispatchLowest limit, tied to your plan
WalletGET /v1/wallet and relatedLow limit, intended for polling at minute granularity

Response headers

Every response carries the current state of your limit, including successful ones. Use these instead of tracking counts client side.

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window for this endpoint group
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix seconds at which the window refills
Retry-AfterSeconds to wait. Present only on 429
curl -i https://api.hirefinn.ai/v1/finns \
  -H "Authorization: Bearer $FINN_API_KEY"
HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 597
X-RateLimit-Reset: 1753876800

The numbers above are illustrative. Do not treat them as your allocation.

When you exceed the limit

You get 429 Too Many Requests with the standard error envelope described in api-errors.

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "Rate limit exceeded for this endpoint group.",
    "retry_after": 12
  }
}

A 429 means the request was rejected before any work happened. No call was placed, no record was written, no wallet balance was touched. Retrying is safe.

Where a 429 is ambiguous is on write endpoints under a proxy or a network timeout, because you may not have seen the response at all. Send an Idempotency-Key on every write so a retry cannot create a second call or a second deployment. See api-idempotency.

Backing off

Honour Retry-After when present. When it is absent, use exponential backoff with jitter. Retrying on a fixed interval synchronises your workers and reproduces the burst that caused the 429.

import os, random, time
import requests

BASE = "https://api.hirefinn.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FINN_API_KEY']}"}

def get_with_backoff(path, max_attempts=6):
    for attempt in range(max_attempts):
        response = requests.get(f"{BASE}{path}", headers=HEADERS, timeout=30)
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        wait = response.headers.get("Retry-After")
        if wait is not None:
            delay = float(wait)
        else:
            delay = min(2 ** attempt, 60) + random.uniform(0, 1)
        time.sleep(delay)
    raise RuntimeError(f"Rate limited after {max_attempts} attempts: {path}")

print(get_with_backoff("/finns"))

The TypeScript equivalent, throttling ahead of the limit rather than reacting to it:

const BASE = "https://api.hirefinn.ai/v1";
const KEY = process.env.FINN_API_KEY!;

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export async function request(path: string, init: RequestInit = {}) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const response = await fetch(`${BASE}${path}`, {
      ...init,
      headers: { ...init.headers, Authorization: `Bearer ${KEY}` },
    });

    if (response.status === 429) {
      const retryAfter = response.headers.get("Retry-After");
      const delay = retryAfter
        ? Number(retryAfter) * 1000
        : Math.min(2 ** attempt * 1000, 60_000) + Math.random() * 1000;
      await sleep(delay);
      continue;
    }

    // Slow down before you hit the wall.
    const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
    if (!Number.isNaN(remaining) && remaining < 10) {
      await sleep(1000);
    }

    if (!response.ok) {
      throw new Error(`${response.status} ${await response.text()}`);
    }
    return response.json();
  }
  throw new Error(`Rate limited after 6 attempts: ${path}`);
}

What goes wrong

Paginating with a small page size is the most common way to burn a read limit. Fetching 10,000 call records 25 at a time is 400 requests. Request the largest page size the endpoint allows. See api-pagination.

Polling for call completion is the second. A worker polling every second for a five minute call spends 300 requests on one call. Subscribe to webhook-events instead and poll only as a reconciliation pass.

Retrying without jitter turns one 429 into a stampede. If twenty workers all back off two seconds and retry together, they all fail again at the same moment.

Running load tests against production consumes the same pool as your live traffic. There is no separate test allocation.

Requesting an increase

Whether limits can be raised on request, and by how much, is not yet settled for the beta. Contact support with your workspace ID, the endpoint group, and your expected peak requests per minute.