Build a custom flow for managing API keys
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:
- Uses the
useAPIKeys()hook from@clerk/nextjs/experimentalto retrieve and manage API keys. It will usesubjectif provided; otherwise it falls back to the , then the current user. - Displays the API keys in a table with options to create new keys and revoke existing ones.
- Provides a form to create new API keys using clerk.apiKeys.create() with an optional expiration (subject defaults to active org, then user).
- Allows revoking API keys using clerk.apiKeys.revoke().
This example is written for Next.js App Router but can be adapted for any React-based framework.
'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:
- Calls clerk.apiKeys.getAll() to retrieve the list of API keys for the active user or organization.
- Displays the API keys in a table.
- Provides a form to create new API keys using clerk.apiKeys.create().
- Allows revoking API keys using clerk.apiKeys.revoke().
Use the following tabs to view the code necessary for the index.html and main.js files.
<!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>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
Last updated on