Your backend
Server SDKs
What every ZevAuth server SDK does, in every language, and the promise that they all behave the same way.
Your server has three jobs with ZevAuth, whatever language it is written in:
- Check who is calling. Verify the session token on each request.
- Act on users and organizations. Create, update, block, delete, with your secret key.
- React to events. Receive webhooks, and check they really came from ZevAuth.
A fourth applies to apps that render pages on the server: see the session on page requests, through the session bridge.
Every ZevAuth server SDK does all four. Pick your language with the Your backend picker in the sidebar.
| Language | Package | Framework integrations |
|---|---|---|
| Node.js | @zevauth/backend | Next.js, React Router, TanStack Start, Astro, Hono, Express |
| PHP | zevauth/zevauth-php | Laravel, any PSR-7 framework (Symfony, Slim, Mezzio), WordPress |
| Python | zevauth | FastAPI, Django, Flask, any framework |
| Ruby | zevauth | Rails, Rack (Sinatra, Roda, Hanami) |
| Go | github.com/zevop/zevauth-go | net/http and any router built on it (chi, gorilla/mux) |
| Java and Kotlin | com.zevauth:zevauth-java | Spring Boot (com.zevauth:zevauth-spring-boot-starter), Jakarta Servlet |
Until your language’s SDK ships, everything below can be done with any standard JWT library and HTTP client. The tokens are ordinary RS256 JWTs and the API is plain JSON over HTTPS. The sections below give the exact rules.
One contract, every language
The SDKs are not ports of each other. Each one implements the same written contract, and each one runs the same conformance fixtures in its test suite: the same tokens (valid, expired, tampered, signed with the wrong algorithm, signed by an unknown key…), the same webhook signatures and the same API errors, with the same expected answer for every one.
So a token your Node service accepts is accepted by your Python worker, a webhook your PHP app rejects is rejected by your Go service, and an error code means the same thing everywhere.
Checking who is calling
The session token arrives in Authorization: Bearer …. Every SDK
verifies it the same way:
- RS256 only. A token claiming any other algorithm is refused before a
key is looked at, including
noneand the HS256 trick that turns a public key into a password. - Your environment’s keys, fetched from
/v1/{environmentId}/.well-known/jwks.json, cached for up to 10 minutes and refetched when a token names a key it has not seen (that is what a key rotation looks like), at most once every 30 seconds. - Your environment’s issuer (read from the discovery document) and your environment as the audience, so a test token never works on a live route.
- 5 seconds of clock difference allowed on expiry.
No request is made to ZevAuth per token. The key set is cached, so our availability is never in the path of your requests.
Each SDK offers the same two calls, named the way its language names things:
getAuth(request): who is signed in, or nobody. Never fails because a token expired; a page deciding what to render should not crash because a session lapsed.requireAuth(request): the same, but refuses with the response your framework expects.
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | Nobody signed in |
| 403 | session_task_pending | The person must finish setting up two-step verification first |
| 403 | mfa_required | You asked for two-step verification and this session did not pass it |
Both give you the user’s id, the session id, the active organization and
role, has({ role, permission }), and whether the session passed a second
factor.
Acting on users and organizations
With your secret key (sk_…, never in a browser), every SDK offers users
(list, get, create, update, block, sign out everywhere, reset two-step
verification, delete) and organizations (list, get, create, update,
delete, add and remove members). Lists come back a page at a time, and
every SDK can follow the pages for you.
Every SDK retries only what certainly did not happen (a dropped
connection, a 429, a 502, 503 or 504), honours the server’s
Retry-After, and never retries a 500, because the request may have
run. Errors carry the API’s code, the HTTP status, and a request id to
quote to ZevAuth support.
Receiving webhooks
Each delivery is signed in the zevauth-signature header,
t=<timestamp>,v1=<signature>: an HMAC-SHA256, keyed with your endpoint’s
whsec_… secret, over <timestamp>.<raw body>. Every SDK checks it in
constant time and refuses deliveries more than 5 minutes old, which stops
replays.
Verify the raw body, before parsing it. A body parsed and re-encoded is a different string and fails. Each SDK’s page shows how to get the raw body in its frameworks.
Server-rendered apps
If your server renders pages (Laravel, Rails, Django, Spring MVC, Express
templates), the browser requests them itself and sends no Authorization
header. The session bridge hands the session
to your server in your own cookie, without your server ever holding a
refresh token.
Updated at, Sunday, November 1, 2026