# API Keys Page

> The developer settings screen for API keys: list, create with scopes and expiry, reveal the secret once, copy, roll, rename and revoke.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add api-keys-page`
- Docs: https://wingo-ui.com/components/api-keys-page

## Interaction

The header names the page, counts the active keys and holds API docs and Create key. Each key shows its name and creator, the masked prefix (ak_live_3f9a••••), status, scopes, created date, when it was last used and when it expires; a key expiring within a week turns amber with "Expires in 5 days". Expired and revoked keys are muted (an active key past its date shows as expired too), and expired ones offer Renew, which opens the create dialog prefilled and replaces the expired row. The ... menu copies the key prefix, renames, rolls or revokes (rolling and revoking ask first, revoking in red). Create key opens a dialog with the cursor in Name: pick scopes and an expiry (the date it lands on shows under it) and press Enter or Create key. Missing fields are named inline, a failed request keeps the form with the error. Then the same dialog shows the secret once in a read-only field, focused and selected, with Copy and a warning; Done turns on after you copy it or tick "I saved this key somewhere safe", and until then outside clicks and Escape do not close it. The secret never goes into a toast, the URL or storage. At the plan's key limit Create key turns off and a hint says why. On phones the keys become cards with the menu as a bottom sheet, and the dialog is a bottom sheet with full-width buttons.

## Installation

```bash
npx wingo-ui@latest add api-keys-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/api-keys-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/api-keys-page.tsx`
- `components/blocks/api-keys-page-data.ts`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Page Header](https://wingo-ui.com/components/page-header) ([Markdown](https://wingo-ui.com/components/page-header.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
"use client"

import { ApiKeysPage } from "@/components/blocks/api-keys-page"
import type { ApiKey } from "@/components/blocks/api-keys-page-data"

export function DeveloperKeys({ keys }: { keys: ApiKey[] }) {
  return (
    <ApiKeysPage
      defaultKeys={keys}
      docsHref="https://docs.acme.com/api"
      maxKeys={10}
      onCreate={async (input) => {
        const res = await fetch("/api/keys", { method: "POST", body: JSON.stringify(input) })
        if (!res.ok) throw new Error("The key was not created. Try again.")
        return res.json() as Promise<{ key: ApiKey; secret: string }>
      }}
      onRoll={async (id) => {
        const res = await fetch(`/api/keys/${id}/roll`, { method: "POST" })
        return res.json() as Promise<{ secret: string }>
      }}
      onRevoke={(id) => fetch(`/api/keys/${id}`, { method: "DELETE" })}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title?` | `ReactNode` | `"API keys"` | The page title. |
| `description?` | `ReactNode` | `"Use these keys to call the API from your servers. Never share them in client code."` | The line under the title. |
| `docsHref?` | `string` | `""` | Adds an API docs link to the header. Empty hides it. |
| `keys?` | `ApiKey[]` |  | Controlled list: { id, name, prefix, createdAt, lastUsedAt?, expiresAt?, scopes, createdBy?, status: "active" \| "expired" \| "revoked" }. Pair it with onKeysChange. |
| `defaultKeys?` | `ApiKey[]` | `"API_KEYS_SAMPLE"` | Starting list while uncontrolled. The sample has an active key, one expiring in 5 days, an expired and a revoked one. |
| `onKeysChange?` | `(keys: ApiKey[]) => void` |  | The list after a create, roll, rename or revoke. |
| `scopes?` | `ApiKeyScope[]` | `"DEFAULT_API_KEY_SCOPES"` | The scope options { value, label, description? }: checkboxes in the create dialog and badges in the list. Default Read, Write and Admin. |
| `expiryOptions?` | `ApiKeyExpiryOption[]` | `"DEFAULT_API_KEY_EXPIRY_OPTIONS"` | The expiry choices { value, label, days } where days null means never. Default 30 days, 90 days, 1 year and Never. |
| `labels?` | `Partial<ApiKeysPageLabels>` |  | Merged over DEFAULT_API_KEYS_PAGE_LABELS: every string of the page, the dialogs and the toasts. |
| `defaultExpiry?` | `string` | `"90d"` | The expiry preselected in the create dialog. |
| `maxKeys?` | `number` | `0` | Active keys allowed. At the limit Create key turns off and an info hint explains it. 0 means no limit. |
| `warnDays?` | `number` | `7` | An active key expiring within this many days shows an amber "Expires in ...". |
| `onCreate?` | `(input: { name, scopes, expiresIn, renews? }) => Promise<{ key: ApiKey; secret: string }>` |  | Create the key on your server and return it with its secret, which the dialog shows once. renews is the id of the expired key a Renew replaces. A rejection keeps the form and shows its message. Without it the page makes a local sample key so the flow can be tried. |
| `onRevoke?` | `(id: string) => void \| Promise<unknown>` |  | Revoke was confirmed. The row turns Revoked once it resolves; a rejection shows in the confirmation. |
| `onRoll?` | `(id: string) => Promise<{ secret: string }>` |  | Adds Roll key to active rows: after a confirmation the new secret is revealed once, like a new key. |
| `onRename?` | `(id: string, name: string) => void \| Promise<unknown>` |  | Adds Rename to the row menu, with a small dialog. |
| `defaultCreateOpen?` | `boolean` | `false` | Opens the create dialog on mount, e.g. from an onboarding link. |
| `now?` | `Date \| number` |  | The reference time for last used and expiry warnings; default the live clock (dates show until it hydrates). |
| `locale?` | `string` | `""` | Intl locale for dates and relative times. Empty falls back to the UIProvider locale, then en-US (the demo offers en-GB and de-DE). |
| `className?` | `string` |  | Classes for the root, a centered column up to 1152px wide. Other div props go to the root too. |
| `classNames?` | `Partial<Record<ApiKeysPageSlot, string>>` |  | Classes for header, limit, table, empty, dialog and reveal. |

## Parts

### DEFAULT_API_KEYS_PAGE_LABELS (constant)

The English strings; spread it and override what you need for another language.

### DEFAULT_API_KEY_SCOPES (constant)

Read, Write and Admin, each with a one-line description.

### DEFAULT_API_KEY_EXPIRY_OPTIONS (constant)

30 days, 90 days, 1 year and Never.

### API_KEYS_SAMPLE (constant)

Four sample keys (api-keys-page-data.ts): active, expiring in 5 days, expired and revoked.

### API_KEYS_NOW (constant)

The reference time the sample is written against (Oct 13, 2026, 10:30); pass it as now in demos and tests.

### generateDemoApiKey (function)

Returns a made-up { prefix, secret } for demos and tests. Real keys come from your server.

### ApiKey (type)

One key: { id, name, prefix, createdAt, lastUsedAt?, expiresAt?, scopes, createdBy?, status }.

### ApiKeyStatus (type)

"active" | "expired" | "revoked".

### ApiKeyScope (type)

{ value, label, description? }: one scope option.

### ApiKeyExpiryOption (type)

{ value, label, days }: one expiry choice; days null never expires.

### ApiKeyCreateInput (type)

What onCreate receives: { name, scopes, expiresIn, renews? }, expiresIn being the expiry option's value and renews the id of the expired key being renewed.

### ApiKeyCreateResult (type)

What onCreate returns: { key, secret }.

### ApiKeysPageLabels (type)

The shape of labels.

## Examples

- **No keys yet**: defaultKeys={[]}: the empty state with Create key and a docs link.
- **Create a key**: defaultCreateOpen: name, scopes and expiry, then the one-time reveal with Copy and Done.
- **At the key limit**: maxKeys={3} with three active keys: Create key turns off and a hint says why.
- **Create fails**: The first request fails: the form keeps its values and shows the error. Try again and the key appears.
- **en-GB dates, 14-day warning**: locale="en-GB", warnDays={14}, custom copy, with Rename and Roll in the row menu.

## See also

- [Settings Page](https://wingo-ui.com/components/settings-page) ([Markdown](https://wingo-ui.com/components/settings-page.md))
- [Settings Layout](https://wingo-ui.com/components/settings-layout) ([Markdown](https://wingo-ui.com/components/settings-layout.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Audit Log](https://wingo-ui.com/components/audit-log) ([Markdown](https://wingo-ui.com/components/audit-log.md))

---

Canonical: https://wingo-ui.com/components/api-keys-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
