Skip to main content
Docs

Build a custom flow for managing API keys

Warning

API keys is currently in beta. The API may change before general availability.

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

This guide will demonstrate how to use the Clerk API to build a custom flow for managing API keys. API keys allow your application's users to create keys that grant third-party services access to your application's API endpoints on their behalf.

The following example:

  1. Uses the useAPIKeys() hook from @clerk/nextjs/experimental to retrieve and manage API keys. It will use subject if provided; otherwise it falls back to the , then the current user.
  2. Displays the API keys in a table with options to create new keys and revoke existing ones.
  3. Provides a form to create new API keys using clerk.apiKeys.create()JavaScript Icon with an optional expiration (subject defaults to active org, then user).
  4. Allows revoking API keys using clerk.apiKeys.revoke()JavaScript Icon.

This example is written for Next.js App Router but can be adapted for any React-based framework.

app/components/APIKeysManager.tsx
'use client'

import { useClerk } from '@clerk/nextjs'
import { useAPIKeys } from '@clerk/nextjs/experimental'
import React, { useMemo, useState } from 'react'

export default function APIKeysManager() {
  const clerk = useClerk()
  const [showCreateForm, setShowCreateForm] = useState(false)
  const [formData, setFormData] = useState({
    name: '',
    expirationSeconds: '',
  })

  const { data: apiKeys, isLoading, revalidate } = useAPIKeys()

  const handleCreate = async (e) => {
    e.preventDefault()

    try {
      const expiration =
        formData.expirationSeconds.trim() === '' ? null : Number(formData.expirationSeconds)

      const newApiKey = await clerk.apiKeys.create({
        name: formData.name,
        secondsUntilExpiration: expiration,
      })

      // Store the secret immediately - it won't be available again
      alert(
        `API key created! Secret: ${newApiKey.secret}\n\nMake sure to save this secret - it won't be shown again.`,
      )

      setFormData({ name: '', expirationSeconds: '' })
      setShowCreateForm(false)
      revalidate()
    } catch (error) {
      console.error('Error creating API key:', error)
      alert('Failed to create API key')
    }
  }

  const handleRevoke = async (apiKeyId) => {
    if (!confirm('Are you sure you want to revoke this API key?')) {
      return
    }

    try {
      await clerk.apiKeys.revoke({
        apiKeyId,
        revocationReason: 'Revoked by user',
      })
      revalidate()
    } catch (error) {
      console.error('Error revoking API key:', error)
      alert('Failed to revoke API key')
    }
  }

  if (isLoading) {
    return <div>Loading API keys...</div>
  }

  return (
    <div>
      <h1>API Keys</h1>
      <button onClick={() => setShowCreateForm(!showCreateForm)}>
        {showCreateForm ? 'Cancel' : 'Create API Key'}
      </button>

      {showCreateForm && (
        <form onSubmit={handleCreate}>
          <div>
            <label>
              Name:
              <input
                type="text"
                value={formData.name}
                onChange={(e) => setFormData({ ...formData, name: e.target.value })}
                required
              />
            </label>
          </div>
          <div>
            <label>
              Expiration (seconds, optional):
              <input
                type="number"
                min="0"
                value={formData.expirationSeconds}
                onChange={(e) => setFormData({ ...formData, expirationSeconds: e.target.value })}
              />
            </label>
          </div>
          <button type="submit">Create</button>
        </form>
      )}

      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Expiration</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {!apiKeys || apiKeys.length === 0 ? (
            <tr>
              <td colSpan="3">No API keys found</td>
            </tr>
          ) : (
            apiKeys.map((apiKey) => (
              <tr key={apiKey.id}>
                <td>{apiKey.name}</td>
                <td>
                  {apiKey.expiration
                    ? new Date(apiKey.expiration).toLocaleDateString()
                    : 'No expiration'}
                </td>
                <td>
                  <button onClick={() => handleRevoke(apiKey.id)}>Revoke</button>
                </td>
              </tr>
            ))
          )}
        </tbody>
      </table>
    </div>
  )
}

The following example:

  1. Calls clerk.apiKeys.getAll()JavaScript Icon to retrieve the list of API keys for the active user or organization.
  2. Displays the API keys in a table.
  3. Provides a form to create new API keys using clerk.apiKeys.create()JavaScript Icon.
  4. Allows revoking API keys using clerk.apiKeys.revoke()JavaScript Icon.

Use the following tabs to view the code necessary for the index.html and main.js files.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App - API Keys</title>
  </head>
  <body>
    <div id="app"></div>

    <h1>API Keys</h1>
    <button id="create-btn">Create API Key</button>

    <div id="create-form" style="display: none">
      <form id="api-key-form">
        <div>
          <label>
            Name:
            <input type="text" id="name-input" required />
          </label>
        </div>
        <div>
          <label>
            Expiration (seconds, optional):
            <input type="number" min="0" id="expiration-input" />
          </label>
        </div>
        <button type="submit">Create</button>
        <button type="button" id="cancel-btn">Cancel</button>
      </form>
    </div>

    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Expiration</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody id="api-keys-table-body"></tbody>
    </table>

    <script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
  </body>
</html>
main.js
import { Clerk } from '@clerk/clerk-js'

const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

if (!pubKey) {
  throw new Error('Add your VITE_CLERK_PUBLISHABLE_KEY to .env file')
}

const clerk = new Clerk(pubKey)
await clerk.load()

if (clerk.isSignedIn) {
  await loadAPIKeys()

  // Create form handlers
  document.getElementById('create-btn').addEventListener('click', () => {
    document.getElementById('create-form').style.display = 'block'
  })

  document.getElementById('cancel-btn').addEventListener('click', () => {
    document.getElementById('create-form').style.display = 'none'
    document.getElementById('api-key-form').reset()
  })

  document.getElementById('api-key-form').addEventListener('submit', async (e) => {
    e.preventDefault()

    const name = document.getElementById('name-input').value
    const expirationSeconds = document.getElementById('expiration-input').value
    const secondsUntilExpiration =
      expirationSeconds.trim() === '' ? null : Number(expirationSeconds)

    try {
      const newApiKey = await clerk.apiKeys.create({
        name,
        secondsUntilExpiration,
      })

      // Store the secret immediately - it won't be available again
      alert(
        `API key created! Secret: ${newApiKey.secret}\n\nMake sure to save this secret - it won't be shown again.`,
      )

      document.getElementById('api-key-form').reset()
      document.getElementById('create-form').style.display = 'none'
      await loadAPIKeys()
    } catch (error) {
      console.error('Error creating API key:', error)
      alert('Failed to create API key')
    }
  })
} else {
  // If there is no active user, mount Clerk's <SignIn />
  document.getElementById('app').innerHTML = `
    <div id="sign-in"></div>
  `

  const signInDiv = document.getElementById('sign-in')
  clerk.mountSignIn(signInDiv)
}

async function loadAPIKeys() {
  try {
    const response = await clerk.apiKeys.getAll()

    const tableBody = document.getElementById('api-keys-table-body')
    tableBody.innerHTML = ''

    if (response.data.length === 0) {
      const row = tableBody.insertRow()
      const cell = row.insertCell()
      cell.colSpan = 3
      cell.textContent = 'No API keys found'
      return
    }

    response.data.forEach((apiKey) => {
      const row = tableBody.insertRow()
      row.insertCell().textContent = apiKey.name

      const expirationCell = row.insertCell()
      expirationCell.textContent = apiKey.expiration
        ? new Date(apiKey.expiration).toLocaleDateString()
        : 'No expiration'

      const actionsCell = row.insertCell()
      const revokeBtn = document.createElement('button')
      revokeBtn.textContent = 'Revoke'
      revokeBtn.addEventListener('click', async () => {
        if (confirm('Are you sure you want to revoke this API key?')) {
          try {
            await clerk.apiKeys.revoke({
              apiKeyId: apiKey.id,
              revocationReason: 'Revoked by user',
            })
            await loadAPIKeys()
          } catch (error) {
            console.error('Error revoking API key:', error)
            alert('Failed to revoke API key')
          }
        }
      })
      actionsCell.appendChild(revokeBtn)
    })
  } catch (error) {
    console.error('Error loading API keys:', error)
  }
}

Feedback

What did you think of this content?

Last updated on