ZevAuth
Sign up

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

POST /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

POST /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

POST /v1/auth/magic-link
POST /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:

POST /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:

SettingValuesMeaning
policyat_signup later neverWhen, if ever, we send the message
strategiescode linkWhat the message contains
unverifiedAccessallow blockWhether a session is issued before it is confirmed
POST /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/signup creates the account and returns "session": null.
  • POST /v1/auth/signin returns 403 with code email_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

GET /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_url is optional and that one is used.
  • With several, pass redirect_url naming which. It must match a registered entry exactly; prefix matches are refused because that is how open redirects get in.
  • Development environments accept http://localhost and other loopback addresses, so you can test the whole flow locally. Production callbacks must be https.

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=...
errorMeaningWhat to do
zevid_expiredThey took longer than 15 minutes, or the link was reusedOffer the button again
zevid_deniedThey pressed Deny on the consent screenNothing; respect it
zevid_not_a_test_memberOnly once ZevID has made test environments team-only: they are not in your workspaceAdd them under Team, or use your live app. See Test environments
zevid_rejectedZevID answered, but with something we could not acceptOffer the button again; if it repeats, contact us
zevid_errorZevID could not complete the sign-inOffer 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

POST /v1/auth/refresh
{ "refreshToken": "rt_…" }

Switch organization

POST /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

POST /v1/auth/signout
{ "refreshToken": "rt_…" }

Environment

GET /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