---
title: Server SDKs
description: 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:

1. **Check who is calling.** Verify the session token on each request.
2. **Act on users and organizations.** Create, update, block, delete, with
   your secret key.
3. **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](/concepts/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](/sdk/php/) | `zevauth/zevauth-php` | [Laravel](/sdk/laravel/), any PSR-7 framework (Symfony, Slim, Mezzio), WordPress |
| [Python](/sdk/python/) | `zevauth` | [FastAPI](/sdk/fastapi/), [Django](/sdk/django/), [Flask](/sdk/flask/), any framework |
| [Ruby](/sdk/ruby/) | `zevauth` | [Rails](/sdk/rails/), Rack (Sinatra, Roda, Hanami) |
| [Go](/sdk/go/) | `github.com/zevop/zevauth-go` | `net/http` and any router built on it (chi, gorilla/mux) |
| [Java and Kotlin](/sdk/java/) | `com.zevauth:zevauth-java` | [Spring Boot](/sdk/spring/) (`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 `none` and 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.

> **Note:**
> A token is a signed statement that stays valid until it expires, minutes
> after it is issued. A session revoked a moment ago is still accepted
> offline until then. For actions that must react to a sign-out instantly,
> check with the API.

## 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](/concepts/session-bridge/) hands the session
to your server in your own cookie, without your server ever holding a
refresh token.
