# Reference (/docs/plugins/api-key/reference)

API Key plugin options, permissions, and schema reference.



## API Key plugin options [#api-key-plugin-options]

`configId` <span className="opacity-70">`string`</span>

A unique identifier for this configuration. Required when using multiple configurations. Default is `"default"`.

`references` <span className="opacity-70">`"user" | "organization"`</span>

What the API key references. This determines ownership over the API key. Default is `"user"`.

* `"user"`: API keys are owned by users (requires `userId` on creation)
* `"organization"`: API keys are owned by organizations (requires `organizationId` on creation)

`apiKeyHeaders` <span className="opacity-70">`string | string[];`</span>

The header name to check for API key. Default is `x-api-key`.

`customAPIKeyGetter` <span className="opacity-70">`(ctx: GenericEndpointContext) => string | null`</span>

A custom function to get the API key from the context.

`customAPIKeyValidator` <span className="opacity-70">`(options: { ctx: GenericEndpointContext; key: string; }) => boolean | Promise<boolean>`</span>

A custom function to validate the API key.

`customKeyGenerator` <span className="opacity-70">`(options: { length: number; prefix: string | undefined; }) => string | Promise<string>`</span>

A custom function to generate the API key.

`startingCharactersConfig` <span className="opacity-70">`{ shouldStore?: boolean; charactersLength?: number; }`</span>

Customize the starting characters configuration.

<Accordions>
  <Accordion title="startingCharactersConfig Options">
    `shouldStore` <span className="opacity-70">`boolean`</span>

    Whether to store the starting characters in the database.
    If false, we will set `start` to `null`.
    Default is `true`.

    `charactersLength` <span className="opacity-70">`number`</span>

    The length of the starting characters to store in the database.
    This includes the prefix length.
    Default is `6`.
  </Accordion>
</Accordions>

`defaultKeyLength` <span className="opacity-70">`number`</span>

The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length)

`defaultPrefix` <span className="opacity-70">`string`</span>

The prefix of the API key.

Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg `hello_`)

`maximumPrefixLength` <span className="opacity-70">`number`</span>

The maximum length of the prefix.

`minimumPrefixLength` <span className="opacity-70">`number`</span>

The minimum length of the prefix.

`requireName` <span className="opacity-70">`boolean`</span>

Whether to require a name for the API key. Default is `false`.

`maximumNameLength` <span className="opacity-70">`number`</span>

The maximum length of the name.

`minimumNameLength` <span className="opacity-70">`number`</span>

The minimum length of the name.

`enableMetadata` <span className="opacity-70">`boolean`</span>

Whether to enable metadata for an API key.

`keyExpiration` <span className="opacity-70">`{ defaultExpiresIn?: number | null; disableCustomExpiresTime?: boolean; minExpiresIn?: number; maxExpiresIn?: number; }`</span>

Customize the key expiration.

<Accordions>
  <Accordion title="keyExpiration options">
    `defaultExpiresIn` <span className="opacity-70">`number | null`</span>

    The default expires time in milliseconds.
    If `null`, then there will be no expiration time.
    Default is `null`.

    `disableCustomExpiresTime` <span className="opacity-70">`boolean`</span>

    Whether to disable the expires time passed from the client.
    If `true`, the expires time will be based on the default values.
    Default is `false`.

    `minExpiresIn` <span className="opacity-70">`number`</span>

    The minimum expiresIn value allowed to be set from the client. in days.
    Default is `1`.

    `maxExpiresIn` <span className="opacity-70">`number`</span>

    The maximum expiresIn value allowed to be set from the client. in days.
    Default is `365`.
  </Accordion>
</Accordions>

`rateLimit` <span className="opacity-70">`{ enabled?: boolean; timeWindow?: number; maxRequests?: number; }`</span>

Customize the rate-limiting.

<Accordions>
  <Accordion title="rateLimit options">
    `enabled` <span className="opacity-70">`boolean`</span>

    Whether to enable rate limiting. (Default true)

    `timeWindow` <span className="opacity-70">`number`</span>

    The duration in milliseconds where each request is counted.
    Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.

    `maxRequests` <span className="opacity-70">`number`</span>

    Maximum amount of requests allowed within a window.
    Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
  </Accordion>
</Accordions>

`schema` <span className="opacity-70">`InferOptionSchema<ReturnType<typeof apiKeySchema>>`</span>

Custom schema for the API key plugin.

`enableSessionForAPIKeys` <span className="opacity-70">`boolean`</span>

An API Key can represent a valid session, so we can mock a session for the user if we find a valid API key in the request headers. Default is `false`.

`storage` <span className="opacity-70">`"database" | "secondary-storage"`</span>

Storage backend for API keys. Default is `"database"`.

* `"database"`: Store API keys in the database adapter (default)
* `"secondary-storage"`: Store API keys in the configured secondary storage (e.g., Redis)

`fallbackToDatabase` <span className="opacity-70">`boolean`</span>

When `storage` is `"secondary-storage"`, enable fallback to database if key is not found in secondary storage.
Default is `false`.

<Callout>
  When `storage` is set to `"secondary-storage"`, you must configure `secondaryStorage` in your Better Auth options. API keys will be stored using key-value patterns:

  * `api-key:${hashedKey}` - Primary lookup by hashed key
  * `api-key:by-id:${id}` - Lookup by ID
  * `api-key:by-ref:${referenceId}` - Reference's API key list (user or organization)

  If an API key has an expiration date (`expiresAt`), a TTL will be automatically set in secondary storage to ensure automatic cleanup.
</Callout>

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

export const auth = betterAuth({
  secondaryStorage: {
    get: async (key) => {
      return 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",
    }),
  ],
});
```

`customStorage` <span className="opacity-70">`SecondaryStorage`</span>

Custom secondary storage for API keys. If provided, it is used instead of `ctx.context.secondaryStorage`. Custom storage takes precedence over global secondary storage.

Useful when you want to use a different storage backend specifically for API keys, or when you need custom logic for storage operations. It must implement the full secondary-storage contract, including `getAndDelete` and `increment`.

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

export const auth = betterAuth({
  plugins: [
    apiKey({
      storage: "secondary-storage",
      customStorage: {
        get: async (key) => await customStorage.get(key),
        getAndDelete: async (key) => await customStorage.getAndDelete(key),
        increment: async (key, ttl) => await customStorage.increment(key, ttl),
        set: async (key, value, ttl) => await customStorage.set(key, value, ttl),
        delete: async (key) => await customStorage.delete(key), 
      },
    }),
  ],
});
```

`deferUpdates` <span className="opacity-70">`boolean`</span>

Defer non-critical updates (rate limiting counters, timestamps, remaining count) to run after the response is sent using the global `backgroundTasks` handler. This can significantly improve response times on serverless platforms. Default is `false`.

Requires `backgroundTasks.handler` to be configured in the main auth options.

<Callout type="warn">
  Enabling this introduces eventual consistency where the response returns optimistic data before the database is updated. Only enable if your application can tolerate this trade-off for improved latency.
</Callout>

<Tabs items="[&#x22;Vercel&#x22;, &#x22;Cloudflare Workers&#x22;]">
  <Tab value="Vercel">
    ```ts
    import { waitUntil } from "@vercel/functions";

    export const auth = betterAuth({
      advanced: { 
          backgroundTasks: {
             handler: waitUntil,
          },
      }
      plugins: [
        apiKey({
          deferUpdates: true,
        }),
      ],
    });
    ```
  </Tab>

  <Tab value="Cloudflare Workers">
    ```ts
    import { AsyncLocalStorage } from "node:async_hooks";

    const execCtxStorage = new AsyncLocalStorage<ExecutionContext>();

    export const auth = betterAuth({
      advanced: { 
          backgroundTasks: {
             handler: waitUntil,
          },
      }
      plugins: [
        apiKey({
          deferUpdates: true,
        }),
      ],
    });

    // In your request handler, wrap with execCtxStorage.run(ctx, ...)
    ```
  </Tab>
</Tabs>

`permissions` <span className="opacity-70">`{ defaultPermissions?: Statements | ((referenceId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>) }`</span>

Permissions for the API key.

Read more about permissions [below](#permissions).

<Accordions>
  <Accordion title="permissions Options">
    `defaultPermissions` <span className="opacity-70">`Statements | ((referenceId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>)`</span>

    The default permissions for the API key. The `referenceId` is either the user ID or organization ID depending on the `references` setting.
  </Accordion>
</Accordions>

`disableKeyHashing` <span className="opacity-70">`boolean`</span>

Disable hashing of the API key.

<Callout type="warn">
  Security Warning: It's strongly recommended to not disable hashing.
  Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys.
</Callout>

***

## Permissions [#permissions]

API keys can have permissions associated with them, allowing you to control access at a granular level. Permissions are structured as a record of resource types to arrays of allowed actions.

### Setting Default Permissions [#setting-default-permissions]

You can configure default permissions that will be applied to all newly created API keys:

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

export const auth = betterAuth({
  plugins: [
    apiKey({
      permissions: {
        defaultPermissions: {
          files: ["read"],
          users: ["read"],
        },
      },
    }),
  ],
});
```

You can also provide a function that returns permissions dynamically:

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

export const auth = betterAuth({
  plugins: [
    apiKey({
      permissions: {
        defaultPermissions: async (referenceId, ctx) => {
          // referenceId is either userId or orgId depending on config
          // Fetch user/org role or other data to determine permissions
          return {
            files: ["read"],
            users: ["read"],
          };
        },
      },
    }),
  ],
});
```

### Creating API Keys with Permissions [#creating-api-keys-with-permissions]

When creating an API key, you can specify custom permissions:

```ts title="create-api-key.ts"
import { auth } from "@/lib/auth"

const apiKey = await auth.api.createApiKey({
  body: {
    name: "My API Key",
    permissions: {
      files: ["read", "write"],
      users: ["read"],
    },
    userId: "userId",
  },
});
```

### Verifying API Keys with Required Permissions [#verifying-api-keys-with-required-permissions]

When verifying an API key, you can check if it has the required permissions:

```ts title="verify-api-key.ts"
import { auth } from "@/lib/auth"

const result = await auth.api.verifyApiKey({
  body: {
    key: "your_api_key_here",
    permissions: {
      files: ["read"],
    },
  },
});

if (result.valid) {
  // API key is valid and has the required permissions
} else {
  // API key is invalid or doesn't have the required permissions
}
```

### Updating API Key Permissions [#updating-api-key-permissions]

You can update the permissions of an existing API key:

```ts title="update-api-key.ts"
import { auth } from "@/lib/auth"

const apiKey = await auth.api.updateApiKey({
  body: {
    keyId: existingApiKeyId,
    permissions: {
      files: ["read", "write", "delete"],
      users: ["read", "write"],
    },
  },
  headers: user_headers,
});
```

### Permissions Structure [#permissions-structure]

Permissions follow a resource-based structure:

```ts
type Permissions = {
  [resourceType: string]: string[];
};

// Example:
const permissions = {
  files: ["read", "write", "delete"],
  users: ["read"],
  projects: ["read", "write"],
};
```

When verifying an API key, all required permissions must be present in the API key's permissions for validation to succeed.

## Schema [#schema]

Table: `apikey`



<DatabaseTable name="apikey" fields="apiKeyTableFields" />

### Migration from Previous Versions [#migration-from-previous-versions]

If you're upgrading from a previous version, you'll need to migrate the `userId` field to the new reference system:

```sql
-- Add new columns
ALTER TABLE apikey ADD COLUMN config_id VARCHAR(255) NOT NULL DEFAULT 'default';
ALTER TABLE apikey ADD COLUMN reference_id VARCHAR(255);

-- Migrate existing data (copy userId to referenceId)
UPDATE apikey SET reference_id = user_id WHERE reference_id IS NULL;

-- Make reference_id required and add indexes
ALTER TABLE apikey ALTER COLUMN reference_id SET NOT NULL;
CREATE INDEX idx_apikey_reference_id ON apikey(reference_id);
CREATE INDEX idx_apikey_config_id ON apikey(config_id);

-- Optionally drop the old column after verifying migration
-- ALTER TABLE apikey DROP COLUMN user_id;
```

<Callout type="warn">
  **Breaking Change**: The `userId` field has been replaced with `referenceId`. API responses now return `referenceId` instead of `userId`. The owner type (user vs organization) is determined by the configuration's `references` setting, not stored on each key.
</Callout>

