GenerateSaaS

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.

KeyTypeDefaultDescription
enabledbooleantrueMaster flag; false unregisters the plugin and disables every key route.
prefixstring?"key_"Prepended to generated keys (include the underscore). The first 12 characters are stored as start for display.
packages/config/src/index.ts
apiKeys: {
  enabled: true,
  prefix: "key_"
}

Endpoints

RouteGuardAction
GET /api-keysauthGuardList the caller's own keys (metadata only, no secret)
POST /api-keysauthGuardCreate a key ({ name }); returns the plaintext key once
DELETE /api-keys/:idauthGuardRevoke one of the caller's keys
GET /api-keys (admin)adminGuardList every user's keys, paginated, enriched with owner name/email
DELETE /api-keys/:id (admin)adminGuardRevoke 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.

On this page