Appearance
Integrate your app
This is the implementation guide, organised by platform. Before you start you need a registered client — see Before you start.
Pick your path
Every platform section below offers two ways to integrate. They cover the same result — you don't need both.
Follow the guide — step-by-step, you write the code. Best if you want to understand what you're building, you're debugging an existing integration, or your stack isn't covered by a prompt.
Use an AI prompt — paste the prompt block into Claude, Cursor, or any agent. Best if you want a working skeleton fast and will review the output before shipping.
What every integration does
Regardless of platform, the shape is always the same:
- Use Authorization Code + PKCE with
code_challenge_method=S256. PKCE is required on every client;response_typemust becode. Implicit flow andresponse_type=tokenare not supported. - Discover endpoints from
{issuer}/.well-known/openid-configurationinstead of hardcoding them. - Send the user to
/authorize, where EntryIdP runs the face liveness check in its own UI. - Receive
code+stateat your registered redirect URI, verifystate, and exchange the code at/token. - Validate the ID token, then call
/userinfofor profile/identity claims.
There are no typed credentials, OTPs, or social logins anywhere in this flow — the liveness check is the only credential.
Two EntryIdP-specific facts drive the platform sections:
- Login vs enrol — a plain
/authorizeis a login (existing face)./authorize?prompt=createis enrolment (first-time face). See Topic A in each section. - Mobile callbacks are custom URL schemes only — EntryIdP does not host
apple-app-site-associationorassetlinks.json, so Universal Links / App Links are not available.
Web — SPA (React / Vue / Svelte)
A browser-only app with no backend. Use a public client (no client_secret) secured by PKCE.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login:
userManager.signinRedirect()→ standard/authorize. - Enrol: the same call with
{ extraQueryParams: { prompt: 'create' } }→/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): render two buttons — "Sign in" (login) and "Create account" (enrol with
prompt=create). Simplest, and new users skip a wasted round-trip. - Option 2: attempt login first; if the callback returns
error=access_deniedwitherror_description=user_not_registered, restart the flow withprompt=create.
B — Redirect callback setup
Your framework's router handles the redirect URI like any other route (e.g. /callback). On that route, call your library's signinRedirectCallback() to complete the exchange. No platform-specific work beyond registering the route's full URL as your redirect URI.
C — Secure token storage
| Token | Where |
|---|---|
| Access token | In memory only (a JS variable or library state) — never localStorage. |
| Refresh token | Do not request offline_access. SPAs cannot store refresh tokens safely; prompt for re-authentication when the access token expires. |
Use an AI prompt
Add EntryIdP biometric OIDC login to this SPA (React, Vue, or Svelte).
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not add any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
it from an env var, e.g. VITE_ENTRYIDP_ISSUER — never hardcode).
Before writing code:
1. Read package.json to confirm the framework and bundler (Vite, CRA, etc.).
2. Check for an existing OIDC library (oidc-client-ts). If none, install oidc-client-ts.
Implementation:
- Authorization Code + PKCE only. code_challenge_method=S256. Never use implicit flow or
response_type=token.
- This is a browser-only PUBLIC client: no client_secret in any file.
- Do NOT request offline_access — refresh tokens cannot be stored securely in a browser.
- Keep the access token in memory (UserManager state). Never use localStorage.
- Discover endpoints from {issuer}/.well-known/openid-configuration. Do not hardcode URLs.
- Redirect URI from VITE_ENTRYIDP_REDIRECT_URI; it must exactly match the registered URI.
Login vs enrol (EntryIdP-specific):
- Add a "Sign in" button → signinRedirect() (plain /authorize = login of an existing face).
- Add a "Create account" button → signinRedirect with extraQueryParams { prompt: 'create' }
(first-time face enrolment).
- In the callback, if the URL contains error=access_denied and
error_description=user_not_registered, send the user to the "Create account" flow.
Files: src/auth/entryidp.ts (UserManager config), an AuthProvider/context, a Callback
route that calls signinRedirectCallback(), Login/Logout buttons, and .env.example.
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret anywhere.
- No offline_access scope; no refresh tokens.
- No implicit flow; endpoints fetched from discovery, not hardcoded.Web — server-rendered (Next.js / ASP.NET Core / Express)
An app with a backend. Use a confidential client: keep the client_secret server-side and submit it as client_secret_post at /token.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login: start the OIDC challenge as usual →
/authorize. - Enrol: add
prompt=createto the authorization request (most libraries expose this as an extra/authorization parameter) →/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): two server routes —
/login(plain) and/signup(addsprompt=create) — behind two buttons. - Option 2: on the callback, detect
error=access_denied+error_description=user_not_registeredand redirect the user into theprompt=createflow.
B — Redirect callback setup
A standard HTTP handler at your registered redirect path (e.g. /auth/callback, or /signin-oidc if you use the ASP.NET Core OIDC middleware default). Verify state against the value you stored server-side before exchanging the code. Nothing platform-specific.
C — Secure token storage
| Token | Where |
|---|---|
| Access token | Server-side session. Never send it to the browser. |
| Refresh token | Encrypted DB column or server-side encrypted session. Never expose to the browser. |
Refresh tokens rotate on every use — see Refresh tokens rotate below.
Use an AI prompt
Add EntryIdP biometric OIDC login to this server-rendered app (Next.js, ASP.NET Core,
or Express).
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not add any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from config/env — never hardcode).
Before writing code:
1. Detect the framework and version from package.json or the .csproj file.
2. Use the framework's standard OIDC integration:
- Next.js: next-auth with a custom OIDC provider (wellKnown = discovery URL).
- ASP.NET Core: AddOpenIdConnect with Authority = issuer, ResponseType = "code",
UsePkce = true, SaveTokens = true, GetClaimsFromUserInfoEndpoint = true.
- Express: openid-client with Issuer.discover(issuer).
3. Confirm sessions are persisted server-side, not in a plain client cookie.
Implementation:
- Authorization Code + PKCE. code_challenge_method=S256. No implicit flow, never
response_type=token, and never grant_type=client_credentials.
- This is a CONFIDENTIAL client: client_secret stays server-side only (env var / user-
secrets / Key Vault). It must never appear in client-rendered code or be committed.
- Request scopes: openid profile offline_access (adjust to what was registered).
- Discover all endpoints from {issuer}/.well-known/openid-configuration.
- Store access_token and refresh_token in the server-side session; never send the refresh
token to the browser.
- Validate the ID token with the library (verify signature via JWKS, iss, aud, exp, nonce).
Do not decode JWTs without verifying.
Login vs enrol (EntryIdP-specific):
- /login route → plain /authorize (login of an existing face).
- /signup route → same request plus prompt=create (first-time face enrolment).
- On the callback, if error=access_denied and error_description=user_not_registered,
redirect the user into the /signup (prompt=create) flow.
Refresh handling: on refresh, ALWAYS overwrite the stored refresh_token with the new one
from the response (EntryIdP rotates refresh tokens and burns the family on replay).
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret in client-side bundles or committed config.
- No client_credentials grant. No implicit flow.
- Endpoints fetched from discovery, not hardcoded.iOS — native (Swift + AppAuth-iOS)
Use a public client with PKCE. Drive the flow with AppAuth-iOS.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login: build the authorization request as usual →
/authorize. - Enrol: add
prompt=createviaOIDAuthorizationRequest'sadditionalParameters→/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): two buttons — "Sign in" and "Create account" — the latter passing
["prompt": "create"]. - Option 2: attempt login; if the redirect returns
error=access_deniedwitherror_description=user_not_registered, retry withprompt=create.
B — Redirect callback setup
- Use
ASWebAuthenticationSession(AppAuth uses it under the hood). Do not useWKWebView— Apple rejects apps that put auth in an embedded web view. - Symbol names (AppAuth-iOS 1.7.x): drive the flow with
OIDExternalUserAgentIOS(presenting:)(a presentingUIViewController) — there is noOIDExternalUserAgentASWebAuthenticationSessiontype. Read OAuth errors viaOIDOAuthErrorResponseErrorKeyinuserInfo(notOIDOAuthErrorResponseKey). - Use a custom URL scheme. EntryIdP does not host
apple-app-site-association, so Universal Links are not available. - Register the scheme in
Info.plistunderCFBundleURLTypes. The redirect URI (e.g.com.yourapp://callback) must exactly match what was registered for your client.
C — Secure token storage
| Token | Where |
|---|---|
| Access token | Keychain — persist AppAuth's OIDAuthState (e.g. via NSKeyedArchiver / encode(with:)). |
| Refresh token | Same Keychain entry — AppAuth's OIDAuthState bundles both tokens. |
Use an AI prompt
Add EntryIdP biometric OIDC login to this native iOS app (Swift) using AppAuth-iOS.
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not build any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from configuration — never hardcode).
Before writing code:
1. Confirm the dependency manager (SPM / CocoaPods) and add AppAuth-iOS if missing.
2. Find Info.plist and AppDelegate / SceneDelegate to wire up the redirect.
Implementation:
- Authorization Code + PKCE only (AppAuth adds PKCE automatically with S256). Never use
implicit flow or response_type=token.
- PUBLIC client: no client_secret anywhere.
- Discover config with OIDAuthorizationService.discoverConfiguration(forIssuer:).
- Use ASWebAuthenticationSession (AppAuth default). Do NOT use WKWebView for auth.
- Use the correct AppAuth-iOS symbol names (verified against 1.7.x — do NOT invent variants):
* External user agent: OIDExternalUserAgentIOS(presenting: viewController). It uses
ASWebAuthenticationSession internally and takes a PRESENTING UIViewController — NOT an
ASWebAuthenticationPresentationContextProviding. There is NO type named
OIDExternalUserAgentASWebAuthenticationSession.
* OAuth error key: OIDOAuthErrorResponseErrorKey (in userInfo). There is NO
OIDOAuthErrorResponseKey. To read error_description use OIDOAuthErrorFieldErrorDescription.
- Redirect URI is a CUSTOM URL SCHEME (e.g. com.yourapp://callback) registered in
Info.plist under CFBundleURLTypes. EntryIdP does NOT support Universal Links — do not
configure apple-app-site-association.
- Persist OIDAuthState to the Keychain (it holds both access and refresh tokens). Never
store tokens in UserDefaults.
Login vs enrol (EntryIdP-specific):
- "Sign in" → standard authorization request (login of an existing face).
- "Create account" → same request with additionalParameters ["prompt": "create"]
(first-time face enrolment).
- If the auth response returns error=access_denied with
error_description=user_not_registered, retry with prompt=create.
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret.
- No WKWebView for authentication; no Universal Links.
- Custom URL scheme redirect only; endpoints from discovery, not hardcoded.Android — native (Kotlin + AppAuth-Android)
Use a public client with PKCE. Drive the flow with AppAuth-Android.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login: build the
AuthorizationRequestas usual →/authorize. - Enrol: add
prompt=createviaAuthorizationRequest.Builder.setAdditionalParameters(mapOf("prompt" to "create"))→/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): "Sign in" and "Create account" buttons, the latter setting
prompt=create. - Option 2: attempt login; if the result carries
error=access_denied+error_description=user_not_registered, retry withprompt=create.
B — Redirect callback setup
- Use Chrome Custom Tabs (AppAuth uses them automatically). Do not use a
WebView. - Use a custom URL scheme. EntryIdP does not host
assetlinks.json, so App Links are not available. - Add an
<intent-filter>with yourandroid:schemeto the redirect activity inAndroidManifest.xml. The redirect URI (e.g.com.yourapp://callback) must exactly match the registered value.
C — Secure token storage
| Token | Where |
|---|---|
| Access token | EncryptedSharedPreferences backed by the Android Keystore. |
| Refresh token | Same store (persist AppAuth's serialized AuthState). |
Use an AI prompt
Add EntryIdP biometric OIDC login to this native Android app (Kotlin) using AppAuth-Android.
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not build any sign-in form or
credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from config — never hardcode in logic).
Before writing code:
1. Add net.openid:appauth to build.gradle if missing. Also add inside defaultConfig:
manifestPlaceholders["appAuthRedirectScheme"] = "<your-custom-scheme>"
This is required — AppAuth's bundled manifest uses this placeholder and the Gradle
manifest merger will fail without it.
2. Open AndroidManifest.xml and declare net.openid.appauth.RedirectUriReceiverActivity
with tools:node="merge" and an <intent-filter> for your custom scheme.
Implementation:
- Authorization Code + PKCE only. Never implicit flow or response_type=token.
- PUBLIC client: no client_secret anywhere. Use NoClientAuthentication.INSTANCE when
calling performTokenRequest.
- PKCE: AppAuth does NOT add PKCE automatically. Call
setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier()) on the request builder
(S256 challenge is derived from the verifier automatically).
- Discovery: do NOT use AuthorizationServiceConfiguration.fetchFromIssuer — AppAuth
requires subject_types_supported in the discovery document and throws
MissingArgumentException when it is absent. Instead, fetch
{issuer}/.well-known/openid-configuration manually over HTTP on a background thread,
extract authorization_endpoint and token_endpoint, and construct
AuthorizationServiceConfiguration(authEndpointUri, tokenEndpointUri) directly.
- Use Chrome Custom Tabs (AppAuth default). Do NOT use a WebView for auth.
- Redirect URI is a CUSTOM URL SCHEME (e.g. com.yourapp://callback) declared as an
<intent-filter> in AndroidManifest.xml. EntryIdP does NOT support App Links — do not
configure assetlinks.json.
- Persist AppAuth AuthState in EncryptedSharedPreferences (Android Keystore-backed). It
holds both access and refresh tokens.
Login vs enrol (EntryIdP-specific):
- "Sign in" → standard AuthorizationRequest (login of an existing face).
- "Create account" → setAdditionalParameters(mapOf("prompt" to "create"))
(first-time face enrolment).
- If the result is error=access_denied with error_description=user_not_registered,
retry with prompt=create.
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret.
- No WebView for authentication; no App Links.
- Custom URL scheme redirect only; endpoints from manual discovery fetch, not hardcoded.React Native (Expo or bare)
Use a public client with PKCE. Expo managed → expo-auth-session; bare → react-native-app-auth.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login: start the auth request as usual →
/authorize. - Enrol: add
prompt=create—expo-auth-sessionvia the request'sextraParams,react-native-app-authviaadditionalParameters→/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): "Sign in" and "Create account" buttons, the latter passing
prompt=create. - Option 2: attempt login; on
error=access_denied+error_description=user_not_registered, retry withprompt=create.
B — Redirect callback setup
- Expo managed:
AuthSession.makeRedirectUri({ scheme: 'com.yourapp' })and setschemeinapp.json. Use a custom URL scheme — no Universal Links / App Links. - Bare: configure
redirectUrlto match the registered URI; register the scheme inInfo.plist(iOS) and an<intent-filter>inAndroidManifest.xml(Android), exactly as the native sections above.react-native-app-authlaunchesASWebAuthenticationSession/ Custom Tabs for you.
C — Secure token storage
| Token | Where |
|---|---|
| Access token | expo-secure-store (Expo) or react-native-keychain (bare). |
| Refresh token | Same secure store. |
Use an AI prompt
Before pasting: edit the
PLATFORMSline at the top of the prompt to match your project —mobile only,web only, ormobile + web. Each platform needs its own EntryIdP client registration (different redirect URI schemes), so tell the AI whichclient_idbelongs to which.
<!-- PLATFORMS: mobile only | web only | mobile + web — edit before pasting -->
Add EntryIdP biometric OIDC login to this app.
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not build any sign-in form or
credential-entry UI.
Each platform has its own EntryIdP client registration and client_id (different redirect
URI schemes — do not share one client_id across platforms). Read all client_ids and the
issuer from env vars — never hardcode.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration).
Before writing code:
1. Confirm which platforms are in scope from the PLATFORMS line above.
2. For React Native mobile: determine Expo managed vs bare from app.json / package.json.
Expo managed → use expo-auth-session. Bare → use react-native-app-auth.
3. For web: use the browser's native fetch/redirect or a library such as oidc-client-ts
or AppAuth-JS — do not use a React Native auth library on web.
Implementation — applies to ALL platforms:
- Authorization Code + PKCE only (S256). Never implicit flow or response_type=token.
- PUBLIC client: no client_secret anywhere.
- Discover endpoints from {issuer}/.well-known/openid-configuration.
Mobile-specific:
- Redirect URI is a CUSTOM URL SCHEME (e.g. com.yourapp://auth/callback).
Expo: AuthSession.makeRedirectUri({ scheme: 'com.yourapp' }) + set scheme in app.json.
Bare: set redirectUrl and register the scheme in Info.plist (iOS) +
AndroidManifest.xml intent-filter (Android).
- EntryIdP does NOT support Universal Links / App Links — do not configure
apple-app-site-association or assetlinks.json.
- Store tokens with expo-secure-store (Expo) or react-native-keychain (bare). Never
AsyncStorage.
Web-specific:
- Redirect URI is an https:// URL (e.g. https://yourapp.com/auth/callback).
- Store the access token in memory only. Store the refresh token in an httpOnly cookie
or, if SPA-only with no backend, in sessionStorage — never localStorage.
- Handle the callback in the page mounted at the redirect URI; exchange the code
server-side if a backend is present, otherwise client-side via the auth library.
Login vs enrol (EntryIdP-specific — same on all platforms):
- "Sign in" → standard request (login of an existing face).
- "Create account" → add prompt=create — first-time face enrolment.
(expo: extraParams; app-auth: additionalParameters; oidc-client-ts/AppAuth-JS:
extraQueryParams / customRequestParameters)
- On error=access_denied with error_description=user_not_registered, retry with
prompt=create.
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret on any platform.
- Mobile: custom URL scheme redirects only; no Universal Links / App Links.
- Web: https:// redirects only; no custom schemes.
- No implicit flow; endpoints from discovery, not hardcoded.Flutter (flutter_appauth)
Use a public client with PKCE. Drive the flow with flutter_appauth.
Pick your path: Follow the guide below, or jump to the AI prompt.
Follow the guide
A — Login vs enrol
- Login: call
FlutterAppAuth.authorizeAndExchangeCode(...)as usual →/authorize. - Enrol: add
prompt=createvia the request'sadditionalParameters→/authorize?prompt=create.
Choose one of:
- Option 1 (recommended): "Sign in" and "Create account" buttons, the latter passing
additionalParameters: {'prompt': 'create'}. - Option 2: attempt login; on
error=access_denied+error_description=user_not_registered, retry withprompt=create.
B — Redirect callback setup
- Register
redirectUrlas a custom URL scheme (e.g.com.yourapp://callback). EntryIdP does not hostapple-app-site-associationorassetlinks.json, so Universal Links / App Links are not available. - iOS: add the scheme to
Info.plistunderCFBundleURLTypes. - Android: add the scheme to
AndroidManifest.xml.flutter_appauthcontributes its redirect activity, but you must declare your scheme in the manifest (commonly via theappAuthRedirectSchememanifest placeholder).
C — Secure token storage
| Token | Where |
|---|---|
| Access token | flutter_secure_storage. |
| Refresh token | Same store. |
Use an AI prompt
Add EntryIdP biometric OIDC login to this Flutter app using flutter_appauth.
EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not build any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from config — never hardcode).
Before writing code:
1. Add flutter_appauth and flutter_secure_storage to pubspec.yaml if missing.
2. Open Info.plist (iOS) and AndroidManifest.xml / build.gradle (Android) to register the
redirect scheme.
Implementation:
- Authorization Code + PKCE only (flutter_appauth adds PKCE with S256). Never implicit
flow or response_type=token.
- PUBLIC client: no client_secret anywhere.
- Use FlutterAppAuth.authorizeAndExchangeCode with issuer discovery (discoveryUrl =
{issuer}/.well-known/openid-configuration). Do not hardcode endpoints.
- Redirect URI is a CUSTOM URL SCHEME (e.g. com.yourapp://callback). iOS: CFBundleURLTypes
in Info.plist. Android: set the appAuthRedirectScheme manifest placeholder and declare
the scheme. EntryIdP does NOT support Universal Links / App Links.
- Store tokens with flutter_secure_storage. Never shared_preferences.
Login vs enrol (EntryIdP-specific):
- "Sign in" → standard authorize call (login of an existing face).
- "Create account" → additionalParameters: {'prompt': 'create'} (first-time face
enrolment).
- On error=access_denied with error_description=user_not_registered, retry with
prompt=create.
Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret.
- Custom URL scheme redirect only; no Universal Links / App Links.
- No implicit flow; endpoints from discovery, not hardcoded.Refresh tokens rotate
Wherever you store a refresh token (server-side apps and mobile apps that requested offline_access):
- Every refresh response contains a new
refresh_token. Overwrite the stored one with it immediately. - Refresh tokens are single-use. Replaying an already-used token is treated as a compromise: EntryIdP revokes the entire token family and the user must re-authenticate (you'll get
invalid_grantwitherror_description=refresh token replay detected).
SPAs don't have this concern — they don't request offline_access.
Pre-launch checklist
Before going to production, confirm:
Configuration
- [ ] Issuer points to the right environment; no endpoint URLs are hardcoded (all fetched from discovery).
- [ ] Redirect URI and post-logout redirect URI exactly match registered values (scheme, path, trailing slash).
- [ ]
client_secret(confidential clients only) is in a secrets store / env var — never committed, never in browser or mobile code.
Authorization & callback
- [ ]
response_type=code,code_challengepresent,code_challenge_method=S256. - [ ]
stateis random per attempt, stored server-side (or in app state), and compared byte-for-byte on callback. - [ ]
nonceis sent and checked against the ID token (recommended). - [ ] Callback handles the
error=access_denied+error_description=user_not_registeredcase (enrol path). - [ ] No implicit flow, no
response_type=token, noclient_credentials.
Tokens
- [ ] ID token validated: signature (RS256 via JWKS
kid),iss,aud,exp,nonce;alg=nonerejected. - [ ] Tokens stored per the platform table above (in-memory for SPAs, secure OS storage for mobile, server-side for backends).
- [ ] On every refresh, the new
refresh_tokenoverwrites the old one.
Assurance
- [ ]
face_liveness_verifiedis not treated as identity verification. For legal-identity decisions, requestidentity:verifiedand checkidentity_verifiedfrom/userinfo. - [ ]
email_verified/phone_number_verifiedare treated as alwaysfalse.
Need exact endpoint shapes, scope/claim lists, or error meanings? See the Reference.