API token example: create, scope, rotate and revoke a key
A real key format, a real header, and the lifecycle a credential goes through — shown on an API you can call for free.
What an API token is (and what it is not)
An API token is a secret string that proves a request comes from a known account. The provider mints it, you store it, your code sends it on every request. "API key", "API token", "access key" and "secret key" are mostly the same object under different names; the distinctions that matter are below.
| Term | What it is | Lifetime | Where it is sent |
|---|---|---|---|
| API key | A long random credential bound to an account; the provider stores only its hash | Until rotated or revoked | A request header (X-API-Key) |
| Access token | A short-lived credential issued by an OAuth authorization server | Minutes to an hour | Authorization: Bearer … |
| Refresh token | A long-lived OAuth credential used only to obtain new access tokens | Days to months | The token endpoint only, from a backend |
| Client secret | The password of an OAuth client, exchanged for tokens; never sent to the API itself | Until rotated | The token endpoint only |
| Session token | A credential for a signed-in person, tied to a browser session | Minutes to a day | A cookie or bearer header, from that browser |
| Publishable key | A non-secret identifier safe to ship in a browser, limited by origin and ability | Until revoked | Client-side code |
This page is about the first row. If your situation involves a user granting access, read API key vs OAuth first.
An API token example: the header on the wire
OffenderSearch keys look like os_live_… (production) and os_test_… (sandbox). The prefix tells you, and the server, which host the key belongs to; the rest is random. Here is a complete authenticated request as it crosses the network. The key shown is an illustration, not a working credential.
POST /v1/search HTTP/1.1
Host: api.offendersearch.app
X-API-Key: os_live_7f3a9c21b0d4e8f6a2c5b1d9e0f7a3c4b6d8e2f1a5c7b9d3
Content-Type: application/json
{"query": {"firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12"}}Three things to notice. The key is in a header, so it never appears in a URL, a log line keyed on the path, or a referrer. The body is ordinary JSON — authentication and payload are separate concerns. And nothing else is required: no token exchange, no nonce, no signature. (Request signing is a different mechanism, used here for webhooks; see API signature.)
Generate an API key: where it comes from
- Create an account. Sign up. Every new account has 25 free calls and needs no card to make them.
- Open Dashboard → API keys. /dashboard/keys lists every key on the account with its name, masked prefix, creation time, last use and status.
- Create a key. Name it for the service or environment that will hold it —
screening-service (prod), notmy key. The full secret is shown once, at creation. Copy it into your secret store now; it is stored on our side only as a one-way hash and cannot be shown again. - Create a test key for the sandbox. The same page has Create test key. It begins
os_test_, works only onsandbox.offendersearch.app, answers with the sandbox's fixed scenarios and is never billed. Up to 3 test keys may be active at once, with 10 mints a day.
{
"id": "key_01j9x…",
"name": "screening-service (prod)",
"maskedKey": "os_live_7f3a…",
"createdAt": "2026-10-28T14:02:11Z",
"lastUsedAt": "2026-10-28T14:20:47Z",
"status": "active",
"kind": "secret"
}There is no "generate API key" endpoint callable *with* an API key, on purpose: a credential that can mint credentials turns one leak into unlimited leaks. Keys are created by a signed-in person in the dashboard, or by the dashboard's own session-authenticated route.
How to use the API key in code
Read the key from the environment, pick the host from its prefix, send it as X-API-Key. The examples search the sandbox for a synthetic person; a name the sandbox does not list returns a complete answer with zero records, which is exactly what you want to see first. The names that return matches are listed on the sandbox page.
# Keep the key in the environment, never in the command history of a shared machine.
export OFFENDERSEARCH_API_KEY=os_test_… # a sandbox key from Dashboard → API keys
curl -X POST https://sandbox.offendersearch.app/v1/search \
-H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": {"firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12"}}'The same header works for every product on the account — sex offender search, criminal records, monitoring, the Map API. The published Python and Node SDKs read OFFENDERSEARCH_API_KEY for you and pick the host from the prefix.
Scoping a token: what a key is allowed to do
Two models exist. Scoped tokens carry permissions inside the credential (reports:read); the API checks the scope on every call. Pure credentials carry nothing; permissions live on the account the key resolves to. OffenderSearch keys are the second kind — a key is a pure authentication credential with no per-key features — and the account carries the entitlements: whether the Map API is turned on, whether the account is an approved FCRA partner, the plan's rate limits.
| Model | Pro | Con | Fits |
|---|---|---|---|
| Scopes in the token | Least privilege per credential; one leaked token does less | Scope sprawl; every caller must know which scope it needs | Large APIs with many distinct permissions and many clients |
| Permissions on the account | Nothing to configure per key; a new key is immediately right | A key is as powerful as the account | Data APIs where every call does one kind of thing |
The scoping you *do* get from us is by kind: a secret key for your backend; a test key that only the sandbox honours; and a publishable Map API browser key limited to the sites it may be used from. Pick the least powerful kind that does the job — a map rendered in a browser never needs a secret key.
Rotate an API key without a gap
Rotation replaces a secret that may have leaked, or that has simply been in service long enough. There are two ways to do it and they have different gap behaviour.
| Method | How | Gap? |
|---|---|---|
| Rotate in place | Dashboard → API keys → Rotate. The key keeps its id and name; a new secret is shown once; the old secret stops working immediately. | Yes — between the rotation and your redeploy, requests with the old secret answer 401. Use it when the old secret must die *now*. |
| Create, deploy, revoke | Create a second key, deploy it everywhere, confirm lastUsedAt on the old key stops moving, then revoke the old one. | No — two live keys coexist, so the switch is gapless. Use it for scheduled rotation. |
Test keys cannot be rotated in place: a test key's id is signed into the token itself, so a "new secret under the same id" would leave the old token valid on the sandbox. Revoke the test key and create a new one instead — the dashboard says so when you try.
Revoke a key
Dashboard → API keys → Revoke. It is immediate and permanent: the next request with that key answers 401. A revoked key stays in the list with status: revoked so the audit trail of what existed, and when it was last used, survives the revocation. For a test key, the sandbox learns of the revocation within its refresh interval of about five minutes; a new test key works at once.
Keys belong to the account, not to the person who created them. A team member who leaves does not take a key with them, and a key keeps working until someone revokes it — so revoking is a step in off-boarding, not something that happens by itself.
Store the token safely
# .env — never committed; the file is in .gitignore
OFFENDERSEARCH_API_KEY=os_live_…
# CI / production: inject from the secret manager at run time
# GitHub Actions secrets.OFFENDERSEARCH_API_KEY
# Cloud Run --set-secrets OFFENDERSEARCH_API_KEY=offendersearch-key:latest
# Kubernetes a Secret mounted as an env var, not a ConfigMap- Environment variables or a secret manager, read at start-up. Never a string literal in code, never a config file in the repository, never a Slack message.
- Server-side only. A key in a browser bundle, a mobile app or a public repository has been published; rotate it the moment you notice.
- One key per service and per environment. Name them that way in the dashboard. A leak then costs one revocation.
- Watch `lastUsedAt`. A key that is supposed to be retired but keeps being used is a deployment you forgot. A key that is used from somewhere it should not be is an incident.
- Prefer the test key for everything that is not production. Integration tests, CI, demos and tutorials run against the sandbox with
os_test_keys — nothing to bill, nothing real to leak.
API token errors: 401 and 403
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"detail": "Invalid or missing API key"}
# Causes, in the order to check them:
# 1. header name is not exactly X-API-Key
# 2. an os_test_ key was sent to api.offendersearch.app (or os_live_ to the sandbox)
# 3. the key was revoked or rotated — the dashboard shows status and the new prefix| Status | Meaning | What to do |
|---|---|---|
401 | No key, an unknown key, a revoked key, or a test key on the live host (and the reverse) | Check the header name, the prefix against the host, and the key's status in the dashboard |
403 | The key is valid; the account lacks the entitlement for this route (detail.code names it) | Turn the feature on in the dashboard, or request access |
429 | The per-key or per-account rate limit (sandbox: 60 a minute and 2,000 a day per test key) | Wait Retry-After seconds; nothing was billed |
Branch on the status, not on the text of detail. The complete contract — including 422 validation errors and when to retry — is on the API errors page.
API token FAQ
How do I generate an API key?
Sign up, open Dashboard → API keys, click Create key, and copy the secret while it is on screen. For a sandbox key click Create test key instead; it begins os_test_ and is never billed.
What does an api token example look like?
A header on a request: X-API-Key: os_live_… followed by about 48 random characters. The prefix identifies the environment, the rest is the secret, and the server compares a hash of the whole string with the hash it stored.
Is an api key the same as a client secret?
Same handling, different use. An API key is sent to the API on every request. A client secret is sent only to an OAuth token endpoint, which hands back a short-lived access token that is then sent to the API. Both must stay server-side.
What are the api token types?
API keys (long-lived account credentials), OAuth access tokens (short-lived, bearer), refresh tokens (long-lived, exchange-only), session tokens (a signed-in person), and publishable keys (non-secret, browser-safe, origin-limited). The table at the top of this page compares them.
Can an API key be used in a browser?
A secret key, no — it is then public. For browser use, providers issue a publishable key that can do only what is safe in public; OffenderSearch does this for the Map API, with the key limited to the origins you list.
What happens when an api key is required and missing?
The API answers 401 Unauthorized with a JSON detail. Nothing is searched and nothing is billed. The two endpoints that do not require a key are the coverage catalogues, GET /v1/sources and GET /v1/criminal/sources.
What this page does not cover
- OAuth token issuance, refresh and the authorization code flow — see API key vs OAuth.
- Signing requests with the key as an HMAC secret. We do not sign inbound requests; we sign outbound webhooks — see API signature.
- Per-key scopes or per-key billing. Keys here carry neither; usage is recorded per account and shown in the dashboard.
- Organisation SSO for signing in to the dashboard itself, which is separate from API authentication.