Credential management overview
Credential list, provider directory, delete and rename; entry points for each credential type.
Credential management is how an account manages its own ways to sign in: email, password, passkey, authenticator app, recovery codes, third-party identities. Everything here requires a session token; resource tokens are rejected — an OAuth client has no business changing how a user signs in.
Shared rules
| Rule | Notes |
|---|---|
| Two-step additions | The first step (paths ending in options, or email/options) requires max_age=300 at the start of the flow; the second step (the actual write) only needs a normal session |
| Delete | Requires max_age=300 |
| Rename | No step-up |
| Read | No step-up |
| Last sign-in method | Deleting it is refused, so an account can never lock itself out |
Why the second step is relaxed
The intermediate state (pending email, pending challenge, pending secret) is already bound to a userId and can be consumed once.
It proves "someone just completed a verification on this session"; layering another max_age on top would only bounce the user to a fresh sign-in between two requests.
The credential object
Every credential returned by the list and the add endpoints has the same shape:
| Field | Notes |
|---|---|
id | Credential id, used for delete and rename |
type | email / password / passkey / authenticator / recovery_code / oauth |
display | UI label (email address, device name, provider name) |
provider | Provider of a third-party identity; null otherwise |
label | User-assigned label, may be null |
created | Creation time |
verified | Whether it has been verified |
primary | Whether it is the primary credential (currently only emails have this) |
removable | Whether the API allows deleting it |
Endpoints
| Method | Path | Purpose | Page |
|---|---|---|---|
GET | /v1/auth/credentials | List all credentials of this account | This page |
GET | /v1/auth/providers | Provider directory (public, account-independent) | This page |
GET | /v1/auth/credentials/providers | Provider directory plus link state | This page |
DELETE | /v1/auth/credentials/:credentialId | Remove a credential | This page |
PATCH | /v1/auth/credentials/:credentialId | Rename | This page |
POST | /v1/auth/credentials/email/options etc. | Email add / remove / set primary | |
PUT | /v1/auth/credentials/password | Set or change the password | Password |
POST | /v1/auth/credentials/passkey/options, /credentials/passkey | Register a passkey | Passkey |
POST | /v1/auth/credentials/authenticator/options, /credentials/authenticator | Bind an authenticator app | Authenticator app |
POST | /v1/auth/credentials/provider/:provider | Bind a third-party identity | Third-party identity |
POST | /v1/auth/credentials/recovery-codes | Generate recovery codes | Recovery codes |
GET /credentials
Query: limit (default 20, max 100), cursor.
{
"success": true,
"credentials": [
{
"id": "cred_...",
"type": "email",
"display": "alice@example.com",
"provider": null,
"label": null,
"created": "2026-01-04T10:00:00.000Z",
"verified": true,
"primary": true,
"removable": false
}
],
"next_cursor": null
}Newest first. removable: false usually means "this is the last remaining way to sign in".
GET /providers (public)
{
"success": true,
"providers": [
{ "provider": "github", "display": "GitHub", "available": true }
]
}The sign-in page renders these buttons before the user is signed in, so the endpoint is anonymous. It deliberately omits linked: shipping a field that is always false in a public response means the UI renders a fake value as real, which is worse than omitting it.
available means "this deployment has credentials configured for that provider", not "the code path exists".
GET /credentials/providers
Same fields as the public directory plus linked:
{
"success": true,
"providers": [
{ "provider": "github", "display": "GitHub", "available": true, "linked": true }
]
}It pairs with the "Security settings" page, which needs to know both which providers exist and which ones are already linked.
DELETE /credentials/:credentialId
{ "success": true, "message": "Credential removed" }Deleting writes an audit entry carrying the removed credential's id — when investigating later, the question the user needs answered is exactly "was that the one that disappeared". Unlinking a third-party identity is recorded separately as provider.unbind, because that is the remediation action when an account is being controlled from outside.
PATCH /credentials/:credentialId
| Parameter | Type | Required | Notes |
|---|---|---|---|
label | string | null | ✅ | New label, 1–64 chars; null clears it, an empty string is rejected |
{ "success": true, "credential": { "id": "cred_...", "type": "passkey" } }Only passkeys and third-party identities carry labels; other types are rejected. For a third-party identity the label is purely local display text — renaming it does not change which account is bound.
Related
- How step-up works: Session elevation
- What credential changes leave behind: Audit