Skip to main content

End User Authentication

These endpoints handle the core authentication lifecycle for your application's end users: registration, login, session management, and user profiles.

Authentication Headers

End user auth endpoints use application credentials, not dashboard JWTs:

HeaderValueRequired
X-Application-KeyYour app's public key (za_live_pub_xxxx)Always
X-Application-SecretYour app's secret key (za_live_sec_xxxx)Server-side endpoints
AuthorizationBearer <access_token>User-authenticated endpoints
Content-Typeapplication/jsonPOST/PATCH requests

Server-side endpoints (register, login) require both the public key and secret key.

Refresh (/auth/sessions/refresh) needs only the public key — the refresh token is the credential, so it is safe for client-side use. See Refresh Token.

User-authenticated endpoints (profile, sessions) require the public key plus the user's access token. No secret key needed — safe for client-side use.

For the full per-endpoint credential breakdown, see the End-User Field & Endpoint Matrix.

Password Requirements

Before showing a registration form, fetch the application's password requirements for client-side validation:

curl https://api.zyphr.dev/v1/auth/password-requirements \
-H "X-Application-Key: za_live_pub_xxxx"
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/password-requirements', {
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
},
});
const { data } = await response.json();
// data.requirements — { min_length, require_uppercase, require_lowercase, require_numbers, require_special }
info

This endpoint only requires the public key — no secret needed. Safe to call from the frontend.

Registration

curl -X POST https://api.zyphr.dev/v1/auth/users/register \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "SecureP@ss123",
"name": "Jane Doe",
"metadata": { "plan": "pro", "source": "landing-page" }
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/register', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'jane@example.com',
password: 'SecureP@ss123',
name: 'Jane Doe',
metadata: { plan: 'pro', source: 'landing-page' },
}),
});

Parameters

ParameterTypeRequiredDescription
emailstringYesUser's email address
passwordstringYesMust meet application's password requirements
namestringNoDisplay name
metadataobjectNoArbitrary key-value data attached to the user

Response (201 Created)

{
"data": {
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": false,
"metadata": { "plan": "pro", "source": "landing-page" },
"created_at": "2025-01-15T10:00:00Z"
},
"tokens": {
"access_token": "eyJhbGci...",
"refresh_token": "zrt_xxxx",
"expires_in": 3600,
"token_type": "Bearer"
}
}
}

Error Responses

StatusCodeCause
400validation_errorMissing or invalid email/password
400password_validation_errorPassword doesn't meet requirements (includes errors array and requirements)
409conflictEmail already registered

Login

curl -X POST https://api.zyphr.dev/v1/auth/users/login \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "SecureP@ss123"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/login', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'jane@example.com',
password: 'SecureP@ss123',
}),
});

Parameters

ParameterTypeRequiredDescription
emailstringYesUser's email
passwordstringYesUser's password
custom_claimsobjectNoCustom data to embed in the JWT (max 4KB). Must be a flat object.

Standard Response (No MFA)

{
"data": {
"mfa_required": false,
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": true,
"avatar_url": null,
"metadata": { "plan": "pro" },
"last_login_at": "2025-01-15T10:00:00Z",
"mfa_enabled": false
},
"tokens": {
"access_token": "eyJhbGci...",
"refresh_token": "zrt_xxxx",
"expires_in": 3600,
"token_type": "Bearer"
}
}
}

MFA-Required Response

When the user has MFA enabled, login returns a challenge token instead of auth tokens:

{
"data": {
"mfa_required": true,
"user": {
"id": "usr_abc123",
"email": "jane@example.com"
},
"mfa_challenge": {
"token": "mfa_challenge_xxxx",
"expires_at": "2025-01-15T10:05:00Z"
}
},
"meta": {
"message": "MFA verification required. Use POST /v1/auth/mfa/verify with the challenge token."
}
}

See Multi-Factor Authentication for completing the MFA flow.

Error Responses

StatusCodeCause
400validation_errorMissing email/password or invalid custom_claims
401unauthorizedInvalid email or password, or account is not active
403account_lockedToo many failed attempts. Includes locked_until and attempt_count.
403session_limit_exceededMax concurrent sessions reached. Includes max_sessions and current_sessions.

Session Management

Refresh Token

Exchange a refresh token for new access and refresh tokens. This endpoint is publishable-key-safe — it needs only X-Application-Key (no application secret), because the refresh token itself is the credential. That means client apps (browser / mobile) can refresh directly:

curl -X POST https://api.zyphr.dev/v1/auth/sessions/refresh \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "zrt_xxxx",
"custom_claims": { "role": "admin" }
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/sessions/refresh', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY, // publishable key only
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: storedRefreshToken,
}),
});
ParameterTypeRequiredDescription
refresh_tokenstringYesThe refresh token from login or previous refresh
custom_claimsobjectNoUpdate JWT claims. If omitted, preserves existing claims.
Refresh tokens are single-use — persist the new one immediately

Each successful refresh returns a new refresh_token and invalidates the one you presented (single-use rotation — it limits replay of a stolen token). Your client must persist the new access_token and refresh_token atomically before the next refresh. If you lose the new refresh token (e.g. the app crashes after the response but before you save it), the next refresh presents the old, now-revoked token and gets a 401 — which looks like an expired session but is actually a lost-token bug.

If you use @zyphr-dev/auth-core (and the auth-react / auth-react-native bindings), its SessionManager stores the rotated tokens for you and dedupes concurrent refreshes. Hand-rolled clients must replicate this: save first, then proceed.

Application secret still accepted

Sending X-Application-Secret on refresh is harmless (backward compatible) — it is simply no longer required. If you previously refreshed from a server with the secret, that keeps working unchanged.

Revoke Session (Logout)

curl -X POST https://api.zyphr.dev/v1/auth/sessions/revoke \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "refresh_token": "zrt_xxxx" }'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/sessions/revoke', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: storedRefreshToken }),
});

Always returns success (even for invalid tokens) to prevent token enumeration.

Revoke All Sessions

Requires the user's access token. Revokes every active session for the authenticated user:

curl -X POST https://api.zyphr.dev/v1/auth/sessions/revoke-all \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json"
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/sessions/revoke-all', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
// response.data.sessions_revoked — number of sessions revoked

List Active Sessions

curl https://api.zyphr.dev/v1/auth/sessions \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN"
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/sessions', {
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'Authorization': `Bearer ${accessToken}`,
},
});

Returns a list of active sessions with device info (user agent, IP, created/last used timestamps).

Password Management

Zyphr exposes three distinct password endpoints. They are not interchangeable — pick the one that matches what the user knows and how they are (or aren't) authenticated:

EndpointAuthWhen to use
POST /v1/auth/forgot-passwordApp credentialsUser forgot their password. Emails a reset link.
POST /v1/auth/reset-passwordApp credentialsConsumes the emailed token and sets a new password.
POST /v1/auth/users/change-passwordPublic key + user tokenUser is signed in and knows their current password.
  • forgot-passwordreset-password is the two-step recovery flow for a user who is locked out. forgot-password always returns success (to prevent email enumeration) and, if the address matches a real user, emails a reset link. The user then submits the token from that link to reset-password. See Password Reset below.
  • change-password is the in-session flow for a user who is already logged in and wants to rotate their password. It requires the user's access token and proof of the current password — no email round-trip.

Change Password (Authenticated Session)

Changes the signed-in user's password. Uses the public key + the user's access token (no secret key) — safe for client-side use. The user is identified from the access token, never from the request body, so a user can only ever change their own password.

curl -X POST https://api.zyphr.dev/v1/auth/users/change-password \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "OldP@ss123",
"new_password": "NewSecureP@ss456"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/change-password', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
current_password: 'OldP@ss123',
new_password: 'NewSecureP@ss456',
}),
});
const { data } = await response.json();
// data.tokens — a FRESH token pair; replace your stored tokens with these.
Node SDK (recommended)
import { ZyphrClient } from '@zyphr-dev/node-sdk';

const zyphr = new ZyphrClient({
applicationKey: process.env.ZYPHR_APP_PUBLIC_KEY, // za_live_pub_*
accessToken, // applied automatically
});

// Pass camelCase arguments — the SDK serializes them to the snake_case wire
// body (current_password / new_password) for you.
const result = await zyphr.auth.profile.changeEndUserPassword({
currentPassword: 'OldP@ss123',
newPassword: 'NewSecureP@ss456',
});
zyphr.setAccessToken(result.data.tokens.access_token); // store the fresh token
Prefer the typed SDK method over a raw body

The REST endpoint's wire contract is snake_case (current_password / new_password). The typed changeEndUserPassword method takes camelCase arguments and converts them for you — posting { currentPassword, newPassword } directly to the endpoint is rejected with current_password is required. See Client-side Auth.

ParameterTypeRequiredDescription
current_passwordstringYesThe user's existing password. Verified server-side.
new_passwordstringYesThe new password. Must meet the application's password requirements.

On success, Zyphr:

  1. Verifies current_password against the stored hash.
  2. Updates the password.
  3. Revokes all existing sessions for the user (every other logged-in device is signed out).
  4. Returns a fresh token pair so the calling session stays authenticated — replace your stored access_token / refresh_token with the new ones.
  5. Emits the user.password_changed webhook (with method: "session").

Response (200 OK)

{
"data": {
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": true,
"avatar_url": null,
"metadata": { "plan": "pro" }
},
"tokens": {
"access_token": "eyJhbGci...",
"refresh_token": "zrt_xxxx",
"expires_in": 3600,
"token_type": "Bearer"
}
}
}

Error Responses

StatusCodeCause
400validation_errorMissing current_password or new_password
400password_validation_errorNew password doesn't meet requirements (includes errors array and requirements)
400no_password_setThe account has no password to change (e.g. an OAuth-only account). Route the user through your set-password / OAuth-link flow instead.
401invalid_current_passwordThe supplied current_password is incorrect
Sessions are revoked on change

Because a password change revokes all existing sessions, other devices must sign in again. The calling session survives only because a new token pair is issued in the response — be sure to persist it.

Password Reset

The two-step recovery flow for a user who has forgotten their password. Both endpoints use application credentials (public key + secret key) and are server-side.

Request a Reset (forgot-password)

Emails a reset link to the user. Always returns success — even for an unknown address — to prevent email enumeration.

curl -X POST https://api.zyphr.dev/v1/auth/forgot-password \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com" }'
ParameterTypeRequiredDescription
emailstringYesThe user's email address
redirect_urlstringNoWhere the reset link should point. Must match one of the application's allowed redirect URIs.

Consume the Token (reset-password)

Sets a new password using the token from the reset email.

curl -X POST https://api.zyphr.dev/v1/auth/reset-password \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"token": "RESET_TOKEN_FROM_EMAIL",
"new_password": "NewSecureP@ss456"
}'
ParameterTypeRequiredDescription
tokenstringYesThe reset token delivered in the email
new_passwordstringYesThe new password. Must meet the application's password requirements.

Like change-password, a successful reset revokes all of the user's existing sessions and emits user.password_changed. Unlike change-password, it does not return a token pair — the user logs in fresh with their new password.

StatusCodeCause
400validation_errorMissing token or new_password
400password_validation_errorNew password doesn't meet requirements
400invalid_tokenThe reset token is invalid or expired

User Profile

These endpoints use the public key + user access token (no secret key needed).

Get Current User

curl https://api.zyphr.dev/v1/auth/users/me \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN"
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/me', {
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'Authorization': `Bearer ${accessToken}`,
},
});

Response

{
"data": {
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": true,
"avatar_url": "https://example.com/avatar.jpg",
"metadata": { "plan": "pro" },
"created_at": "2025-01-15T10:00:00Z",
"last_login_at": "2025-01-20T08:30:00Z"
}
}
}

Update Profile

curl -X PATCH https://api.zyphr.dev/v1/auth/users/me \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Smith",
"avatar_url": "https://example.com/new-avatar.jpg",
"metadata": { "plan": "enterprise" }
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/me', {
method: 'PATCH',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Jane Smith',
metadata: { plan: 'enterprise' },
}),
});
ParameterTypeRequiredDescription
namestringNoUpdated display name
avatar_urlstringNoProfile image URL
metadataobjectNoReplaces existing metadata

Delete Account (GDPR Self-Service)

Users can delete their own account. This is a soft delete — the account is marked as deleted and all sessions are revoked.

curl -X DELETE https://api.zyphr.dev/v1/auth/users/me \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "Authorization: Bearer ACCESS_TOKEN"
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/users/me', {
method: 'DELETE',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'Authorization': `Bearer ${accessToken}`,
},
});
StatusCodeCause
200Account deleted successfully
404not_foundUser not found
410goneAccount already deleted

Endpoint Reference

MethodEndpointAuthDescription
GET/v1/auth/password-requirementsPublic keyGet password requirements
POST/v1/auth/users/registerApp credentialsRegister new user
POST/v1/auth/users/loginApp credentialsLogin with email/password
POST/v1/auth/sessions/refreshApp credentialsRefresh access token
POST/v1/auth/sessions/revokeApp credentialsRevoke session (logout)
POST/v1/auth/sessions/revoke-allApp credentials + user tokenRevoke all sessions
GET/v1/auth/sessionsPublic key + user tokenList active sessions
POST/v1/auth/users/change-passwordPublic key + user tokenChange password (in-session; knows current password)
POST/v1/auth/forgot-passwordApp credentialsEmail a password reset link
POST/v1/auth/reset-passwordApp credentialsConsume the emailed token, set a new password
GET/v1/auth/users/mePublic key + user tokenGet current user
PATCH/v1/auth/users/mePublic key + user tokenUpdate profile
DELETE/v1/auth/users/mePublic key + user tokenDelete account