API reference
Authentication
Signing people in and keeping them signed in.
These take a publishable key. They are designed to be called from a browser.
Sign up
/v1/auth/signup { "email": "ada@example.com", "password": "correct horse battery staple" }
Returns the user and, unless your environment requires email confirmation first, a session.
Sign in
/v1/auth/signin { "email": "ada@example.com", "password": "…" }
email accepts an address or a username. Failures are uniform: a wrong
password and an unknown account return the same error, so the endpoint cannot
be used to enumerate your users.
{
"user": { "id": "user_…", "email": "ada@example.com", "…": "…" },
"session": {
"accessToken": "eyJ…",
"refreshToken": "rt_…",
"expiresIn": 900,
"tokenType": "Bearer",
"organization": null
}
}
Passwordless
/v1/auth/magic-link /v1/auth/email-code { "email": "ada@example.com" }
Both answer the same way for any address:
{ "sent": true, "message": "If that address has an account, we have sent it a message." }
Codes are redeemed with:
/v1/auth/email-code/verify { "email": "ada@example.com", "code": "418293" }
Magic links are redeemed by the hosted page, which returns a one-time code your app exchanges.
Email verification
Two independent settings, both under Authentication → Identifiers → Email
in the console, and both published on
GET /v1/environment so a client can render the right screen:
| Setting | Values | Meaning |
|---|---|---|
policy | at_signup later never | When, if ever, we send the message |
strategies | code link | What the message contains |
unverifiedAccess | allow block | Whether a session is issued before it is confirmed |
/v1/auth/verify-email Takes either the token from a link, or an address and the short code:
{ "email": "ada@example.com", "code": "418293" }
When unverifiedAccess is block:
POST /v1/auth/signupcreates the account and returns"session": null.POST /v1/auth/signinreturns 403 with codeemail_not_verified, and sends a fresh verification message on the way out, so somebody returning after the first one expired is not stranded.
When it is allow, a session is issued and email_verified in the access
token is false until they confirm. Your API decides what that restricts.
See Use cases for both set up end to end.
Sign in with ZevID
/v1/auth/zevid/start A brokered login through ZevID. Send the person’s browser here and we redirect them to the consent screen; they come back to a URL you registered, carrying a one-time code your app exchanges for a session.
https://api.zevauth.net/v1/auth/zevid/start
?publishable_key=pk_live_...
&redirect_url=https://yourapp.com/after-signin
What the verification email contains
Your choice, under Authentication → Email in the console: a confirmation
link, a six-digit code, or both. The email, the API and <SignUp /> all follow
it — a code-only environment sends an email with no button and shows a code
input; a link-only environment sends one tap and shows “open the link”. Codes
suit mobile apps and embedded webviews where links open the wrong browser;
links suit desktop web. The setting rides on
GET /v1/environment as
emailVerification.strategies, so a custom UI can branch the same way.
Before it will work
Register the URL the person should land on afterwards, in the console under
Project settings → Domains & callbacks. Nothing else is needed, and nothing
works without it: with no callback registered the endpoint answers
invalid_request and says so.
- With exactly one callback registered,
redirect_urlis optional and that one is used. - With several, pass
redirect_urlnaming which. It must match a registered entry exactly; prefix matches are refused because that is how open redirects get in. - Development environments accept
http://localhostand other loopback addresses, so you can test the whole flow locally. Production callbacks must behttps.
Everything else about the flow, including the PKCE verifier, is decided here and stored, so the callback reads nothing from the request it cannot trust.
How it comes back
Success returns the person to your registered callback with a one-time code:
https://yourapp.com/after-signin?code=hoc_...
@zevauth/react and @zevauth/js exchange it on load and the session starts
on your origin, with your publishable key. Without the SDK, exchange it yourself:
POST /v1/auth/handoff/exchange { "code": "hoc_..." }
Failure also returns to your callback, never to a ZevAuth page, so the person is back in your product with something you can act on:
https://yourapp.com/after-signin?error=zevid_expired&error_description=...&state=...
error | Meaning | What to do |
|---|---|---|
zevid_expired | They took longer than 15 minutes, or the link was reused | Offer the button again |
zevid_denied | They pressed Deny on the consent screen | Nothing; respect it |
zevid_not_a_test_member | Only once ZevID has made test environments team-only: they are not in your workspace | Add them under Team, or use your live app. See Test environments |
zevid_rejected | ZevID answered, but with something we could not accept | Offer the button again; if it repeats, contact us |
zevid_error | ZevID could not complete the sign-in | Offer the button again |
error_description is written for the person and safe to show as-is.
When they revoke it later
A person can remove your app from their ZevID account at any time. When they
do, ZevID tells us, and three things happen in your environment: every session
that user holds is ended, the linked identity is marked revoked (the console
shows it on the user), and you receive a user.identity.revoked
webhook with data.provider: "zevid". Nothing else about the
account changes. If they sign in with ZevID again, that is a fresh consent and
the mark is lifted.
The one case that cannot come back to you is a state we never issued, since
there is no application to return to. That renders a plain page telling the
person to go back to the app and try again.
If you use @zevauth/react, <SignIn /> renders the button and
client.signIn.withZevId() performs this redirect for you.
Refresh
/v1/auth/refresh { "refreshToken": "rt_…" }
Switch organization
/v1/auth/switch-organization { "refreshToken": "rt_…", "organizationId": "org_…" }
Pass null to return to personal scope. Returns a fresh user and session,
because the organization is a claim inside the tokens.
Sign out
/v1/auth/signout { "refreshToken": "rt_…" }
Environment
/v1/environment Which environment your key resolves to, which sign-in methods are enabled, your limits, and your branding. The SDK calls this once at start-up.
{
"id": "env_test_...",
"kind": "test",
"publishableKey": "pk_test_...",
"signInMethods": ["password", "google"],
"limits": {
"maxEndUsers": 500,
"accessTokenTtlSeconds": 3600,
"refreshTokenTtlSeconds": 2592000,
"sessionMaxLifetimeSeconds": 604800,
"requestsPerMinute": 1000,
"emailDelivery": "captured"
}
}
publishableKey is the key your frontend ships with. It is returned here so a
provisioning script, a CI job, or a per-branch preview environment can seed a
frontend .env without a person opening the dashboard to copy it.
emailDelivery is captured in test environments, meaning we store messages
instead of sending them. Read them with
GET /v1/sandbox/emails so flows that depend on a link,
like verification or password reset, can be tested end to end.
Updated at, Wednesday, October 21, 2026