# API key vs OAuth client credentials: which to use

> An API key identifies your application. OAuth proves that a user, or a machine, granted it something. Here is each flow as it actually runs, and the decision rule that picks one.

- **HTML:** https://offendersearch.app/docs/learn/api-key-vs-oauth
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## API key vs OAuth: the short answer

Use an **API key** when your own backend calls a service and no end user is in the picture. Use **OAuth 2.0 client credentials** when a machine needs short-lived, scoped, centrally revocable credentials from an authorization server. Use the **OAuth authorization code flow** when a person is delegating access to their own data in a third-party app. Everything else on this page is the detail behind those three sentences.

|  | API key | OAuth client credentials | OAuth authorization code |
| --- | --- | --- | --- |
| Identifies | An application (an account) | An application, via an authorization server | A user, who granted an application access |
| Credential sent | The key itself, on every request | A short-lived access token obtained from the token endpoint | A short-lived access token; a refresh token to get more |
| Lifetime | Until revoked or rotated | Minutes to an hour, then re-issued | Access minutes; refresh days to months |
| Scopes | Usually none; permissions live on the account | Yes, at issue time | Yes, consented by the user |
| User present? | No | No | Yes, once, at consent |
| Setup cost | One header | A token endpoint and a client registration | Redirects, a callback route, PKCE, token storage |
| Fits | Server-to-server integrations of one account | Fleets of services, zero-trust networks, platforms issuing credentials to tenants | Sign in with…, third-party apps acting for a user |

## API key authentication, with an example

An API key is a long random secret the provider issues to an account. The client sends it on every request, usually in a header. The provider stores a one-way hash, looks the hash up, and treats the request as that account. There is no token exchange, no expiry clock and no consent screen — which is why it is the right shape for a backend talking to a data API.

This is how the [OffenderSearch API authenticates](/docs/platform/authentication): one `X-API-Key` header, one key format, every product. The key is a pure credential — it carries no scopes; what an account may do (the Map API, FCRA partner access) is a property of the account and applies to every key on it. Run the request below against the [sandbox](/sandbox) with a test key.

**curl**
```bash
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"}}'
```

**Python**
```python
import os, requests

# An API key is a credential, not an OAuth token: send it on every request, no token exchange.
resp = requests.post(
    "https://sandbox.offendersearch.app/v1/search",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_API_KEY"],  # os_test_… on the sandbox
             "Content-Type": "application/json"},
    json={"query": {"firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12"}},
    timeout=60,
)
if resp.status_code == 401:
    raise SystemExit("401: wrong header name, wrong key prefix for this host, or a revoked key")
resp.raise_for_status()
print(resp.json()["status"], resp.json()["counts"]["records"], "records")
```

**Node**
```javascript
const res = await fetch("https://sandbox.offendersearch.app/v1/search", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.OFFENDERSEARCH_API_KEY, // os_test_… on the sandbox
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ query: { firstName: "Jordan", lastName: "Rivera", dob: "1988-04-12" } }),
});
if (res.status === 401) throw new Error("401: header, key prefix or revoked key");
const data = await res.json();
console.log(data.status, data.counts.records, "records");
```

> **Header, not query string.** Send a key in a header (`X-API-Key`) or an `Authorization` header, never as `?api_key=` in the URL. Query strings land in server logs, proxy logs, browser history and referrer headers; a request header lands in none of them.

Where the key comes from, how it is shown once, rotated and revoked is on the [API token example](/docs/learn/api-token-example) page.

## OAuth 2.0 client credentials flow, step by step

The client credentials grant (RFC 6749 §4.4) is OAuth for machines. The client authenticates to the authorization server with its own `client_id` and `client_secret`, asks for a token with the scopes it needs, and receives a short-lived access token it then presents to the API as a bearer token. No user is involved at any point.

1. **Register the client.** The authorization server issues a `client_id` and `client_secret` to your service. The secret is a credential exactly like an API key — same storage rules.
2. **Request a token.** `POST /oauth/token` with `grant_type=client_credentials`, the client credentials (HTTP Basic or form fields) and the `scope` you want.
3. **Receive the access token.** The response carries `access_token`, `token_type: Bearer`, `expires_in` (seconds) and the granted `scope`. There is no refresh token in this grant: when the token expires you simply ask for another.
4. **Call the API.** `Authorization: Bearer <access_token>` on every request until `expires_in` elapses. Cache the token for its lifetime; do not request a new one per call.
5. **Handle 401.** An expired or revoked token answers `401`. Fetch a fresh token once, retry once, then surface the error.

*A client credentials exchange (generic authorization server)*
```http
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=client_credentials&scope=reports:read

HTTP/1.1 200 OK
Content-Type: application/json

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "reports:read"
}

GET /v1/reports HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
```

What it buys over a static key: tokens expire on their own, so a leaked token is a small window; scopes are decided at issue time; one authorization server can revoke a client for every API behind it. What it costs: a token endpoint to run or rent, a client registry, and a token cache in every caller.

## OAuth authorization code flow (with PKCE)

The authorization code grant (RFC 6749 §4.1, with PKCE from RFC 7636) is OAuth for people. The user is sent to the authorization server, signs in, consents to a scope, and is redirected back with a one-time **authorization code**. Your backend exchanges that code for an access token and, usually, a refresh token. The access token acts *as the user*, limited to the consented scope.

*The four legs of the authorization code flow*
```bash
# 1. Send the user to the authorization server (browser redirect)
GET https://auth.example.com/authorize
  ?response_type=code
  &client_id=abc123
  &redirect_uri=https://your-app.example.com/callback
  &scope=profile:read
  &state=5f2c…            # CSRF token you generated
  &code_challenge=…       # PKCE: SHA-256 of a verifier you keep
  &code_challenge_method=S256

# 2. The user signs in and consents; the server redirects back
GET https://your-app.example.com/callback?code=SplxlOBe…&state=5f2c…

# 3. Your backend exchanges the code (never the browser)
POST https://auth.example.com/oauth/token
grant_type=authorization_code&code=SplxlOBe…&redirect_uri=…&client_id=abc123&code_verifier=…

# 4. Access token (short-lived) + refresh token (long-lived, store server-side)
{ "access_token": "…", "expires_in": 3600, "refresh_token": "…", "token_type": "Bearer" }
```

- **PKCE** (`code_challenge` / `code_verifier`) stops an intercepted code from being exchanged by anyone but the client that started the flow. Use it for every client, not only mobile and single-page apps.
- **`state`** is your CSRF token: generate it, store it in the session, and reject a callback whose `state` does not match.
- **Refresh tokens** are long-lived and must be stored server-side, encrypted at rest, and rotated when the server supports rotation. A refresh token in a browser is a credential in a browser.
- **The browser never sees the client secret.** The code exchange happens from your backend; a public client (SPA, mobile) uses PKCE and no secret at all.

## Access tokens, bearer tokens and JWTs

An **access token** is whatever the authorization server hands back; the API accepts it as a **bearer token** — whoever bears it is authorized, which is why it is short-lived. Two formats are common. An **opaque** token is a random string the API has to look up (introspection). A **JWT** (RFC 7519) is a signed JSON document the API can verify locally with the issuer's public key, no lookup needed.

*A JWT access token, decoded*
```bash
# A JWT access token is three base64url parts: header.payload.signature
eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMTAifQ
.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJzdmMtcmVwb3J0cyIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc5MzAwMDAwMCwic2NvcGUiOiJyZXBvcnRzOnJlYWQifQ
.MEUCIQD…

# decoded payload
{ "iss": "https://auth.example.com", "sub": "svc-reports", "aud": "api.example.com",
  "exp": 1793000000, "scope": "reports:read" }
```

| Claim | Meaning | What a resource server checks |
| --- | --- | --- |
| `iss` | Who issued the token | Matches the authorization server you trust |
| `sub` | Who the token is about (a user or a client) | Becomes the principal for the request |
| `aud` | Which API it was minted for | Equals this API, or reject — a token for another service is not valid here |
| `exp` | Expiry (Unix seconds) | In the future, with a small clock skew allowance |
| `scope` | What was granted | Covers the operation being attempted |
| signature | Issuer's signature over header + payload | Verifies against the issuer's published key (`kid` picks which) |

> **Warning.** A JWT is signed, not encrypted. Anything in the payload is readable by whoever holds the token. Never put a secret, or a person's identifying data, in a token claim.

## Which to use: the decision rule

Ask one question first: **is an end user delegating access to their own data?** If yes, it is the authorization code flow, full stop. If no, it is a machine credential, and the choice between an API key and client credentials is about operations, not security theatre.

| Situation | Use | Why |
| --- | --- | --- |
| Your backend calls a data API under your account | API key | One credential, one header, no token plumbing. Rotate on a schedule. |
| A third-party app acts for your users | Authorization code + PKCE | The user consents; the token is scoped to them and expires. |
| Dozens of services, several APIs, one identity provider | Client credentials | Central issue and revocation; short-lived tokens; scopes per service. |
| A worker or CI job with no user present | API key or client credentials | Never borrow a user's token for a job that outlives their session. |
| Code running in a browser or mobile app | Authorization code + PKCE, or a publishable key with origin limits | Anything shipped to a client is public. A secret API key in a client has been published. |
| An MCP server or AI agent calling a tool | The tool's own credential, held server-side | The agent holds the key; the model never sees it. See the [MCP server](/mcp-server). |

Many systems use both at once: a key identifies the application for rate limiting while an OAuth token authorizes access to one user's data. That is not indecision; it is two questions with two answers.

## Securing APIs: the checklist that applies to both

- **TLS everywhere.** A credential sent over plain HTTP is a credential on the wire. Refuse `http://` at the edge.
- **Credentials in headers, never in URLs.** Query strings are logged by everything between the client and the server.
- **Store hashes, not secrets.** The provider keeps a one-way hash of each key and compares hashes; a database leak then leaks nothing usable. (This is how OffenderSearch stores keys.)
- **Show a secret once.** At creation, then never again. If it is lost, rotate.
- **Rotate without a gap.** Create the new credential, deploy it, then revoke the old one. Two live keys may coexist for exactly this reason.
- **One credential per service or environment.** A leak is then one revocation, not a redeploy of everything.
- **Least privilege.** Scopes for tokens; separate accounts or publishable keys with origin limits for anything that runs in a browser.
- **Rate limit per credential and answer `429` with `Retry-After`.** It protects the API and tells an honest client what to do. Ours is documented under [errors, rate limits and retries](/docs/platform/errors-and-retries).
- **Log the principal, not the credential.** Record which key id made a request; never write the key itself to a log line.
- **Validate `aud` and `exp` on every token**, and verify signatures against the issuer's published keys, not a key you copied once.

## How OffenderSearch authenticates (so you know what to expect)

Plainly: the APIs use **API keys**, and there is **no OAuth flow and no bearer token** for them. Create a key at [Dashboard → API keys](/dashboard/keys), send it as `X-API-Key`, done. A key beginning `os_test_` works only on `sandbox.offendersearch.app`; a key beginning `os_live_` works only on `api.offendersearch.app`; sending one to the other host answers `401`. The two keyless endpoints are the coverage catalogues, `GET /v1/sources` and `GET /v1/criminal/sources`, so you can audit coverage before you sign up.

Two places look like OAuth and are not. The FCRA consumer portal issues a **30-minute session** (`POST /v1/consumer/portal/session`) that a consumer's browser sends as a bearer token — it is a session for a person reading their own report, documented on the [consumer portal](/docs/fcra/consumer-portal) page, not a way to call the APIs. And the [Map API](/docs/map) can issue a **publishable browser key** limited to the sites it may be used from, for maps rendered client-side; it can read map tiles and nothing else.

Webhooks are the other direction of trust: every event we send is signed with an HMAC over a timestamp and the body, so your receiver can prove it came from us. That scheme is on the [API signature](/docs/learn/api-signature) page, and you can exercise it with the [webhook tester](/tools/webhook-tester).

## API authentication errors and what they mean

| Status | API key world | OAuth world | Fix |
| --- | --- | --- | --- |
| `401` | No key, unknown key, revoked key, or the wrong prefix for the host | Missing, expired, malformed or revoked access token | Check the header name; check the prefix against the host; refresh the token once and retry once |
| `403` | Valid key, but the account lacks the entitlement (`detail.code` names it) | Valid token, insufficient `scope`, or wrong `aud` | Enable the feature on the account; request the scope at issue time |
| `429` | Per-key or per-account limit hit; `Retry-After` says how long | Same, per client | Wait the header's seconds, then retry; nothing was billed |

Branch on the status code, not on the sentence in `detail` — the wording varies by route and may change. The full error contract is on the [API errors](/docs/learn/api-errors) page.

## API authentication FAQ

### api key vs oauth — which is more secure?

Neither is more secure by nature. An API key stored as a hash, sent in a header over TLS, rotated on a schedule and scoped to one service is as safe as a well-run OAuth deployment, and simpler to get right. OAuth is more secure *when a user is delegating access*, because a key cannot express "this user allowed this app to do this much for this long".

### What is the oauth client credentials flow?

The grant a machine uses to get an access token for itself: it authenticates with its client id and secret at the token endpoint and receives a short-lived bearer token with the requested scopes. No user, no redirect, no refresh token.

### What is an access token?

The credential an API accepts on a request after OAuth has run: a short-lived string (opaque, or a signed JWT) presented as `Authorization: Bearer …`. It says what was granted, to whom, by which issuer, until when.

### Is an api key a bearer token?

Functionally yes — whoever holds it is authorized — which is why a key needs the same handling as a token: TLS, headers, server-side only. Some APIs accept keys as `Authorization: Bearer <key>`; OffenderSearch uses a dedicated `X-API-Key` header so a key is never confused with an OAuth token.

### What is the authorization code flow for?

Letting a person grant a third-party application access to their data on another service without sharing their password: the user consents at the authorization server, the app receives a code, and exchanges it on the backend for tokens scoped to that consent.

### Does OffenderSearch support OAuth or Auth0?

No. The APIs authenticate with an API key in the `X-API-Key` header. There is nothing to configure in Auth0 or any identity provider to call them; your backend holds the key and makes the call.

### What is restful api authentication?

Any of the schemes on this page applied to a REST API: an API key header, a bearer access token from OAuth, or (rarely today) HTTP Basic. REST itself defines none of them; it only asks that each request carry what the server needs to authenticate it.

## What this page does not cover

- Configuring a specific identity provider (Auth0, Okta, Cognito, Entra). The flows above are the standard ones those products implement; their console steps are in their own documentation.
- SAML, OpenID Connect sign-in, and user-facing login for your application. OIDC is OAuth plus an identity token; the flows are the same, the purpose is sign-in rather than API access.
- mTLS and request signing as authentication. Signing is covered separately on the [API signature](/docs/learn/api-signature) page, because we use it for webhooks.
- An OAuth server for the OffenderSearch APIs. There is none; see the section above.

## Related

- [API token example: create, scope, rotate and revoke a key](https://offendersearch.app/docs/learn/api-token-example)
- [API signature: HMAC signing for requests and webhooks](https://offendersearch.app/docs/learn/api-signature)
- [API errors: designing codes a client can act on](https://offendersearch.app/docs/learn/api-errors)
- [Authentication & keys (platform reference)](https://offendersearch.app/docs/platform/authentication)
- [Sandbox & production environments](https://offendersearch.app/docs/platform/environments)
- [Public API for testing: the sandbox](https://offendersearch.app/sandbox)
- [Webhook tester](https://offendersearch.app/tools/webhook-tester)
- [Background check API](https://offendersearch.app/background-check-api)
- [Sex offender API](https://offendersearch.app/sex-offender-api)
- [Criminal records API](https://offendersearch.app/criminal-records-api)
- [Postman collection](https://offendersearch.app/postman)
- [All learning guides](https://offendersearch.app/docs/learn)
