API Keys
Issue per-user API keys for programmatic access, authenticated by header and rate-limited from the owner's plan.
API keys let users authenticate to the backend programmatically instead of with a session cookie, gated by config.apiKeys.enabled in packages/config/src/index.ts. When enabled (the shipped default) Better Auth's apiKey plugin is registered, the Settings → Developers page issues and revokes keys, and any request carrying a valid x-api-key header resolves to a full session. When off, the plugin is never registered and every /api-keys route returns 400.
How a key authenticates
The plugin is configured with enableSessionForAPIKeys: true (packages/auth/src/config.ts). On any request, auth.api.getSession({ headers }) inspects the x-api-key header, looks up the key, and returns the owner's session - so authGuard works transparently whether the caller sent a cookie or a key.
curl https://app.example.com/api/billing/usage \
-H "x-api-key: key_xxxxxxxxxxxx..."Keys are stored hashed in the apikeys table; the plaintext value is returned once at creation and never again.
Configuration
config.apiKeys is a discriminated union: { enabled: false } or { enabled: true; prefix? }.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master flag; false unregisters the plugin and disables every key route. |
prefix | string? | "key_" | Prepended to generated keys (include the underscore). The first 12 characters are stored as start for display. |
apiKeys: {
enabled: true,
prefix: "key_"
}There are no environment variables - keys live in the database, not in config or env.
Endpoints
| Route | Guard | Action |
|---|---|---|
GET /api-keys | authGuard | List the caller's own keys (metadata only, no secret). |
POST /api-keys | authGuard | Create a key ({ name }); returns the plaintext key once. |
DELETE /api-keys/:id | authGuard | Revoke one of the caller's keys. |
GET /api-keys (admin) | adminGuard | List every user's keys, paginated, enriched with owner name/email. |
DELETE /api-keys/:id (admin) | adminGuard | Revoke any key. |
Create and revoke are written to the audit log (API_KEY_CREATED, API_KEY_REVOKED). See API Layer for how these mount and Authorization for the guards.
Rate limits
Each key is stamped with a request quota at creation time, derived from the owner's plan. The create route (packages/api/src/routes/internal/api-keys.ts) calls resolveApiKeyRateLimitPlan (from @repo/payments) when minting the key. The plan's apiRateLimit from packages/config/src/pricing.ts wins, falling back to pricingConfig.defaultApiRateLimit (100 requests / day).
The quota is baked in when the key is created - changing a user's plan does not retroactively re-limit their existing keys. Have users rotate keys after an upgrade to pick up the new ceiling. When billingScope is "organization", the active org's plan drives the limit, not the user's.
Frequently asked questions
Can a key act on behalf of an organization?
Keys are always user-referenced (Better Auth supports only user-owned keys), but under billingScope: "organization" the rate limit is resolved from the caller's active organization's plan. See Payments.
I lost a key - can I recover it? No. Only the hash is stored. Revoke it and issue a new one from Settings → Developers.
How is this different from the Content API key?
X-Content-Api-Key is a single shared secret (the CONTENT_API_KEY env var) guarding the public /v1/content route. API keys here are per-user, database-backed, and authenticate as the owning user. See API Layer.