Skip to main content
Clerk authentication and user management emulation with the Frontend API that @clerk/clerk-js (and @clerk/nextjs and @clerk/clerk-react) signs users up and in through, an OAuth 2.0 / OIDC provider, RS256 session tokens, and the Clerk Backend API for users, email addresses, organizations, memberships, invitations, and sessions.

At a glance

Start

Clerk listens on http://localhost:4010 when all services start, or on http://localhost:4000 when it is the only service. Programmatically:
When the store is empty, the emulator creates a user Test User with primary email [email protected] and password test_password, and an OAuth application Baseline App with client_id clerk_baseline_client, client_secret clerk_baseline_secret, and redirect URI http://localhost:3000/api/auth/callback/clerk. Because an OAuth application always exists, /oauth/authorize and /oauth/token always validate client_id, redirect_uri, and (for non-public applications) client_secret.

@clerk/backend

The SDK appends /v1 to apiUrl, which matches the emulator routes.

@clerk/nextjs, @clerk/clerk-react, and clerk-js

The publishable key encodes clerk.instance.frontend_api (default baseline.clerk.accounts.dev); GET /_clerk/keys prints it for a custom host. The browser SDK sends Frontend API calls to the proxy URL, and the server SDK’s authenticateRequest (clerkMiddleware in Next.js) verifies __session against the emulator’s JWKS and runs the handshake through it. With clerk-js directly:

OIDC clients

Point any OIDC client at the emulator URL as the issuer. Discovery advertises /oauth/authorize, /oauth/token, /oauth/userinfo, /oauth/token_info, /oauth/token/revoke, /v1/jwks, the authorization_code and refresh_token grants, and the offline_access scope.
The callback URL http://localhost:3000/api/auth/callback/clerk matches the default OAuth application.

Auth

Backend API routes (/v1/users, /v1/organizations, /v1/sessions, and so on) require an Authorization: Bearer <secret key> header. When started from the CLI, any non-empty bearer token is accepted, so a placeholder such as sk_test_baseline works. OAuth access and refresh tokens (oat_..., ort_...) are not secret keys and get 401. Errors use the Clerk shape: { "errors": [{ "code", "message", "long_message", "meta" }] }. Error codes are lowercase, as Clerk sends them: resource_not_found, form_identifier_exists, form_param_missing, form_param_value_invalid, authentication_invalid, and so on. Timestamps (created_at, updated_at, last_active_at, expire_at, and the rest) are Unix milliseconds. List responses use offset pagination (limit defaults to 10, maximum 500). GET /v1/users, GET /v1/sessions, and GET /v1/jwt_templates return a plain array, as Clerk does; add paginated=true for { data, total_count }. Every other list returns { data, total_count }.

Frontend API

The emulator also serves Clerk’s Frontend API, which @clerk/clerk-js calls from the browser (and so @clerk/nextjs, @clerk/clerk-react, and the other SDKs built on it). Point the SDK at the emulator with proxyUrl and a publishable key that encodes the instance’s Frontend API host. GET /_clerk/keys returns both:
Routes, with Clerk’s { response, client } envelope and { errors: [{ message, long_message, code, meta }], clerk_trace_id } errors. Writes may be sent as POST with _method, as clerk-js does, and bodies may be form encoded or JSON:
  • POST /v1/dev_browser — Create dev browser token (Clerk-Db-Jwt header). Development instances carry the client in __clerk_db_jwt; an unknown token is 401 dev_browser_unauthenticated.
  • GET and PATCH /v1/environment: auth, display, user, and organization settings from clerk.instance.
  • GET, PUT, POST, and DELETE /v1/client; DELETE /v1/client/sessions.
  • GET /v1/client/sessions/:id, POST .../touch (active_organization_id sets the active organization), .../end, .../remove, .../tokens (organization_id picks the organization, "" the personal account), and .../tokens/:template.
  • Sign-ins: POST /v1/client/sign_ins (identifier and password, email_code, reset_password_email_code, oauth_*, transfer), GET .../:id, .../prepare_first_factor, .../attempt_first_factor, .../reset_password.
  • Sign-ups: POST /v1/client/sign_ups, GET and PATCH .../:id, .../prepare_verification, .../attempt_verification (email codes).
  • /v1/me: GET, PATCH, DELETE, PATCH /v1/me/metadata, POST /v1/me/change_password, email addresses (add, read, verify, delete), GET /v1/me/organization_memberships, and DELETE /v1/me/organization_memberships/:organizationId.
  • GET /v1/client/handshake and the Backend API’s GET /v1/clients/handshake_payload, so authenticateRequest can refresh a missing or expired __session cookie.
  • Social sign-in: an oauth_* strategy returns an external_verification_redirect_url to the emulator’s account chooser at /_clerk/oauth/:strategy, which returns through /v1/oauth_callback to the attempt’s redirect_url. A new account on sign-in (or a taken one on sign-up) becomes transferable, as on Clerk.
Behavior to know:
  • Session tokens (60 seconds) are v2: sid, sub, azp (the request’s origin), iss (https://<frontend_api>), nbf, exp, iat, v: 2, fva, and with an active organization o: { id, rol, slg, per, fpm }. verifyToken and authenticateRequest from @clerk/backend accept them against the emulator’s JWKS.
  • Passwords are checked for length (form_password_length_too_short, minimum password_min_length) and against a list of breached passwords (form_password_pwned, off with hibp: false). A taken email or username is form_identifier_exists, an unknown one at sign-in form_identifier_not_found, and a wrong password form_password_incorrect.
  • Codes are not emailed. GET /_clerk/emails?email_address= lists the codes sent, GET /_clerk/simulate/verification-code?email_address= returns the latest, and the inspector’s Emails tab shows them. Addresses with +clerk_test take 424242 and get no code, while test_mode is on (the default on development instances).
  • Single session mode is on by default: a second sign-in on a signed-in client is session_exists. Seed single_session_mode: false for multi-session apps.
  • Sessions made here are Backend API sessions: GET /v1/sessions lists them, and revoking one through the Backend API signs the browser out.
  • GET /npm/* redirects clerk-js and @clerk/ui bundle requests to the public npm CDN, so the SDK’s script loader works through proxyUrl. Offline, serve the bundle yourself (clerkJSUrl).

OAuth and OIDC

  • GET /.well-known/openid-configuration — OIDC discovery
  • GET /v1/jwks — JSON Web Key Set (JWKS)
  • GET /oauth/authorize — Authorize: shows the user picker
  • POST /oauth/authorize/callback — Submit user picker: redirects to redirect_uri with code and state
  • POST /oauth/token — Exchange token (authorization_code and refresh_token grants)
  • GET /oauth/userinfo — Get user info: for an OAuth access token
  • POST /oauth/token_info — Introspect token (RFC 7662): for the client the token was issued to
  • POST /oauth/token/revoke — Revoke token (RFC 7009); revoking a refresh token also ends the access tokens of its grant
  • GET /oauth/authorize/deny — Deny consent: the consent screen’s Deny link, redirects with error=access_denied

Authorization code flow

  1. Redirect the user to /oauth/authorize with client_id, redirect_uri, scope, state, and optionally nonce, code_challenge, and code_challenge_method
  2. The emulator renders a user picker with every seeded user
  3. On selection, the emulator redirects to redirect_uri with code and state
  4. Exchange the code via POST /oauth/token with grant_type=authorization_code
Only response_type=code is supported. Authorization codes expire after 10 minutes, work once, and only for the client they were issued to. Client credentials can be sent in the body or as Authorization: Basic, for both grants. PKCE supports plain and S256; any other code_challenge_method redirects back with error=invalid_request. The token response contains an opaque access_token (oat_...), token_type, expires_in (3600), and scope. It adds an RS256 id_token when openid was granted, and a refresh_token (ort_...) when offline_access was granted. The id_token carries iss (the emulator URL), sub (the Clerk user ID), aud (the client ID), sid, nonce (when the authorize request sent one), email, email_verified, and name. Each code exchange creates an active session. grant_type=refresh_token rotates the refresh token: the presented one stops working and a new set is returned. It must be sent by the same client and its session must still be active. A scope parameter may narrow the grant; asking for a scope that was not granted returns invalid_scope. Revoking the session ends the refresh token and its access tokens. /oauth/userinfo accepts only a live OAuth access token and answers 401 with WWW-Authenticate: Bearer error="invalid_token" otherwise. It returns sub, plus name, given_name, family_name, preferred_username, and picture for profile, and email and email_verified for email. Introspection takes the token and the client’s credentials (client_secret_basic or client_secret_post); public clients cannot introspect. Revocation also accepts a public client’s client_id alone. Another client’s or an unknown token is {"active": false} on introspection and a quiet 200 on revocation. Seed an OAuth application with consent_screen_enabled: true to show Clerk’s consent screen after the user picks an account: it lists the requested scopes with Allow and Deny. Approval is remembered per user and application, so the screen comes back only for new scopes or when the client sends prompt=consent. Deny redirects with error=access_denied and the state. Applications have no consent screen unless seeded with it.

Signing key

ID tokens and session tokens are signed with a key stored in emulator state (kid: baseline-clerk-1), so they stay valid across snapshot restores and persisted restarts. Set clerk.signing_key.private_jwk to an RSA private JWK to pin the key. See OIDC signing keys.

Users

  • GET /v1/users — List users (limit, offset, query, email_address, external_id, username, user_id, order_by). Filters other than query may repeat.
  • GET /v1/users/count — Count users (same filters)
  • GET /v1/users/:userId — Get user
  • POST /v1/users — Create user (422 form_identifier_exists for an email address or username already in use, 422 form_param_format_invalid for a malformed email)
  • PATCH /v1/users/:userId — Update user
  • DELETE /v1/users/:userId — Delete user and its email addresses, memberships, and sessions
  • POST /v1/users/:userId/ban — Ban user
  • POST /v1/users/:userId/unban — Unban user
  • POST /v1/users/:userId/lock — Lock user
  • POST /v1/users/:userId/unlock — Unlock user
  • PATCH /v1/users/:userId/metadata — Merge user metadata: deep merge public, private, and unsafe metadata (null removes a key)
  • PUT /v1/users/:userId/metadata — Replace user metadata: replaces each metadata field sent; fields left out are kept
  • GET /v1/users/:userId/organization_memberships — List user memberships (limit, offset, role, order_by)
  • POST /v1/users/:userId/verify_password — Verify password ({ "verified": true }, or 422 form_password_incorrect)

Email addresses

  • GET /v1/email_addresses/:emailId — Get email address
  • POST /v1/email_addresses — Create email address
  • PATCH /v1/email_addresses/:emailId — Update email address
  • DELETE /v1/email_addresses/:emailId — Delete email address

Organizations

  • GET /v1/organizations — List organizations (limit, offset, query)
  • GET /v1/organizations/:orgId — Get organization by ID or slug
  • POST /v1/organizations — Create organization (created_by adds that user as org:admin; a slug already in use, ignoring case, is 422 form_identifier_exists)
  • PATCH /v1/organizations/:orgId — Update organization (the same slug check)
  • DELETE /v1/organizations/:orgId — Delete organization
  • PATCH /v1/organizations/:orgId/metadata — Merge organization metadata: deep merge (null removes a key)
  • PUT /v1/organizations/:orgId/metadata — Replace organization metadata: replaces each metadata field sent

Memberships

  • GET /v1/organization_memberships — List all memberships: across every organization (limit, offset, user_id, role, order_by; filters may repeat)
  • GET /v1/organizations/:orgId/memberships — List members (limit, offset, role)
  • POST /v1/organizations/:orgId/memberships — Add member
  • PATCH /v1/organizations/:orgId/memberships/:userId — Update member role
  • DELETE /v1/organizations/:orgId/memberships/:userId — Remove member (returns the removed membership)
  • PATCH /v1/organizations/:orgId/memberships/:userId/metadata — Merge membership metadata: deep merge

Invitations

  • GET /v1/organizations/:orgId/invitations — List invitations (limit, offset, status, which may repeat)
  • GET /v1/organizations/:orgId/invitations/:invitationId — Get invitation
  • POST /v1/organizations/:orgId/invitations — Create invitation (email_address, role, inviter_user_id, public_metadata, private_metadata, expires_in_days from 1 to 365)
  • POST /v1/organizations/:orgId/invitations/bulk — Create invitations in bulk: from an array of invitation bodies; returns { data, total_count }
  • POST /v1/organizations/:orgId/invitations/:invitationId/revoke — Revoke invitation: a pending one

Sessions

  • GET /v1/sessions — List sessions (limit, offset, user_id, status such as active or revoked)
  • GET /v1/sessions/:sessionId — Get session
  • POST /v1/sessions — Create session: for a user_id
  • POST /v1/sessions/:sessionId/revoke — Revoke session
  • POST /v1/sessions/:sessionId/tokens — Create session token (JWT)
  • POST /v1/sessions/:sessionId/tokens/:template — Create session token from template (404 when no template has that name)
Both token routes accept an optional expires_in_seconds (30 to 315360000).

Session tokens

POST /v1/sessions/:sessionId/tokens returns { "object": "token", "jwt": "..." }. The JWT is signed with the same key as the JWKS (kid: baseline-clerk-1) and contains iss, sub, sid, iat, nbf, a one hour exp, metadata when the user has public metadata, and org_id, org_role, org_slug, and org_permissions from the user’s first organization membership. Revoked sessions return session_not_active. POST /v1/sessions/:sessionId/tokens/:template renders the named JWT template instead. The token always has iss, sid, sub, iat, jti, nbf (iat minus allowed_clock_skew), and exp (iat plus expires_in_seconds or the template lifetime), and the rendered claims are merged in. The reserved claims azp, exp, iat, iss, jti, and nbf cannot be set by a template. Claim values may use Clerk shortcodes: {{user.id}}, {{user.external_id}}, {{user.first_name}}, {{user.last_name}}, {{user.full_name}}, {{user.username}}, {{user.image_url}}, {{user.has_image}}, {{user.primary_email_address}}, {{user.email_verified}}, {{user.primary_phone_address}}, {{user.phone_number_verified}}, {{user.two_factor_enabled}}, {{user.created_at}}, {{user.updated_at}}, {{user.public_metadata}} (and {{user.public_metadata.some.path}}), {{user.unsafe_metadata}}, {{org.id}}, {{org.name}}, {{org.slug}}, {{org.image_url}}, {{org.role}}, {{org.public_metadata}}, {{session.id}}, and {{session.actor}}. A value that is a single shortcode keeps its type (a boolean stays a boolean); shortcodes inside a longer string are interpolated. || gives fallbacks, for example {{user.username || user.id}} or {{org.role || 'none'}}. Organization shortcodes come from the user’s first membership. Templates are signed with the instance key unless custom_signing_key is true, in which case signing_algorithm (HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512) and signing_key (a shared secret for HMAC, a PKCS#8 PEM otherwise) are used. That covers integrations such as Supabase, which verify an HS256 token with their JWT secret. lifetime is 30 to 315360000 seconds (default 60) and allowed_clock_skew is 0 to 300 seconds (default 5). Seed templates with jwt_templates:

JWT templates

  • GET /v1/jwt_templates — List templates (a plain array, or { data, total_count } with paginated=true)
  • GET /v1/jwt_templates/:templateId — Get template
  • POST /v1/jwt_templates — Create template (name, claims, lifetime, allowed_clock_skew, custom_signing_key, signing_algorithm, signing_key)
  • PATCH /v1/jwt_templates/:templateId — Update template
  • DELETE /v1/jwt_templates/:templateId — Delete template

Instance

  • GET /v1/instance — Get instance (clerk.instance.get()): id, environment_type, allowed_origins, and the subdomain settings
  • PATCH /v1/instance — Update instance: settings such as allowed_origins (an empty list clears it; null leaves a setting unchanged), support_email, test_mode, hibp; 204
As on Clerk, the environment comes from the secret key: an sk_live_ key reports production, any other key development. Subdomain settings are reported only on production. Unknown settings are 422 form_param_unknown, and an allowed origin must be scheme://host with no path.

Seed config

  • users: email_addresses (required, the first is primary and all are verified), clerk_id, first_name, last_name, username, password, external_id, public_metadata, private_metadata, unsafe_metadata. A user whose first email already exists (such as the default [email protected]) is updated with the fields the entry sets, and keeps its ID.
  • organizations: name (required), clerk_id, slug (derived from name when omitted), max_allowed_memberships, public_metadata, private_metadata, members. Each member references a seeded user by email; role gets an org: prefix when missing.
  • instance: id (defaults to ins_baseline00000000000000000) and allowed_origins for GET /v1/instance, and the Frontend API settings: frontend_api (the host the publishable keys encode, default baseline.clerk.accounts.dev), environment_type (development or production), application_name, single_session_mode (default true), attributes (username, first_name, last_name, password as on, off, or required; email is always required), password_min_length (default 8), social_providers (default oauth_google, oauth_github), organizations_enabled (default true), hibp (breached password check, default on), test_mode (the 424242 code for +clerk_test addresses, default on in development), and support_email.
  • oauth_applications: client_id, name, and redirect_uris (required), client_secret, scopes (defaults to openid profile email), public (skips the client secret check), consent_screen_enabled (see Consent screen). An entry for an existing client_id, such as the default clerk_baseline_client, updates it.
  • jwt_templates: name and claims (required), lifetime, allowed_clock_skew, custom_signing_key, signing_algorithm, signing_key. A template whose name already exists is skipped. See Session tokens.

Inspector

  • GET /_inspector shows users, sessions and clients, sign-in and sign-up attempts, sent codes, and the publishable keys.

Limits

Not built: phone numbers, second factors (TOTP, backup codes), passkeys, web3 wallets, email links, enterprise SSO, captcha, creating organizations from the Frontend API, and the hosted Account Portal pages (their URLs in /v1/environment point at your app).

Conformance

See CONFORMANCE.md for every provider.