API Keys
Issue per-user API keys for programmatic access, authenticated by the x-api-key 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 (packages/config/src/index.ts, on by default). Enabled, Better Auth's apiKey plugin registers, Settings → Developers issues and revokes keys, and any request carrying a valid x-api-key header resolves to a full session; disabled, every /api-keys route returns 400.
How a key authenticates
The plugin runs with enableSessionForAPIKeys: true (packages/auth/src/config.ts), so auth.api.getSession({ headers }) inspects x-api-key, looks up the key, and returns the owner's session. authGuard then 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? }. There are no environment variables - keys live in the database.
| 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_"
}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 plan of the user who owns it. packages/api/src/routes/internal/api-keys.ts calls resolveApiKeyRateLimitPlan (@repo/payments) when minting the key, then takes that plan's apiRateLimit from packages/config/src/pricing.ts, falling back to pricingConfig.defaultApiRateLimit (100 requests / day).
The quota is baked in at creation - 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.
Frequently asked questions
Can a key act on behalf of an organization? No. Better Auth supports only user-owned keys, so a key authenticates as its owner. Metered work still bills whoever funds the resulting session - see Organizations.
Why doesn't a member get their workspace owner's quota? Because a key outlives the membership that would have granted it. Sourcing the limit from the workspace owner would let someone join a paid workspace, mint a key, leave, and keep that throughput forever.
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. These keys are per-user, database-backed, and authenticate as the owning user. See API Layer.