End-User Field & Endpoint Matrix
This is the single reference for which end-user attributes Zyphr stores, which endpoint returns each one, and at which credential scope. If you are trying to determine whether a field exists — or where to read it from — start here rather than probing endpoints.
Credential scopes
Every row in the matrix is tagged with the scope that can read (or write) it. There are three distinct credentials in Auth-as-a-Service, and they are not interchangeable:
| Scope | Credential | Runs | Reads |
|---|---|---|---|
| End-user token | X-Application-Key (za_live_pub_* / za_test_pub_*) + Authorization: Bearer <access_token> | Browser / mobile / any client | The signed-in user's own profile and sign-in state |
| Application key/secret | X-Application-Key (za_live_pub_*) + X-Application-Secret (za_live_sec_*) | Server-side only | Any user in the authenticating application (admin scope) |
| Dashboard JWT | Authorization: Bearer <dashboard JWT> | The Zyphr dashboard UI | Account-scoped management. Not for integrators — this is what our own dashboard uses. |
X-Application-Secret and the dashboard JWT are server-side only. Client apps
use the publishable key (za_live_pub_* / za_test_pub_*) plus the end user's own access token — never
the secret. See Client-side Auth for the credential
boundary.
Attribute matrix
Scope uses the abbreviations above: app-key = application key/secret
(server-side admin), user-token = end-user token (self).
| Attribute | Endpoint(s) | Scope | Read/Write |
|---|---|---|---|
id | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read |
email | GET /auth/users/:id, GET /auth/users/me, PATCH /auth/users/:id | app-key, user-token | Read; write (app-key PATCH) |
name | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read; write (PATCH /auth/users/me, app-key PATCH) |
avatar_url | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read; write (PATCH /auth/users/me, app-key PATCH) |
status | GET /auth/users/:id | app-key | Read; write (app-key PATCH, active/suspended) |
is_anonymous | GET /auth/users/:id | app-key | Read |
metadata | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read; write (PATCH /auth/users/me, app-key PATCH) |
email_verified | GET /auth/users/:id, GET /auth/users/:id/auth-methods, GET /auth/users/me | app-key, user-token | Read |
phone_verified | GET /auth/users/:id, GET /auth/users/:id/auth-methods | app-key | Read |
mfa_enabled | GET /auth/users/:id, GET /auth/users/:id/auth-methods | app-key | Read |
first_activity_at | GET /auth/users/:id | app-key | Read |
last_login_at | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read |
created_at | GET /auth/users/:id, GET /auth/users/me | app-key, user-token | Read |
updated_at | GET /auth/users/:id | app-key | Read |
has_password | GET /auth/users/:id/auth-methods | app-key | Read |
oauth_providers | GET /auth/users/:id/auth-methods, GET /auth/oauth/connections | app-key; user-token (self) | Read |
passkeys (count) | GET /auth/users/:id/auth-methods | app-key | Read |
Notes
GET /auth/users/:idis the application-key-scoped directory lookup (the SDK'sgetEndUserById). It returns the core profile and verification flags but nothas_password,oauth_providers, or the passkey count — those live on the sign-in-methods endpoint below.GET /auth/users/meis the self endpoint (end-user token). It returns the subset of profile fields the user is allowed to see about themselves. It does not returnstatus,is_anonymous, orupdated_at.metadatais not trusted for authorization. It is writable by the end user through their own token. For authorization data use trusted custom claims.
Sign-in methods: GET /auth/users/:id/auth-methods
The single, application-key-scoped endpoint that answers "how can this user sign in?" — everything you need to build a sign-in methods settings screen without stitching together several calls. It returns:
{
"data": {
"has_password": true,
"oauth_providers": ["google", "apple"],
"mfa_enabled": false,
"email_verified": true,
"phone_verified": false,
"passkeys": 2
}
}
| Field | Type | Meaning |
|---|---|---|
has_password | boolean | User has a password credential (false for OAuth-only / passwordless users) |
oauth_providers | string[] | Distinct linked OAuth providers (e.g. google, apple) |
mfa_enabled | boolean | Multi-factor authentication is enabled |
email_verified | boolean | Email address is verified |
phone_verified | boolean | Phone number is verified |
passkeys | integer | Number of registered WebAuthn passkeys |
curl https://api.zyphr.dev/v1/auth/users/USER_ID/auth-methods \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx"
has_password, oauth_providers, and the passkey count are only hereThese three are not on GET /auth/users/:id. If you need any of them, call
auth-methods. An end user can read their own linked providers via the
self-scoped GET /auth/oauth/connections (end-user token) — see OAuth.
Application self-metadata: GET / PATCH /auth/application
Application-secret-scoped read and write of the authenticating application's
own metadata — both GET and PATCH require X-Application-Key and
X-Application-Secret. GET returns it; PATCH sets the test_recipients
allowlist. Both are scoped to the calling application via its credentials — there is
no application id in the path or body, so there is no cross-tenant reach.
{
"data": {
"id": "app_abc123",
"name": "My SaaS App",
"environment": "live",
"signing_algorithm": "HS256",
"auth_email_mode": "allowlist",
"test_recipients": ["qa@example.com", "dev@example.com"]
}
}
| Field | Read | Write | Meaning |
|---|---|---|---|
id | ✓ | — | The application ID |
name | ✓ | — | The application's display name |
environment | ✓ | — | live or test (from the credentials that authenticated the request); null for legacy application-level keys |
signing_algorithm | ✓ | — | HS256 (default), RS256, or ES256 — the algorithm your end-user tokens are signed with |
auth_email_mode | ✓ | ✓ (PATCH) | unrestricted (send auth emails to everyone) or allowlist (restrict to test_recipients) — see below |
test_recipients | ✓ | ✓ (PATCH) | The auth-email allowlist, used only in allowlist mode — see below |
# Read your application's metadata (including the current allowlist)
curl https://api.zyphr.dev/v1/auth/application \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx"
signing_algorithm is the authoritative way to check whether your app issues
symmetric (HS256) or asymmetric (RS256/ES256) tokens. Do not infer it from
the presence of a JWKS — see Verifying tokens & JWKS.
The auth-email gate: auth_email_mode + test_recipients
The auth-email gate decides who receives auth emails (magic link, email OTP, password reset, verification). It has an explicit mode — the safe value is no longer inferred from whether the allowlist is empty:
unrestricted(default) — auth emails go to everyone.test_recipientsis ignored. This is the normal production setting.allowlist— only addresses intest_recipientsreceive auth email. Use this for a test/staging app so a database full of synthetic addresses can't mass- bounce your sending reputation. An EMPTY allowlist inallowlistmode fails closed — it blocks all auth email.
An allowlist is a safety feature, so it's tempting to "leave it on" in production.
Don't: allowlist mode with the wrong (or empty) list silently blocks every real
password reset. Production should be unrestricted; staging should be
allowlist with your QA addresses. Because the mode is now stated explicitly,
this is a deliberate choice rather than a side effect of leaving a field blank —
but it does mean you must set it, not infer it.
Set it from your own infrastructure config with a single application-secret-scoped
PATCH (server-side / CI only — never a client operation):
# Staging: restrict auth email to your QA addresses
curl -X PATCH https://api.zyphr.dev/v1/auth/application \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "auth_email_mode": "allowlist", "test_recipients": ["qa@example.com", "dev@example.com"] }'
# Production: send auth email to everyone
curl -X PATCH https://api.zyphr.dev/v1/auth/application \
-H "X-Application-Key: za_live_pub_xxxx" \
-H "X-Application-Secret: za_live_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "auth_email_mode": "unrestricted", "test_recipients": [] }'
Notes:
- Application-SECRET scoped — requires
X-Application-Secret, so this is a server-side / CI operation, never a client one. GETandPATCHare symmetric — bothauth_email_modeandtest_recipientsappear in theGETresponse, so config-management tooling can diff desired vs. actual state.auth_email_modeis optional onPATCH— omit it to change only the list and leave the mode as-is. It must beallowlistorunrestricted; anything else returns400 validation_error. Same for a malformed email / non-arraytest_recipients.- When a send is suppressed by
allowlistmode, it is not silent: amessagesrow is recorded (status: "suppressed", reasonrecipient_not_allowlisted) and anemail.suppressedwebhook fires. The publicPOST /auth/forgot-passwordstill returns200(enumeration-safe), so watch the webhook / the dashboard, not the HTTP response, to confirm delivery in staging.
auth_email_mode / test_recipients gate the auth emails Zyphr sends on your
behalf (magic link, OTP, password reset, verification). If you maintain an
allowlist on your own sending path, it does not cover auth email — those
messages are generated inside Zyphr and never pass through your code. Configure both,
or a staging environment can look healthy on your side while Zyphr silently sends (or
suppresses) nothing.
Commit the mode + allowlist to your deploy pipeline and set them per environment —
unrestricted for production, allowlist + your QA addresses for staging. Migrating
apps were backfilled to preserve prior behavior (a populated list → allowlist, an
empty list → unrestricted), so nothing changed on upgrade; set the mode explicitly
going forward.
Required credential by endpoint
Which credential each end-user endpoint requires. Use this to design your client/server split up front instead of discovering it per 401. The three credentials (see Credential scopes above):
- publishable —
X-Application-Keyonly (za_*_pub_*). Safe for browser / mobile. - secret —
X-Application-Key+X-Application-Secret(za_*_sec_*). Server-side only. - + end-user token — additionally requires
Authorization: Bearer <access_token>(the signed-in user acting on themselves).
The core session flow — login, refresh, and revoke (logout) — is publishable,
so a client app can sign in, keep the session alive, and sign out without a backend
(this is what the auth-react / auth-react-native SDKs are built on). Login is
protected by rate limiting + account lockout + abuse detection, not by the secret.
Endpoints still marked secret (register, magic-link, phone, email-OTP send/verify,
the user directory, claims) run from your backend. This table is derived from the route
middleware and reflects the shipped behavior.
Sessions & tokens
| Endpoint | Required credential |
|---|---|
POST /auth/users/login | publishable |
POST /auth/users/register | secret |
POST /auth/sessions/refresh | publishable |
POST /auth/sessions/revoke | publishable |
POST /auth/sessions/revoke-all | secret + end-user token |
GET /auth/sessions | publishable + end-user token |
Profile (self)
| Endpoint | Required credential |
|---|---|
GET /auth/users/me | publishable + end-user token |
PATCH /auth/users/me | publishable + end-user token |
DELETE /auth/users/me | publishable + end-user token |
POST /auth/users/change-password | publishable + end-user token |
GET /auth/password-requirements | publishable |
Directory & claims (admin, server-side)
| Endpoint | Required credential |
|---|---|
GET /auth/users, GET /auth/users/by-email, GET /auth/users/:id | secret |
GET /auth/users/:id/auth-methods | secret |
PATCH /auth/users/:id, DELETE /auth/users/:id | secret |
POST /auth/users/invite | secret |
GET / PUT /auth/users/:id/claims | secret |
Passwordless & alternate methods
| Endpoint | Required credential |
|---|---|
POST /auth/magic-link/send, /magic-link/verify | secret |
POST /auth/forgot-password, /reset-password, /validate-reset-token | secret |
POST /auth/verify-email/send, /verify-email/resend | secret + end-user token |
POST /auth/verify-email/confirm | secret |
POST /auth/phone/{register,login}/{send,verify} | secret |
POST /auth/email-otp/{register,login}/{send,verify} | secret |
GET /auth/phone/available, /email-otp/available | publishable |
POST /auth/users/anonymous, /game-center, /google-play-games | publishable |
POST /auth/users/convert | publishable + end-user token |
OAuth & WebAuthn
| Endpoint | Required credential |
|---|---|
GET /auth/oauth/providers, /oauth/:provider/authorize | publishable |
GET /auth/oauth/connections, DELETE /auth/oauth/:provider | publishable + end-user token |
GET /auth/oauth/tokens/:provider, POST /oauth/tokens/:provider/refresh | secret |
GET /auth/webauthn/credentials, /webauthn/status | publishable + end-user token |
POST /auth/webauthn/registration/* | secret + end-user token |
POST /auth/webauthn/authentication/* | secret |
See also
- End User Authentication — full registration, login, session, and profile API
- Managing End Users — server-side directory, invites, and trusted custom claims
- Verifying tokens & JWKS — per-algorithm claim sets and offline verification