Concepts
Mobile apps
Sign-in from iOS and Android apps, with universal links, app links and PKCE.
Native apps sign in the way the OAuth standard for native apps (RFC 8252) recommends. The app opens ZevAuth in the phone’s own browser sign-in sheet, and the person comes back to the app with a one-time code that only that app can redeem.
That sheet is ASWebAuthenticationSession on iOS and Chrome Custom
Tabs on Android. Never use an embedded WebView: the person can’t see the
address bar, and the app could read what they type.
How a sign-in works
- Your app creates a random code verifier and a state, and keeps both in memory.
- It opens
/v1/auth/zevid/startin the system sign-in sheet with:redirect_uri: your app’s link;state;code_challenge: the SHA-256 of the verifier, base64url-encoded;code_challenge_method=S256.
- The person signs in. If they’re already signed in to ZevID in Safari or Chrome, they’re recognised.
- ZevAuth sends them back to your link with
codeand yourstate. Check the state matches what you sent. - Your app exchanges the code at
POST /v1/auth/handoff/exchangewithcodeVerifierand the sameredirectUri, and receives a session.
POST /v1/auth/handoff/exchange
Authorization: Bearer pk_live_…
{ "code": "hoc_…", "codeVerifier": "…", "redirectUri": "https://acme.com/app/callback" }
The code works once and expires after 60 seconds. If someone else intercepts the redirect, the code is still useless to them without your app’s verifier.
Errors come back to the same link with error, error_description and
your state. Tokens never appear in a URL.
The JavaScript SDK does all of this in a browser too:
client.signIn.withZevId({ pkce: true }) binds the sign-in to the tab
that started it.
Where a sign-in may return
| Link | Development | Production |
|---|---|---|
| Universal link (iOS) or app link (Android) on your domain | Allowed | Allowed once your domain’s association file lists your app |
Custom URL scheme, such as com.acme.app:/callback | Allowed, with PKCE | Not allowed, unless ZevAuth support enables it for your app |
http://localhost | Allowed | Not allowed |
Any app on a phone can claim a custom scheme, so a scheme proves nothing about which app opens it. A universal link or app link does, because the phone checks your domain’s association file before opening your app.
Registering your app
-
Verify your project domain under Project settings → Domains & callbacks.
-
Register the link itself as a callback, for example
https://acme.com/app/callback. -
Open Project settings → Mobile apps and choose Add app:
- iOS: the bundle ID (in Xcode: your target → General → Bundle Identifier) and your Apple team ID (your Apple developer account → Membership details).
- Android: the package name (the
applicationIdinbuild.gradle) and the SHA-256 fingerprint of each signing certificate. If you publish with Play App Signing, add the app signing key’s fingerprint from Play Console → your app → Test and release → App integrity → App signing, because Google signs what people install. For your own key or a development build, runkeytool -list -v -keystore your.keystore -alias your-alias. Paste fingerprints in any format; the console fixes colons and case.
-
Publish the association file on the link domain. The Association files panel on the same page generates it from your apps, ready to copy:
- iOS:
https://acme.com/.well-known/apple-app-site-association, served asapplication/jsonwith no file extension. - Android:
https://acme.com/.well-known/assetlinks.json.
Serve them directly, without a redirect: phones don’t follow one.
- iOS:
-
Press Check again. The app shows Link verified once the file lists it.
We read the file when you add the app, when you press Check again, and once a day after that, so a file that changes or disappears is noticed. If the app isn’t listed, the console says exactly what was missing.
Limits
| Per environment | |
|---|---|
| iOS apps | 10 |
| Android apps | 10 |
| Signing certificates per Android app | 5 |
| Apple teams | 1: all iOS apps in an environment belong to the same team |
What people see when they sign in with ZevID
Once an app’s link is verified, “Sign in with ZevID” knows about it. On a phone that matches a verified app, the ZevID consent screen names your app with the device, such as “Acme on iPhone” or “Acme on Android”, so people know exactly where they are signing in. Until then it shows your app’s name alone. Sign-in works either way.
The Shown in Sign in with ZevID status on the Mobile apps page says whether your verified apps are up to date there. If an identifier is ever not accepted, the page says which one and why. We retry automatically; contact ZevAuth support if it persists.
How long a ZevID session lasts
A session that started with “Sign in with ZevID” needs a fresh ZevID sign-in after at most 90 days, or after 30 days without being used, on every platform. You can shorten both for your environment under Project settings → Mobile apps → Sign in with ZevID sessions. You can’t make them longer. These are today’s limits; if they are ever tightened, sessions follow the new limits automatically.
Each new ZevID sign-in also asks ZevID for a sign-in no older than that window, so a session never starts already past it.
When a window passes, refreshing the session fails with
reauthentication_required. The SDK signs the person out and shows
“For your security, please sign in with ZevID again”.
A ZevID session also ends straight away, on the web and in your apps, when the person:
- removes your app from their ZevID account;
- signs out of the ZevID session your app’s sign-in came from (only the sessions made from it end; their other devices stay signed in);
- signs out of ZevID everywhere, changes their ZevID password, or has their ZevID account closed (every ZevID session in every app ends).
Sessions from “Sign in with ZevID” are also checked against ZevID at least once a day, so a sign-out is honoured even if a notification was missed.
Errors
| Status | Code | Means |
|---|---|---|
| 400 | pkce_method_unsupported | code_challenge_method must be S256. |
| 400 | invalid_code_challenge | The challenge must be the base64url SHA-256 of the verifier. |
| 400 | pkce_required | A custom URL scheme needs PKCE. |
| 400 | invalid_state | state must be 1 to 512 characters. |
| 403 | native_redirect_not_verified | In production, the link’s domain doesn’t list any of your registered apps yet. |
| 403 | custom_scheme_not_allowed | Custom URL schemes are for development unless ZevAuth support has enabled them for your app. |
| 400 | pkce_verification_failed | The verifier doesn’t match the challenge the sign-in started with. |
| 400 | redirect_uri_required | A PKCE sign-in must send the same redirectUri to the exchange. |
| 400 | redirect_uri_mismatch | The code was issued for a different redirectUri. |
| 401 | reauthentication_required | A ZevID session needs a fresh ZevID sign-in. |
| 400 | native_app_limit | The environment already has 10 apps for that platform, or the app has more than 5 certificates. |
| 400 | native_app_team_mismatch | Your other iOS apps belong to a different Apple team. |
Updated at, Sunday, November 1, 2026