Skip to main content
The API Key plugin lets you create and manage API keys for your application. It supports rate limiting, custom expiration, permissions, metadata, organization-owned keys, and optional Redis-backed storage.

Installation

1

Install the package

npm install @better-auth/api-key
2

Add the plugin to your auth config

auth.ts
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"

export const auth = betterAuth({
  plugins: [
    apiKey()
  ]
})
3

Migrate the database

npx auth migrate
4

Add the client plugin

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { apiKeyClient } from "@better-auth/api-key/client"

export const authClient = createAuthClient({
  plugins: [
    apiKeyClient()
  ]
})

Usage

Create an API key

const { data: apiKey } = await authClient.apiKey.create({
  name: "My Project Key",
  expiresIn: 60 * 60 * 24 * 30, // 30 days in seconds (optional)
  prefix: "proj_",               // optional custom prefix
  metadata: { environment: "production" }, // optional
})

// The key value is only returned once on creation
console.log(apiKey.key) // "proj_abc123..."
The raw key value is only returned at creation time. Store it securely — it cannot be retrieved again.
Creating a key server-side (for a specific user):
const apiKey = await auth.api.createApiKey({
  body: {
    name: "CI/CD Key",
    userId: "user-id",
    permissions: {
      files: ["read", "write"],
      users: ["read"],
    },
  },
})

Verify an API key

Verification is always server-side. Pass the key from the incoming request:
const result = await auth.api.verifyApiKey({
  body: {
    key: request.headers.get("x-api-key"),
    // Optional: check required permissions
    permissions: {
      files: ["read"],
    },
  },
})

if (result.valid) {
  // result.key contains the API key details (except the raw key value)
  console.log(result.key.name, result.key.referenceId)
} else {
  console.error(result.error?.message)
}
By default, Better Auth looks for the API key in the x-api-key request header.

Get an API key

const { data: key } = await authClient.apiKey.get({
  id: "api-key-id",
})
// Returns everything except the raw key value

List API keys

const { data } = await authClient.apiKey.list({
  query: {
    limit: 10,
    offset: 0,
    sortBy: "createdAt",
    sortDirection: "desc",
  },
})
// { apiKeys, total, limit, offset }

// List organization-owned keys:
const { data: orgKeys } = await authClient.apiKey.list({
  query: { organizationId: "org-id" },
})

Update an API key

await authClient.apiKey.update({
  keyId: "api-key-id",
  name: "Renamed Key",
  // Server-only fields require using auth.api.updateApiKey:
  // enabled, remaining, refillAmount, refillInterval,
  // metadata, expiresIn, rateLimitEnabled, rateLimitTimeWindow,
  // rateLimitMax, permissions
})

Delete an API key

await authClient.apiKey.delete({
  keyId: "api-key-id",
})

Permissions

API keys support fine-grained, resource-based permissions.

Set default permissions

auth.ts
apiKey({
  permissions: {
    defaultPermissions: {
      files: ["read"],
      users: ["read"],
    },
    // Or dynamically:
    // defaultPermissions: async (referenceId, ctx) => ({
    //   files: ["read"],
    // }),
  },
})

Create a key with permissions

const key = await auth.api.createApiKey({
  body: {
    name: "Read-only Key",
    userId: "user-id",
    permissions: {
      files: ["read"],
      projects: ["read"],
    },
  },
})

Verify permissions

const result = await auth.api.verifyApiKey({
  body: {
    key: "api-key-value",
    permissions: { files: ["write"] }, // will be false if key only has ["read"]
  },
})

Using API keys for authentication

Enable session creation from API keys to allow API key holders to authenticate as the associated user:
auth.ts
apiKey({
  enableSessionForAPIKeys: true,
})
With this enabled, if a valid API key is found in the x-api-key header, Better Auth will mock a user session for the request.

Configuration

auth.ts
import { apiKey } from "@better-auth/api-key"

apiKey({
  // Unique ID for this configuration (required with multiple configs)
  configId: "default",

  // Who owns the key: "user" | "organization" (default: "user")
  references: "user",

  // Header name to read API key from (default: "x-api-key")
  apiKeyHeaders: "x-api-key",

  // Length of generated keys (default: 64)
  defaultKeyLength: 64,

  // Default prefix for all keys
  defaultPrefix: "sk_",

  // Require a name on key creation
  requireName: false,

  // Enable storing metadata on keys
  enableMetadata: true,

  // Key expiration config
  keyExpiration: {
    defaultExpiresIn: null,       // null = never expires
    disableCustomExpiresTime: false,
    minExpiresIn: 1,              // minimum days
    maxExpiresIn: 365,            // maximum days
  },

  // Rate limiting
  rateLimit: {
    enabled: true,
    timeWindow: 60 * 1000,        // 1 minute window
    maxRequests: 100,             // max requests per window
  },

  // Enable session creation from API keys
  enableSessionForAPIKeys: false,
})

Secondary storage (Redis)

For high-performance API key lookups, store keys in Redis instead of your primary database:
auth.ts
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"

export const auth = betterAuth({
  secondaryStorage: {
    get: async (key) => await redis.get(key),
    set: async (key, value, ttl) => {
      if (ttl) await redis.set(key, value, { EX: ttl })
      else await redis.set(key, value)
    },
    delete: async (key) => await redis.del(key),
  },
  plugins: [
    apiKey({
      storage: "secondary-storage",
    }),
  ],
})

Multiple configurations

You can run multiple API key configurations side-by-side (e.g., public and private keys) by passing an array directly to apiKey(). Each configuration must have a unique configId:
auth.ts
import { apiKey } from "@better-auth/api-key"

betterAuth({
  plugins: [
    apiKey([
      { configId: "public", defaultPrefix: "pk_" },
      { configId: "secret", defaultPrefix: "sk_" },
    ]),
  ],
})
Specify configId when creating or verifying keys to use a specific configuration.

Schema

The API Key plugin creates an apikey table:
FieldTypeDescription
idstringPrimary key
configIdstringConfiguration ID this key belongs to
namestringOptional human-readable name
startstringFirst few characters of the key (for display)
prefixstringKey prefix (stored in plaintext)
keystringHashed API key
referenceIdstringOwner ID (user ID or organization ID)
enabledbooleanWhether the key is active
expiresAtDateExpiration date
rateLimitEnabledbooleanWhether rate limiting is active
rateLimitTimeWindownumberRate limit time window in ms
rateLimitMaxnumberMax requests per window
requestCountnumberRequests made in current window
remainingnumberRemaining requests (if using remaining count)
refillAmountnumberAmount to refill remaining count
refillIntervalnumberRefill interval in ms
permissionsstringJSON-serialized permissions
metadatastringJSON-serialized metadata
createdAtDateCreation timestamp
updatedAtDateLast update timestamp
API keys are hashed before storage by default. Disabling hashing (disableKeyHashing: true) is strongly discouraged as it exposes raw keys if your database is breached.

Build docs developers (and LLMs) love