@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
http://localhost:4010 when all services start, or on http://localhost:4000 when it is the only service.
Programmatically:
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
/v1 to apiUrl, which matches the emulator routes.
@clerk/nextjs, @clerk/clerk-react, and clerk-js
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.
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:
{ 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-Jwtheader). Development instances carry the client in__clerk_db_jwt; an unknown token is401 dev_browser_unauthenticated.GETandPATCH /v1/environment: auth, display, user, and organization settings fromclerk.instance.GET,PUT,POST, andDELETE /v1/client;DELETE /v1/client/sessions.GET /v1/client/sessions/:id,POST .../touch(active_organization_idsets the active organization),.../end,.../remove,.../tokens(organization_idpicks 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,GETandPATCH .../: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, andDELETE /v1/me/organization_memberships/:organizationId.GET /v1/client/handshakeand the Backend API’sGET /v1/clients/handshake_payload, soauthenticateRequestcan refresh a missing or expired__sessioncookie.- Social sign-in: an
oauth_*strategy returns anexternal_verification_redirect_urlto the emulator’s account chooser at/_clerk/oauth/:strategy, which returns through/v1/oauth_callbackto the attempt’sredirect_url. A new account on sign-in (or a taken one on sign-up) becomestransferable, as on Clerk.
- 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 organizationo: { id, rol, slg, per, fpm }.verifyTokenandauthenticateRequestfrom@clerk/backendaccept them against the emulator’s JWKS. - Passwords are checked for length (
form_password_length_too_short, minimumpassword_min_length) and against a list of breached passwords (form_password_pwned, off withhibp: false). A taken email or username isform_identifier_exists, an unknown one at sign-inform_identifier_not_found, and a wrong passwordform_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_testtake424242and get no code, whiletest_modeis 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. Seedsingle_session_mode: falsefor multi-session apps. - Sessions made here are Backend API sessions:
GET /v1/sessionslists them, and revoking one through the Backend API signs the browser out. GET /npm/*redirects clerk-js and@clerk/uibundle requests to the public npm CDN, so the SDK’s script loader works throughproxyUrl. Offline, serve the bundle yourself (clerkJSUrl).
OAuth and OIDC
GET /.well-known/openid-configuration— OIDC discoveryGET /v1/jwks— JSON Web Key Set (JWKS)GET /oauth/authorize— Authorize: shows the user pickerPOST /oauth/authorize/callback— Submit user picker: redirects toredirect_uriwithcodeandstatePOST /oauth/token— Exchange token (authorization_codeandrefresh_tokengrants)GET /oauth/userinfo— Get user info: for an OAuth access tokenPOST /oauth/token_info— Introspect token (RFC 7662): for the client the token was issued toPOST /oauth/token/revoke— Revoke token (RFC 7009); revoking a refresh token also ends the access tokens of its grantGET /oauth/authorize/deny— Deny consent: the consent screen’s Deny link, redirects witherror=access_denied
Authorization code flow
- Redirect the user to
/oauth/authorizewithclient_id,redirect_uri,scope,state, and optionallynonce,code_challenge, andcode_challenge_method - The emulator renders a user picker with every seeded user
- On selection, the emulator redirects to
redirect_uriwithcodeandstate - Exchange the code via
POST /oauth/tokenwithgrant_type=authorization_code
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.
Consent screen
Seed an OAuth application withconsent_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 thanquerymay repeat.GET /v1/users/count— Count users (same filters)GET /v1/users/:userId— Get userPOST /v1/users— Create user (422 form_identifier_existsfor an email address or username already in use,422 form_param_format_invalidfor a malformed email)PATCH /v1/users/:userId— Update userDELETE /v1/users/:userId— Delete user and its email addresses, memberships, and sessionsPOST /v1/users/:userId/ban— Ban userPOST /v1/users/:userId/unban— Unban userPOST /v1/users/:userId/lock— Lock userPOST /v1/users/:userId/unlock— Unlock userPATCH /v1/users/:userId/metadata— Merge user metadata: deep merge public, private, and unsafe metadata (nullremoves a key)PUT /v1/users/:userId/metadata— Replace user metadata: replaces each metadata field sent; fields left out are keptGET /v1/users/:userId/organization_memberships— List user memberships (limit,offset,role,order_by)POST /v1/users/:userId/verify_password— Verify password ({ "verified": true }, or422 form_password_incorrect)
Email addresses
GET /v1/email_addresses/:emailId— Get email addressPOST /v1/email_addresses— Create email addressPATCH /v1/email_addresses/:emailId— Update email addressDELETE /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 slugPOST /v1/organizations— Create organization (created_byadds that user asorg:admin; a slug already in use, ignoring case, is422 form_identifier_exists)PATCH /v1/organizations/:orgId— Update organization (the same slug check)DELETE /v1/organizations/:orgId— Delete organizationPATCH /v1/organizations/:orgId/metadata— Merge organization metadata: deep merge (nullremoves 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 memberPATCH /v1/organizations/:orgId/memberships/:userId— Update member roleDELETE /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 invitationPOST /v1/organizations/:orgId/invitations— Create invitation (email_address,role,inviter_user_id,public_metadata,private_metadata,expires_in_daysfrom 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,statussuch asactiveorrevoked)GET /v1/sessions/:sessionId— Get sessionPOST /v1/sessions— Create session: for auser_idPOST /v1/sessions/:sessionId/revoke— Revoke sessionPOST /v1/sessions/:sessionId/tokens— Create session token (JWT)POST /v1/sessions/:sessionId/tokens/:template— Create session token from template (404when no template has that name)
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 }withpaginated=true)GET /v1/jwt_templates/:templateId— Get templatePOST /v1/jwt_templates— Create template (name,claims,lifetime,allowed_clock_skew,custom_signing_key,signing_algorithm,signing_key)PATCH /v1/jwt_templates/:templateId— Update templateDELETE /v1/jwt_templates/:templateId— Delete template
Instance
GET /v1/instance— Get instance (clerk.instance.get()):id,environment_type,allowed_origins, and the subdomain settingsPATCH /v1/instance— Update instance: settings such asallowed_origins(an empty list clears it;nullleaves a setting unchanged),support_email,test_mode,hibp;204
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 fromnamewhen omitted),max_allowed_memberships,public_metadata,private_metadata,members. Each member references a seeded user byemail;rolegets anorg:prefix when missing.instance:id(defaults toins_baseline00000000000000000) andallowed_originsforGET /v1/instance, and the Frontend API settings:frontend_api(the host the publishable keys encode, defaultbaseline.clerk.accounts.dev),environment_type(developmentorproduction),application_name,single_session_mode(defaulttrue),attributes(username,first_name,last_name,passwordason,off, orrequired; email is always required),password_min_length(default 8),social_providers(defaultoauth_google,oauth_github),organizations_enabled(defaulttrue),hibp(breached password check, default on),test_mode(the424242code for+clerk_testaddresses, default on in development), andsupport_email.oauth_applications:client_id,name, andredirect_uris(required),client_secret,scopes(defaults toopenid profile email),public(skips the client secret check),consent_screen_enabled(see Consent screen). An entry for an existingclient_id, such as the defaultclerk_baseline_client, updates it.jwt_templates:nameandclaims(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 /_inspectorshows 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.