# Source: https://www.better-auth.com/llms.txt/docs/plugins/api-key.md

# API Key

API Key plugin for Better Auth.



The API Key plugin allows you to create and manage API keys for your application. It provides a way to authenticate and authorize API requests by verifying API keys.

## Features

* Create, manage, and verify API keys
* [Built-in rate limiting](/docs/plugins/api-key#rate-limiting)
* [Custom expiration times, remaining count, and refill systems](/docs/plugins/api-key#remaining-refill-and-expiration)
* [metadata for API keys](/docs/plugins/api-key#metadata)
* Custom prefix
* [Sessions from API keys](/docs/plugins/api-key#sessions-from-api-keys)
* [Secondary storage support](/docs/plugins/api-key#secondary-storage) for high-performance API key lookups

## Installation

<Steps>
  <Step>
    ### Add Plugin to the server

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

    export const auth = betterAuth({
        plugins: [ // [!code highlight]
            apiKey() // [!code highlight]
        ] // [!code highlight]
    })
    ```
  </Step>

  <Step>
    ### Migrate the database

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items={["migrate", "generate"]}>
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install" persist>
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x @better-auth/cli migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install" persist>
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x @better-auth/cli generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    See the [Schema](#schema) section to add the fields manually.
  </Step>

  <Step>
    ### Add the client plugin

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { apiKeyClient } from "better-auth/client/plugins"

    export const authClient = createAuthClient({
        plugins: [ // [!code highlight]
            apiKeyClient() // [!code highlight]
        ] // [!code highlight]
    })
    ```
  </Step>
</Steps>

## Usage

You can view the list of API Key plugin options [here](/docs/plugins/api-key#api-key-plugin-options).

### Create an API key


### Client Side

```ts
const { data, error } = await authClient.apiKey.create({
    name: project-api-key, // optional
    expiresIn, // optional
    userId: user-id, // optional
    prefix: project-api-key, // optional
    remaining, // optional
    metadata, // optional
});
```

### Server Side

```ts
const data = await auth.api.createApiKey({
    body: {
        name: project-api-key, // optional
        expiresIn, // optional
        userId: user-id, // optional
        prefix: project-api-key, // optional
        remaining, // optional
        metadata, // optional
    }
});
```

### Type Definition

```ts
type createApiKey = {
      /**
       * Name of the Api Key.
       */
      name?: string = 'project-api-key'
      /**
       * Expiration time of the Api Key in seconds.
       */
      expiresIn?: number = 60 * 60 * 24 * 7
      /**
       * User Id of the user that the Api Key belongs to. server-only.
       * @serverOnly
       */
      userId?: string = "user-id"
      /**
       * Prefix of the Api Key.
       */
      prefix?: string = 'project-api-key'
      /**
       * Remaining number of requests. server-only.
       * @serverOnly
       */
      remaining?: number = 100
      /**
       * Metadata of the Api Key.
       */
      metadata?: any | null = { someKey: 'someValue' 
}
```


<Callout>
  API keys are assigned to a user.
</Callout>

#### Result

It'll return the `ApiKey` object which includes the `key` value for you to use.
Otherwise if it throws, it will throw an `APIError`.

***

### Verify an API key


### Client Side

```ts
const { data, error } = await authClient.apiKey.verify({
    key: your_api_key_here,
    permissions, // optional
});
```

### Server Side

```ts
const data = await auth.api.verifyApiKey({
    body: {
        key: your_api_key_here,
        permissions, // optional
    }
});
```

### Type Definition

```ts
type verifyApiKey = {
      /**
       * The key to verify.
       */
      key: string = "your_api_key_here"
      /**
       * The permissions to verify. Optional.
       */
      permissions?: Record<string, string[]>
  
}
```


#### Result

```ts
type Result = {
  valid: boolean;
  error: { message: string; code: string } | null;
  key: Omit<ApiKey, "key"> | null;
};
```

***

### Get an API key


### Client Side

```ts
const { data, error } = await authClient.apiKey.get({
    id: some-api-key-id,
});
```

### Server Side

```ts
const data = await auth.api.getApiKey({
    query: {
        id: some-api-key-id,
    },
    // This endpoint requires session cookies.
    headers: await headers()
});
```

### Type Definition

```ts
type getApiKey = {
      /**
       * The id of the Api Key.
       */
      id: string = "some-api-key-id"
  
}
```


#### Result

You'll receive everything about the API key details, except for the `key` value itself.
If it fails, it will throw an `APIError`.

```ts
type Result = Omit<ApiKey, "key">;
```

***

### Update an API key


### Client Side

```ts
const { data, error } = await authClient.apiKey.update({
    keyId: some-api-key-id,
    userId: some-user-id, // optional
    name: some-api-key-name, // optional
    enabled, // optional
    remaining, // optional
    refillAmount, // optional
    refillInterval, // optional
    metadata, // optional
});
```

### Server Side

```ts
const data = await auth.api.updateApiKey({
    body: {
        keyId: some-api-key-id,
        userId: some-user-id, // optional
        name: some-api-key-name, // optional
        enabled, // optional
        remaining, // optional
        refillAmount, // optional
        refillInterval, // optional
        metadata, // optional
    }
});
```

### Type Definition

```ts
type updateApiKey = {
      /**
       * The id of the Api Key to update.
       */
      keyId: string = "some-api-key-id"
      /**
       * The id of the user which the api key belongs to. server-only.
       * @serverOnly
       */
      userId?: string = "some-user-id"
      /**
       * The name of the key.
       */
      name?: string = "some-api-key-name"
      /**
       * Whether the Api Key is enabled or not. server-only.
       * @serverOnly
       */
      enabled?: boolean = true
      /**
       * The number of remaining requests. server-only.
       * @serverOnly
       */
      remaining?: number = 100
      /**
       * The refill amount. server-only.
       * @serverOnly
       */
      refillAmount?: number = 100
      /**
       * The refill interval in milliseconds. server-only.
       * @serverOnly
       */
      refillInterval?: number = 1000
      /**
       * The metadata of the Api Key. server-only.
       * @serverOnly
       */
      metadata?: any | null = { "key": "value" 
}
```


#### Result

If fails, throws `APIError`.
Otherwise, you'll receive the API Key details, except for the `key` value itself.

***

### Delete an API Key


### Client Side

```ts
const { data, error } = await authClient.apiKey.delete({
    keyId: some-api-key-id,
});
```

### Server Side

```ts
const data = await auth.api.deleteApiKey({
    body: {
        keyId: some-api-key-id,
    },
    // This endpoint requires session cookies.
    headers: await headers()
});
```

### Type Definition

```ts
type deleteApiKey = {
      /**
       * The id of the Api Key to delete.
       */
      keyId: string = "some-api-key-id"
  
}
```


#### Result

If fails, throws `APIError`.
Otherwise, you'll receive:

```ts
type Result = {
  success: boolean;
};
```

***

### List API keys


### Client Side

```ts
const { data, error } = await authClient.apiKey.list({});
```

### Server Side

```ts
const data = await auth.api.listApiKeys({

    // This endpoint requires session cookies.
    headers: await headers()
});
```

### Type Definition

```ts
type listApiKeys = {
  
}
```


#### Result

If fails, throws `APIError`.
Otherwise, you'll receive:

```ts
type Result = ApiKey[];
```

***

### Delete all expired API keys

This function will delete all API keys that have an expired expiration date.


### Client Side

```ts
const { data, error } = await authClient.apiKey.deleteAllExpiredApiKeys({});
```

### Server Side

```ts
const data = await auth.api.deleteAllExpiredApiKeys({});
```

### Type Definition

```ts
type deleteAllExpiredApiKeys = {
  
}
```


<Callout>
  We automatically delete expired API keys every time any apiKey plugin
  endpoints were called, however they are rate-limited to a 10 second cool down
  each call to prevent multiple calls to the database.
</Callout>

***

## Sessions from API keys

Any time an endpoint in Better Auth is called that has a valid API key in the headers, you can automatically create a mock session to represent the user by enabling `sessionForAPIKeys` option.

<Callout type="warn">
  This is generally not recommended, as it can lead to security issues if not used carefully. A leaked api key can be used to impersonate a user.
</Callout>

<Callout>
  **Rate Limiting Note**: When `enableSessionForAPIKeys` is enabled, the API key is validated once per request, and rate limiting is applied accordingly.
  If you manually verify an API key and then fetch a session separately, both operations will increment the rate limit counter. Using `enableSessionForAPIKeys` avoids this double increment.
</Callout>

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      enableSessionForAPIKeys: true,
    }),
  ],
});
```

<Tabs items={['Server']}>
  <Tab value="Server">
    ```ts
    const session = await auth.api.getSession({
          headers: new Headers({
                'x-api-key': apiKey,
          }),
    });
    ```
  </Tab>
</Tabs>

The default header key is `x-api-key`, but this can be changed by setting the `apiKeyHeaders` option in the plugin options.

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      apiKeyHeaders: ["x-api-key", "xyz-api-key"], // or you can pass just a string, eg: "x-api-key"
    }),
  ],
});
```

Or optionally, you can pass an `apiKeyGetter` function to the plugin options, which will be called with the `GenericEndpointContext`, and from there, you should return the API key, or `null` if the request is invalid.

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      apiKeyGetter: (ctx) => {
        const has = ctx.request.headers.has("x-api-key");
        if (!has) return null;
        return ctx.request.headers.get("x-api-key");
      },
    }),
  ],
});
```

## Storage Modes

The API Key plugin supports multiple storage modes for flexible API key management, allowing you to choose the best strategy for your use case.

### Storage Mode Options

#### `"database"` (Default)

Store API keys only in the database adapter. This is the default mode and requires no additional configuration.

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      storage: "database", // Default, can be omitted
    }),
  ],
});
```

#### `"secondary-storage"`

Store API keys only in secondary storage (e.g., Redis).
No fallback to database. Best for high-performance scenarios where all keys are migrated to secondary storage.

```ts
import { createClient } from "redis";
import { betterAuth } from "better-auth";
import { apiKey } from "better-auth/plugins";

const redis = createClient();
await redis.connect();

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",
    }),
  ],
});
```

#### Secondary Storage with Fallback

Check secondary storage first, then fallback to database if not found.

**Read behavior:**

* Checks secondary storage first
* If not found, queries the database
* **Automatically populates secondary storage** when falling back to database (cache warming)
* Ensures frequently accessed keys stay in cache over time

**Write behavior:**

* Writes to **both** database and secondary storage
* Ensures consistency between both storage layers

```ts
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",
      fallbackToDatabase: true,
    }),
  ],
});
```

### Custom Storage Methods

You can provide custom storage methods specifically for API keys, overriding the global `secondaryStorage` configuration:

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      storage: "secondary-storage",
      customStorage: {
        get: async (key) => {
          // Custom get logic for API keys
          return await customStorage.get(key);
        },
        set: async (key, value, ttl) => {
          // Custom set logic for API keys
          await customStorage.set(key, value, ttl);
        },
        delete: async (key) => {
          // Custom delete logic for API keys
          await customStorage.delete(key);
        },
      },
    }),
  ],
});
```

## Rate Limiting

Every API key can have its own rate limit settings. The built-in rate-limiting applies whenever an API key is validated, which includes:

* When verifying an API key via the `/api-key/verify` endpoint
* When using API keys for session creation (if `enableSessionForAPIKeys` is enabled), rate limiting applies to all endpoints that use the API key

For other endpoints/methods that don't use API keys, you should utilize Better Auth's [built-in rate-limiting](/docs/concepts/rate-limit).

<Callout type="warn">
  **Double Rate-Limit Increment**: If you manually verify an API key using `verifyApiKey()` and then fetch a session using `getSession()` with the same API key header, both operations will increment the rate limit counter, resulting in two increments for a single request. To avoid this, either:

  * Use `enableSessionForAPIKeys: true` and let Better Auth handle session creation automatically (recommended)
  * Or verify the API key once and reuse the validated result instead of calling both methods separately
</Callout>

You can refer to the rate-limit default configurations below in the API Key plugin options.

An example default value:

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      rateLimit: {
        enabled: true,
        timeWindow: 1000 * 60 * 60 * 24, // 1 day
        maxRequests: 10, // 10 requests per day
      },
    }),
  ],
});
```

For each API key, you can customize the rate-limit options on create.

<Callout>
  You can only customize the rate-limit options on the server auth instance.
</Callout>

```ts
const apiKey = await auth.api.createApiKey({
  body: {
    rateLimitEnabled: true,
    rateLimitTimeWindow: 1000 * 60 * 60 * 24, // 1 day
    rateLimitMax: 10, // 10 requests per day
  },
  headers: user_headers,
});
```

### How does it work?

The rate limiting system uses a sliding window approach:

1. **First Request**: When an API key is used for the first time (no previous `lastRequest`), the request is allowed and `requestCount` is set to 1.

2. **Within Window**: For subsequent requests within the `timeWindow`, the `requestCount` is incremented. If `requestCount` reaches `rateLimitMax`, the request is rejected with a `RATE_LIMITED` error code.

3. **Window Reset**: If the time since the last request exceeds the `timeWindow`, the window resets: `requestCount` is reset to 1 and `lastRequest` is updated to the current time.

4. **Rate Limit Exceeded**: When a request is rejected due to rate limiting, the error response includes a `tryAgainIn` value (in milliseconds) indicating how long to wait before the window resets.

**Disabling Rate Limiting**:

* **Globally**: Set `rateLimit.enabled: false` in plugin options
* **Per Key**: Set `rateLimitEnabled: false` when creating or updating an API key
* **Null Values**: If `rateLimitTimeWindow` or `rateLimitMax` is `null`, rate limiting is effectively disabled for that key

When rate limiting is disabled (globally or per-key), requests are still allowed but `lastRequest` is updated for tracking purposes.

## Remaining, refill, and expiration

The remaining count is the number of requests left before the API key is disabled.
The refill interval is the interval in milliseconds where the `remaining` count is refilled when the interval has passed since the last refill (or since creation if no refill has occurred yet).
The expiration time is the expiration date of the API key.

### How does it work?

#### Remaining:

Whenever an API key is used, the `remaining` count is updated.
If the `remaining` count is `null`, then there is no cap to key usage.
Otherwise, the `remaining` count is decremented by 1.
If the `remaining` count is 0, then the API key is disabled & removed.

#### refillInterval & refillAmount:

Whenever an API key is created, the `refillInterval` and `refillAmount` are set to `null` by default.
This means that the API key will not be refilled automatically.
However, if both `refillInterval` & `refillAmount` are set, then whenever the API key is used:

* The system checks if the time since the last refill (or since creation if no refill has occurred) exceeds the `refillInterval`
* If the interval has passed, the `remaining` count is reset to `refillAmount` (not incremented)
* The `lastRefillAt` timestamp is updated to the current time

#### Expiration:

Whenever an API key is created, the `expiresAt` is set to `null`.
This means that the API key will never expire.
However, if the `expiresIn` is set, then the API key will expire after the `expiresIn` time.

## Custom Key generation & verification

You can customize the key generation and verification process straight from the plugin options.

Here's an example:

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      customKeyGenerator: (options: {
        length: number;
        prefix: string | undefined;
      }) => {
        const apiKey = mySuperSecretApiKeyGenerator(
          options.length,
          options.prefix
        );
        return apiKey;
      },
      customAPIKeyValidator: async ({ ctx, key }) => {
        const res = await keyService.verify(key)
        return res.valid
      },
    }),
  ],
});
```

<Callout>
  If you're **not** using the `length` property provided by `customKeyGenerator`, you **must** set the `defaultKeyLength` property to how long generated keys will be.

  ```ts
  export const auth = betterAuth({
    plugins: [
      apiKey({
        customKeyGenerator: () => {
          return crypto.randomUUID();
        },
        defaultKeyLength: 36, // Or whatever the length is
      }),
    ],
  });
  ```
</Callout>

If an API key is validated from your `customAPIKeyValidator`, we still must match that against the database's key.
However, by providing this custom function, you can improve the performance of the API key verification process,
as all failed keys can be invalidated without having to query your database.

## Metadata

We allow you to store metadata alongside your API keys. This is useful for storing information about the key, such as a subscription plan for example.

To store metadata, make sure you haven't disabled the metadata feature in the plugin options.

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      enableMetadata: true,
    }),
  ],
});
```

Then, you can store metadata in the `metadata` field of the API key object.

```ts
const apiKey = await auth.api.createApiKey({
  body: {
    metadata: {
      plan: "premium",
    },
  },
});
```

You can then retrieve the metadata from the API key object.

```ts
const apiKey = await auth.api.getApiKey({
  body: {
    keyId: "your_api_key_id_here",
  },
});

console.log(apiKey.metadata.plan); // "premium"
```

## API Key plugin options

`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-user:${userId}` - User's API key list

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

```ts
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">`{ get: (key: string) => Promise<unknown> | unknown; set: (key: string, value: string, ttl?: number) => Promise<void | null | unknown> | void; delete: (key: string) => Promise<void | null | string> | void; }`</span>

Custom storage methods for API keys. If provided, these methods will be used instead of `ctx.context.secondaryStorage`. Custom methods take 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.

```ts
export const auth = betterAuth({
  plugins: [
    apiKey({
      storage: "secondary-storage",
      customStorage: {
        get: async (key) => await customStorage.get(key),
        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={["Vercel", "Cloudflare Workers"]}>
  <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 | ((userId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>) }`</span>

Permissions for the API key.

Read more about permissions [here](/docs/plugins/api-key#permissions).

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

    The default permissions for the API key.
  </Accordion>
</Accordions>

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

Disable hashing of the API key.

â ï¸ 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.

***

## 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

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

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

You can also provide a function that returns permissions dynamically:

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

### Creating API Keys with Permissions

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

```ts
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

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

```ts
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

You can update the permissions of an existing API key:

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

### 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

Table: `apikey`

<DatabaseTable
  fields={[
  {
    name: "id",
    type: "string",
    description: "The ID of the API key.",
    isUnique: true,
    isPrimaryKey: true,
  },
  {
    name: "name",
    type: "string",
    description: "The name of the API key.",
    isOptional: true,
  },
  {
    name: "start",
    type: "string",
    description:
      "The starting characters of the API key. Useful for showing the first few characters of the API key in the UI for the users to easily identify.",
    isOptional: true,
  },
  {
    name: "prefix",
    type: "string",
    description: "The API Key prefix. Stored as plain text.",
    isOptional: true,
  },
  {
    name: "key",
    type: "string",
    description: "The hashed API key itself.",
  },
  {
    name: "userId",
    type: "string",
    description: "The ID of the user associated with the API key.",
    isForeignKey: true,
  },
  {
    name: "refillInterval",
    type: "number",
    description: "The interval to refill the key in milliseconds.",
    isOptional: true,
  },
  {
    name: "refillAmount",
    type: "number",
    description: "The amount to refill the remaining count of the key.",
    isOptional: true,
  },
  {
    name: "lastRefillAt",
    type: "Date",
    description: "The date and time when the key was last refilled.",
    isOptional: true,
  },
  {
    name: "enabled",
    type: "boolean",
    description: "Whether the API key is enabled.",
  },
  {
    name: "rateLimitEnabled",
    type: "boolean",
    description: "Whether the API key has rate limiting enabled.",
  },
  {
    name: "rateLimitTimeWindow",
    type: "number",
    description: "The time window in milliseconds for the rate limit.",
    isOptional: true,
  },
  {
    name: "rateLimitMax",
    type: "number",
    description:
      "The maximum number of requests allowed within the `rateLimitTimeWindow`.",
    isOptional: true,
  },
  {
    name: "requestCount",
    type: "number",
    description:
      "The number of requests made within the rate limit time window.",
  },
  {
    name: "remaining",
    type: "number",
    description: "The number of requests remaining.",
    isOptional: true,
  },
  {
    name: "lastRequest",
    type: "Date",
    description: "The date and time of the last request made to the key.",
    isOptional: true,
  },
  {
    name: "expiresAt",
    type: "Date",
    description: "The date and time when the key will expire.",
    isOptional: true,
  },
  {
    name: "createdAt",
    type: "Date",
    description: "The date and time the API key was created.",
  },
  {
    name: "updatedAt",
    type: "Date",
    description: "The date and time the API key was updated.",
  },
  {
    name: "permissions",
    type: "string",
    description: "The permissions of the key.",
    isOptional: true,
  },
  {
    name: "metadata",
    type: "Object",
    isOptional: true,
    description: "Any additional metadata you want to store with the key.",
  },
]}
/>