HextaUI

API keys

The API keys page of an AI product, like the OpenAI and Anthropic consoles. Create keys with scoped permissions and an expiry, see the secret once with a copy that confirms, revoke with undo, rename in place, rotate with a grace period, and see usage per key.

OpenAI, Anthropic and Vercel all ship the same API keys page: a list of keys you can only see the end of, a dialog to make a new one, and a single chance to copy the secret. API keys is that page as one block that drops into any Settings section. It handles the parts that go wrong: secrets lost because a dialog closed, keys revoked by accident, and lists that jump when something leaves.

Each row shows the key’s name, the masked secret like hx_live_…a3F9, its permissions, the project, when it was made and when it was last used, or Never used. Keys that expire within a week get a warning badge and expired ones say so. Pass usage and a small chart of daily requests sits beside the key on wider screens.

Create key opens a dialog with a name, a project, permissions (all, read only, or restricted to the resources you tick) and an expiry of 30 days, 90 days, never or a date you pick. Then the same dialog turns into the reveal: the full key in a monospace box, a Copy button that confirms with a check, and a box to tick before Done works. Try to close it before copying and it stays open with a warning and a Close anyway button. The tab asks before closing too.

Revoking asks first, naming the key. Then the row collapses away and a toast offers Undo for five seconds, and onRevoke only runs once that window closes, so Undo needs nothing from your server. Leaving the page or creating another key sends any waiting revokes right away. Rotate key swaps in a new secret and can keep the old one working for an hour, a day or a week. Rename in place with F2 or a double-click, Enter to save and Escape to cancel.

  1. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  3. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/api-keys

Wire it to your API

ApiKeySettings shows the keys you pass and calls you to create, revoke and rename them. Throw from any callback and the person sees why, with their input kept. Show ApiKeySettingsSkeleton while the list loads.

"use client"

import * as React from "react"
import { IconKey } from "@tabler/icons-react"

import { SettingsSection, SettingsShell, type SettingsSectionItem } from "../settings/settings"
import {
  ApiKeySettings,
  ApiKeySettingsSkeleton,
  type ApiKey,
  type ApiKeyInput,
  type ApiKeySecret,
} from "@/components/blocks/api-keys/api-keys"

const sections: SettingsSectionItem[] = [
  { id: "api-keys", label: "API keys", icon: <IconKey /> },
]

async function request<T>(input: string, init?: RequestInit) {
  const response = await fetch(input, {
    ...init,
    headers: { "content-type": "application/json", ...init?.headers },
  })
  if (!response.ok) {
    const body = (await response.json().catch(() => ({}))) as { error?: string }
    throw new Error(body.error ?? "Something went wrong.")
  }
  return (response.status === 204 ? null : await response.json()) as T
}

export function ApiKeysPage() {
  const [keys, setKeys] = React.useState<ApiKey[] | null>(null)
  const [failed, setFailed] = React.useState(false)

  const load = React.useCallback(() => {
    setFailed(false)
    request<{ keys: ApiKey[] }>("/api/keys")
      .then((data) => setKeys(data.keys))
      .catch(() => setFailed(true))
  }, [])

  React.useEffect(load, [load])

  const create = async (input: ApiKeyInput) => {
    const result = await request<ApiKeySecret>("/api/keys", {
      method: "POST",
      body: JSON.stringify(input),
    })
    setKeys((current) => [result.key, ...(current ?? [])])
    return result
  }

  const revoke = async (key: ApiKey) => {
    await request(`/api/keys/${key.id}`, { method: "DELETE" })
    setKeys((current) => current?.filter((item) => item.id !== key.id) ?? null)
  }

  const rename = async (key: ApiKey, name: string) => {
    const updated = await request<ApiKey>(`/api/keys/${key.id}`, {
      method: "PATCH",
      body: JSON.stringify({ name }),
    })
    setKeys((current) => current?.map((item) => (item.id === key.id ? updated : item)) ?? null)
  }

  return (
    <SettingsShell sections={sections} className="h-svh">
      <SettingsSection
        id="api-keys"
        status={failed ? "error" : keys ? "ready" : "loading"}
        skeleton={<ApiKeySettingsSkeleton />}
        onRetry={load}
      >
        <ApiKeySettings
          keys={keys ?? []}
          limit={10}
          onCreate={create}
          onRevoke={revoke}
          onRename={rename}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

Make keys on your server

generateApiKey uses crypto.getRandomValues, so it works in Node, edge runtimes and Workers. Store a hash and the last four characters, and send the secret back once.

import { generateApiKey, keyHint, type ApiKey, type ApiKeyInput } from "@/components/blocks/api-keys/api-keys"

async function sha256(value: string) {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")
}

export async function createKey(
  input: ApiKeyInput,
  save: (record: ApiKey & { hash: string }) => Promise<void>
) {
  const secret = generateApiKey("hx_live_")
  const key: ApiKey = {
    id: crypto.randomUUID(),
    name: input.name.slice(0, 60),
    hint: keyHint(secret),
    permission: input.permission,
    resources: input.resources,
    project: input.project,
    createdAt: new Date().toISOString(),
    lastUsedAt: null,
    expiresAt: input.expiresAt ? new Date(input.expiresAt).toISOString() : null,
  }
  await save({ ...key, hash: await sha256(secret) })
  return { key, secret }
}

Rotation, your own scopes and no undo

Pass onRotate to add Rotate key with a grace period for the old secret. Set undoTimeout to 0 to revoke the moment it’s confirmed, and pass resources, prefix and snippet to match your API.

"use client"

import { ApiKeySettings, type ApiKey, type ApiKeyOption, type ApiKeySecret } from "@/components/blocks/api-keys/api-keys"

const resources: ApiKeyOption[] = [
  { value: "chat", label: "Chat", description: "Send messages and read replies." },
  { value: "search", label: "Search", description: "Query your indexed documents." },
]

export function RotatingKeys({
  keys,
  api,
}: {
  keys: ApiKey[]
  api: {
    create: (input: unknown) => Promise<ApiKeySecret>
    revoke: (id: string) => Promise<void>
    rotate: (id: string, expireOldInHours: number) => Promise<ApiKeySecret>
  }
}) {
  return (
    <ApiKeySettings
      keys={keys}
      prefix="sk_test_"
      resources={resources}
      undoTimeout={0}
      expiringSoon={14}
      snippet={`curl https://api.example.com/v1/chat \\\n  -H "Authorization: Bearer $EXAMPLE_KEY"`}
      onCreate={(input) => api.create(input)}
      onRevoke={(key) => api.revoke(key.id)}
      onRotate={(key, { expireOldIn }) => api.rotate(key.id, expireOldIn)}
    />
  )
}

Anatomy

The parts you compose, from the outside in.

PartDescription
ApiKeySettingsThe list, its header with Create key, the limit note, the empty state and every dialog.
ApiKeySettingsSkeletonA loading placeholder shaped like the list, for SettingsSection’s skeleton prop.
generateApiKeyMakes a random key with a prefix from crypto.getRandomValues.
keyHintThe last characters of a secret, to store and show as the masked key.

ApiKeySettings

Also accepts every div prop.

PropTypeDefault
keys{ id, name, hint, permission, resources?, project?, createdAt, lastUsedAt?, expiresAt?, usage? }. Dates take a Date, an ISO string or a timestamp. usage is requests per day, oldest first.
ApiKey[]–
onCreateMake the key and return it with its secret, shown once. Throw to show the message in the dialog.
(input: ApiKeyInput) => Promise<{ key, secret }>–
onRevokeRevoke the key. Runs after the undo window. Throw and the key comes back with Try again.
(key) => void | Promise<void>–
onRenameAdds Rename, F2 and double-click. The new name shows right away and goes back if this throws.
(key, name) => void | Promise<void>–
onRotateAdds Rotate key. expireOldIn is how many hours the old secret keeps working, 0 for right away.
(key, { expireOldIn }) => Promise<{ key, secret }>–
limitThe most keys allowed. At the limit Create key turns off and the note says why.
number–
resourcesWhat a restricted key can be allowed to use. Defaults to models, responses, embeddings, files, agents and usage.
{ value, label, description? }[]–
projectsAdds a Project picker to the create dialog and shows the project on each key.
{ value, label }[]–
prefixShown before the hint in masked keys, unless a key has its own prefix.
string"hx_live_"
snippetThe command shown in the empty state, with a Copy button.
string–
undoTimeoutHow long Undo is offered after revoking, in ms. 0 revokes as soon as it’s confirmed.
number5000
expiringSoonDays before expiry when a key gets the warning badge.
number7
nowFixes the time used for relative dates, for tests and screenshots.
Date–
PropTypeDefault
rowsHow many placeholder keys to show.
number3

generateApiKey

Returns prefix plus random letters and digits.

PropTypeDefault
prefixPut at the start of the key.
string"hx_live_"
lengthRandom characters after the prefix, from 8 to 256.
number40
KeyAction
F2Renames the key whose row has focus.
EnterWhile renaming, saves the name. In the create dialog, creates the key.
EscapeWhile renaming, keeps the old name. In the reveal, warns once if the key wasn’t copied.
TabMoves through each key’s actions, then the dialog fields and buttons.
  • When a key is created, focus moves to Copy and a status message says to copy it now because it won’t be shown again.
  • Confirmations name the key and its masked secret, and focus starts on Cancel.
  • After revoking, focus moves to the next key, or the previous one, or Create key. After Undo it returns to the restored key. After renaming it returns to the key’s actions.
  • Restricted badges name their resources, the usage chart is read as a total, and renames and restores are announced.
  • With reduced motion, rows appear and leave without collapsing.

Code

4 files, added to components/blocks/api-keys.