Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2c85a42
API Keys docs
wobsoriano Nov 12, 2025
9856518
remove unused file
wobsoriano Nov 12, 2025
3646448
run format
wobsoriano Nov 12, 2025
a1b872b
add APIKeys AIO reference
wobsoriano Nov 17, 2025
a6d6493
rename verify method
wobsoriano Dec 3, 2025
3a98e48
fix incorrect method
wobsoriano Dec 3, 2025
0ec9ccd
polish round
jescalan Dec 4, 2025
09be598
add a little security detail
jescalan Dec 4, 2025
379aa9c
fix lint error
jescalan Dec 4, 2025
e5b048f
fix some links
jescalan Dec 4, 2025
ef2c456
chore: add custom flows for API keys
wobsoriano Dec 8, 2025
60e395d
Merge branch 'main' into rob/api-keys-guides
wobsoriano Dec 8, 2025
0ce85eb
chore: consistent casing
wobsoriano Dec 8, 2025
0580c14
fix links
wobsoriano Dec 8, 2025
40dc37e
remove unnecessary condition
wobsoriano Dec 8, 2025
d696b7d
Merge branch 'main' into rob/api-keys-guides
SarahSoutoul Dec 8, 2025
e8b32ff
clarify beta status, add pricing info
jescalan Dec 8, 2025
cfddab0
docs review
SarahSoutoul Dec 9, 2025
02b7250
Linting + fix build
SarahSoutoul Dec 11, 2025
2c10a1b
docs review 2
SarahSoutoul Dec 11, 2025
29e80df
Linting
SarahSoutoul Dec 11, 2025
c7b5c93
Fix APIKeys component reference
SarahSoutoul Dec 11, 2025
73f6f7a
API Keys docs
wobsoriano Nov 12, 2025
04bc329
remove unused file
wobsoriano Nov 12, 2025
ca46d7c
run format
wobsoriano Nov 12, 2025
45140d2
add APIKeys AIO reference
wobsoriano Nov 17, 2025
4c6ce1c
rename verify method
wobsoriano Dec 3, 2025
938c9b3
fix incorrect method
wobsoriano Dec 3, 2025
70bfbcc
polish round
jescalan Dec 4, 2025
a3daf35
add a little security detail
jescalan Dec 4, 2025
d53c03e
fix lint error
jescalan Dec 4, 2025
a45e1df
fix some links
jescalan Dec 4, 2025
140d2f5
chore: add custom flows for API keys
wobsoriano Dec 8, 2025
b073a10
chore: consistent casing
wobsoriano Dec 8, 2025
2f41785
fix links
wobsoriano Dec 8, 2025
cf27769
remove unnecessary condition
wobsoriano Dec 8, 2025
8614a7c
clarify beta status, add pricing info
jescalan Dec 8, 2025
d0e90d2
docs review
SarahSoutoul Dec 9, 2025
4c134f1
Linting + fix build
SarahSoutoul Dec 11, 2025
6ec0bfb
docs review 2
SarahSoutoul Dec 11, 2025
8f31182
Linting
SarahSoutoul Dec 11, 2025
5e21047
Fix APIKeys component reference
SarahSoutoul Dec 11, 2025
0f48699
fix linting
jescalan Dec 11, 2025
a956862
Update docs/guides/development/machine-auth/api-keys.mdx
jescalan Dec 11, 2025
96e6d1c
Fix instructions to enable API Keys
SarahSoutoul Dec 11, 2025
f526fa6
Fix conflicts
SarahSoutoul Dec 11, 2025
6c99ec6
typo fix, resolves lint error
jescalan Dec 11, 2025
54790af
Fix codeblock tabs
SarahSoutoul Dec 11, 2025
8bc3eac
normalize apikeys object naming
jescalan Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_partials/machine-auth/api-keys-beta-callout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> [!WARNING]
> API keys is currently in beta. The API may change before general availability.
330 changes: 330 additions & 0 deletions docs/guides/development/custom-flows/api-keys/manage-api-keys.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
---
title: Build a custom flow for managing API keys
description: Learn how to use the Clerk API to build a custom flow for creating, listing, and revoking API keys.
---

<Include src="_partials/machine-auth/api-keys-beta-callout" />

<Include src="_partials/custom-flows-callout" />

This guide will demonstrate how to use the Clerk API to build a custom flow for managing [API keys](/docs/guides/development/machine-auth/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.

<Tabs items={["Next.js", "JavaScript"]}>
<Tab>
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 [Active Organization](!active-organization), then the current user.
1. Displays the API keys in a table with options to create new keys and revoke existing ones.
1. Provides a form to create new API keys using [`clerk.apiKeys.create()`](/docs/reference/javascript/api-keys#create) with an optional expiration (subject defaults to active org, then user).
1. Allows revoking API keys using [`clerk.apiKeys.revoke()`](/docs/reference/javascript/api-keys#revoke).

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

```jsx {{ filename: 'app/components/APIKeysManager.tsx', collapsible: true }}
'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>
)
}
```
</Tab>

<Tab>
The following example:

1. Calls [`clerk.apiKeys.getAll()`](/docs/reference/javascript/api-keys#get-all) to retrieve the list of API keys for the active user or organization.
1. Displays the API keys in a table.
1. Provides a form to create new API keys using [`clerk.apiKeys.create()`](/docs/reference/javascript/api-keys#create).
1. Allows revoking API keys using [`clerk.apiKeys.revoke()`](/docs/reference/javascript/api-keys#revoke).

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

<CodeBlockTabs options={["index.html", "main.js"]}>
```html {{ filename: 'index.html', collapsible: true }}
<!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>
```
</CodeBlockTabs>

```js {{ filename: 'main.js', collapsible: true }}
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)
}
}
```
</Tab>
</Tabs>
Loading