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, mcp? }); returns the plaintext key once. mcp: true answers 400 while the MCP server is off
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.

MCP access

A key opens the external agent MCP server only when it was created with Allow MCP access ticked (shown while config.mcpServer.enabled is true). That stores the permission { mcp: ["access"] } on the key, and the list marks such keys MCP.

Refused on /api/mcp with the OAuth 401Why
A key without the MCP permissionREST access never implies agent access - the MCP server hands out every chat tool, including credit-spending ones
A revoked, disabled, expired or rate-limited keyChecked by the apiKey plugin, the same as on every route
A key whose owner is banned or deletedThe session would act as an account that may not act

The agent acts as the key's owner in their personal scope, exactly as an OAuth token does. Deleting an account deletes its keys.

Keys are created, changed and revoked only from a signed-in session: POST and DELETE /api-keys, and Better Auth's own /api-key/create, /update and /delete, answer 403 to a request carrying x-api-key (one guard, @repo/auth/api-key-management), so a REST-only key can never mint itself a key with MCP access. Listing keys stays open.

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