Your users' API keys
Let the people who sign in to your app make API keys for your own API, and check them from your API.
Some of your users want to call your API from a script, a CI job or a tool of their own, where signing in through a browser doesn't fit. With API keys, they make a key for your API themselves, from your app, and send it to your API in place of an access token. The key acts as them, with the scopes they gave it, until they or you revoke it.
Turning it on
API keys are off for every API until you turn them on, one API at a time. Your API needs its own app first: a web or machine app of the brand, linked as the API's client_id. It's the app your API already uses to introspect tokens, and the only one that may check the API's keys.
Change the API with allow_user_api_keys: true (and its client_id, if it has none yet). Turning it off later revokes every key your users made for that API, for good: turning it on again revives none of them.
Making a key
People make their keys from your app, through the account API, with an access token for it:
GET /me/v1/apislists the APIs they may make keys for, with each scope your API defines and its description.POST /me/v1/api-keysmakes one, with the API's identifier, a name, the scopes it may use, and, if they like, anexpires_atwithin 366 days. The answer carries the key itself,ak_live_…in production andak_test_…elsewhere, this once: Synq keeps only its hash. Show it to them once, with a way to copy it.GET /me/v1/api-keyslists their keys, never the keys themselves, with when each was last used.DELETE /me/v1/api-keys/{key}revokes one at once.
Making a key needs me:write and a sign-in from the last 5 minutes, since a key outlives every session: send the person to sign in with max_age=300 first, as for adding a passkey. One person has at most 25 live keys (api_key_limit_reached), and an API that doesn't take keys answers api_keys_not_allowed.
Checking a key
Your API checks every request's key with Synq, at the brand's issuer ($ISSUER below, such as https://acme.auth.synq.build), the way it introspects a token (RFC 7662). It authenticates as its own app, with its client secret, and sends the key as token:
curl -X POST "$ISSUER/api-keys/verify" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$KEY"A key that works answers:
{
"active": true,
"token_type": "api_key",
"sub": "usr_01h455vb4pex5vsknk084sn02q",
"scope": "reports:read",
"aud": "https://api.acme.example",
"iss": "https://acme.auth.example.com",
"client_id": "app_01h455vb4pex5vsknk084sn02q",
"iat": 1792440000,
"key_id": "uak_01h455vb4pex5vsknk084sn02q",
"name": "Reports script"
}sub is the person, as in their tokens, scope what the key may do, and iss the brand's issuer. Anything else answers { "active": false }, and never why: a key that was revoked or expired, one for another API, a person who was disabled or is leaving, or a key left with no scope because your API dropped them. A wrong client secret is 401 invalid_client.
Check the key on every request, or keep an answer for a few seconds at most: revoking a key works from its next check. exp is there when the key expires.
When your API's identity_claims name them, the answer also says who the person is beyond sub, as its access tokens do: email, email_verified and linked_accounts, the person's accounts at providers such as GitHub and Google (Linked accounts). So a key's caller is judged as a token's.
With the SDK
On your API's server, createApiKeyVerifier from @synqauth/js/server checks keys for you, and isApiKey tells a key from an access token, so one route can take either:
import { createAccessTokenVerifier, createApiKeyVerifier, isApiKey } from '@synqauth/js/server'
const verifyToken = createAccessTokenVerifier({ issuer, audience: 'https://api.acme.example' })
const verifyKey = createApiKeyVerifier({ issuer, clientId, clientSecret })
const credential = request.headers.get('authorization')?.replace(/^Bearer /, '') ?? ''
const caller = isApiKey(credential)
? await verifyKey(credential, { scopes: ['reports:read'] })
: await verifyToken(credential, { scopes: ['reports:read'] })A key that doesn't work throws invalid_token, and one without a scope you asked for insufficient_scope. In your app, @synqauth/react's <AccountManager> shows an API keys section wherever one of the brand's APIs takes keys: people make a key there, see it once with a copy button, and revoke it. useApiKeys, useKeyableApis and useAuth().apiKeys build your own.
What you see
The management API lists a user's keys and revokes one. Your audit log and webhooks record user.api_key_created and user.api_key_revoked, with the key's id and the start of the key, never the key itself.
A scope your API drops leaves every key that had it, so defining a scope again later gives it to nobody. When someone claims an email address another person signed up with and never proved, the keys that person made are revoked with the rest of what they added.