Developer docs · v1.0.0

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.

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 keyOAuth client credentialsOAuth authorization code
IdentifiesAn application (an account)An application, via an authorization serverA user, who granted an application access
Credential sentThe key itself, on every requestA short-lived access token obtained from the token endpointA short-lived access token; a refresh token to get more
LifetimeUntil revoked or rotatedMinutes to an hour, then re-issuedAccess minutes; refresh days to months
ScopesUsually none; permissions live on the accountYes, at issue timeYes, consented by the user
User present?NoNoYes, once, at consent
Setup costOne headerA token endpoint and a client registrationRedirects, a callback route, PKCE, token storage
FitsServer-to-server integrations of one accountFleets of services, zero-trust networks, platforms issuing credentials to tenantsSign 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: 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 with a test key.

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"}}'
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 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)
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
# 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
# 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" }
ClaimMeaningWhat a resource server checks
issWho issued the tokenMatches the authorization server you trust
subWho the token is about (a user or a client)Becomes the principal for the request
audWhich API it was minted forEquals this API, or reject — a token for another service is not valid here
expExpiry (Unix seconds)In the future, with a small clock skew allowance
scopeWhat was grantedCovers the operation being attempted
signatureIssuer's signature over header + payloadVerifies 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.

SituationUseWhy
Your backend calls a data API under your accountAPI keyOne credential, one header, no token plumbing. Rotate on a schedule.
A third-party app acts for your usersAuthorization code + PKCEThe user consents; the token is scoped to them and expires.
Dozens of services, several APIs, one identity providerClient credentialsCentral issue and revocation; short-lived tokens; scopes per service.
A worker or CI job with no user presentAPI key or client credentialsNever borrow a user's token for a job that outlives their session.
Code running in a browser or mobile appAuthorization code + PKCE, or a publishable key with origin limitsAnything shipped to a client is public. A secret API key in a client has been published.
An MCP server or AI agent calling a toolThe tool's own credential, held server-sideThe agent holds the key; the model never sees it. See the 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.
  • 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, 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 page, not a way to call the APIs. And the Map API 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 page, and you can exercise it with the webhook tester.

API authentication errors and what they mean

StatusAPI key worldOAuth worldFix
401No key, unknown key, revoked key, or the wrong prefix for the hostMissing, expired, malformed or revoked access tokenCheck the header name; check the prefix against the host; refresh the token once and retry once
403Valid key, but the account lacks the entitlement (detail.code names it)Valid token, insufficient scope, or wrong audEnable the feature on the account; request the scope at issue time
429Per-key or per-account limit hit; Retry-After says how longSame, per clientWait 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 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 page, because we use it for webhooks.
  • An OAuth server for the OffenderSearch APIs. There is none; see the section above.