Skip to main content

Security and compliance

Authentication

API keys, scopes and rotation.

API keys

Every request to the Finn API authenticates with an API key sent as a bearer token.

curl https://api.hirefinn.ai/v1/finns \
  -H "Authorization: Bearer fk_live_YOUR_KEY_HERE"

A request with no Authorization header, a malformed header, or a revoked key fails with 401. A key that is valid but lacks the scope for the operation fails with 403. See api-errors for the full error shape.

Keys are created and revoked in the dashboard under Settings → API keys. There is no endpoint that creates keys, and there is no endpoint that lists key secrets. The secret is shown once, at creation time. If you lose it, you revoke that key and create another. There is no recovery path.

Key types

PrefixEnvironmentNotes
fk_live_ProductionPlaces real calls. Consumes wallet balance.
fk_test_SandboxNo telephony, no wallet spend. Use for local-development.

A test key against a production resource returns 404, not 403. The two environments do not share data, so a Finn created with a test key is not visible to a live key.

Scopes

A key carries a fixed set of scopes chosen at creation. Scopes cannot be edited afterwards. To change what a key can do, create a new key and revoke the old one.

ScopeGrants
finns:readRead agents and their configuration
finns:writeCreate, update, delete agents
deployments:readRead deployments and their status
deployments:writeCreate, start, stop deployments
calls:readRead call logs, transcripts, recordings
calls:writePlace outbound calls
audiences:readRead audiences and contacts
audiences:writeCreate and modify audiences, add contacts
numbers:readRead phone number inventory
numbers:writeRent, release, reassign numbers
webhooks:writeCreate and modify webhook endpoints
wallet:readRead balance and transaction history

Grant the smallest set that works. A key used by a reporting job that pulls transcripts nightly needs calls:read and nothing else. If that key leaks, the blast radius is read access to call data rather than the ability to place calls and drain the wallet.

Scopes gate the endpoint, not the row. A key with calls:read reads every call in the workspace. There is no per-Finn or per-deployment key scoping.

Rotation

Keys do not expire on their own. Rotation is manual, and the overlap window is what keeps it from causing an outage.

  1. Create the new key in Settings → API keys with the same scopes as the old one.
  2. Deploy the new secret to every consumer. Do not revoke yet.
  3. Watch the old key's Last used timestamp in the dashboard. It updates on every request.
  4. When the old key has gone quiet for longer than your slowest job's interval, revoke it.

Step 3 is the one people skip. A nightly cron that runs at 02:00 will look idle all afternoon. Revoking on the basis of six quiet hours breaks it that night, and the failure surfaces as 401 in a job nobody is watching. Wait out a full cycle of your longest-running scheduled work before revoking.

Revocation takes effect immediately. In-flight requests holding the old key fail. There is no grace period and no soft-delete, so a revoked key cannot be restored.

Storing keys

Keys are workspace-wide credentials. Anyone holding one acts with its scopes against your production data and your wallet balance.

  • Keep keys in environment variables or a secrets manager, never in source control.
  • Never call the Finn API from browser or mobile client code. The key ships to the user. Proxy through your own backend.
  • Use a separate key per consumer (per service, per cron job, per integration) so you can revoke one without taking down the rest.
  • Rotate immediately if a key appears in a log, a screenshot, a support ticket, or a public repository.
const res = await fetch("https://api.hirefinn.ai/v1/calls?limit=50", {
  headers: {
    Authorization: `Bearer ${process.env.FINN_API_KEY}`,
    "Content-Type": "application/json",
  },
});

if (res.status === 401) {
  throw new Error("Key is invalid or revoked");
}
if (res.status === 403) {
  throw new Error("Key is valid but missing a required scope");
}

const { data } = await res.json();
import os
import requests

res = requests.get(
    "https://api.hirefinn.ai/v1/calls",
    headers={"Authorization": f"Bearer {os.environ['FINN_API_KEY']}"},
    params={"limit": 50},
    timeout=30,
)

if res.status_code == 401:
    raise RuntimeError("Key is invalid or revoked")
if res.status_code == 403:
    raise RuntimeError("Key is valid but missing a required scope")

data = res.json()["data"]

Webhooks authenticate differently

Inbound webhook deliveries from Finn are not authenticated with your API key. They carry a signature computed from a separate signing secret, which is rotated independently. Do not reuse an API key as a webhook secret and do not treat a request as trusted because it names your workspace. See webhook-security.

Rate limits are per key

Limits apply per key, not per workspace, so splitting consumers across keys also splits their limits. A batch job that saturates its own key does not stall your production traffic. See api-rate-limits for the numbers and the retry behavior.