Signing in from a browser
The /api/auth/* endpoints (Auth scope) authenticate a person at a
keyboard, in a browser, on one of our own front ends. They are not a replacement
for the credentials an integration uses: a server-to-server client keeps using a
personal access token, and the reference for these
endpoints is generated by the auth library, so it describes their request and
response bodies but none of the four rules on this page.
If you are building against the API from your own backend, you want Getting started instead.
Four things the reference does not tell you
Section titled “Four things the reference does not tell you”1. Send Accept-Version: 2.0
Section titled “1. Send Accept-Version: 2.0”/api/auth/* sits behind the same version gate as every other endpoint. The
auth library’s own client does not send the header, so a default
createAuthClient() gets 406 on every call before the auth library ever sees
the request.
const authClient = createAuthClient({ baseURL: "https://api.re-zip.com", fetchOptions: { headers: { "Accept-Version": "2.0" }, },})2. Your origin has to be ours
Section titled “2. Your origin has to be ours”Requests are rejected unless the browser’s Origin is a re-zip.com
subdomain — 403 with {"code": "INVALID_ORIGIN"}. Local development from
http://localhost:3500 and http://localhost:3600 is allowed outside
production.
This bites in a way that is easy to misread: the same request succeeds from
curl, because a request with no Origin header at all skips the check. A flow
that works in your terminal and fails in the browser is almost always this.
3. Read the token from set-auth-token, and send it as a bearer
Section titled “3. Read the token from set-auth-token, and send it as a bearer”Cookies are not used. The API only forwards the Authorization header to the
auth layer, so an ambient session cookie authenticates nothing — send
credentials: "omit" and read the session token out of the set-auth-token
response header.
const res = await fetch("https://api.re-zip.com/api/auth/sign-in/email", { method: "POST", credentials: "omit", headers: { "Accept-Version": "2.0", "Content-Type": "application/json" }, body: JSON.stringify({ email, password }),})const token = res.headers.get("set-auth-token")That token is then an ordinary bearer credential for the whole API, not just for
/api/auth/*:
Authorization: Bearer <token>Two-factor sign-in is the one place cookies are used. For a user enrolled in
two-factor, sign-in answers {"twoFactorRedirect": true} and keeps the pending
challenge in an HttpOnly cookie, so send credentials: "include" on the
sign-in request and on the POST /api/auth/two-factor/verify-totp that
completes it. Check twoFactorRedirect before reading set-auth-token: on a
challenge that header names a session that has already been deleted. The cookie
only round-trips same-site: from a re-zip.com subdomain, or from localhost
against an API also running on localhost, and it is the only
cookie the API ever sets — the session itself always arrives as the header.
After a two-factor verification, read the header and not res.body.token.
Verifying a second factor replaces the session — the old one is deleted — and
the response body still names the deleted one, so a client that trusts the body
is immediately signed out.
4. Sessions last 30 minutes
Section titled “4. Sessions last 30 minutes”A session expires 30 minutes after it was issued and the window slides with use,
matching the access tokens these replace. Treat a 401 as “sign in again”
rather than as an error worth reporting.
What these endpoints will not do
Section titled “What these endpoints will not do”The auth library mounts a wider surface than we accept, and the reference lists all of it. Only authentication and password changes are enabled:
| Endpoint | What happens | Use instead |
|---|---|---|
POST /api/auth/sign-up/email | 400 — disabled | Agreements and invitations |
/api/auth/update-user | disabled | PATCH /me |
/api/auth/change-email | disabled | PUT /me/email |
/api/auth/delete-user | disabled | DELETE /me |
Sign-up, password reset and email verification stay on the /me endpoints.
These endpoints authenticate an existing user. POST /api/auth/change-password
changes their password, the same as PUT /me/password; nothing else here
creates or modifies a user.
One inconsistency worth knowing
Section titled “One inconsistency worth knowing”Timestamps that come from the user record are serialised as
2026-05-28 07:24:54.849432 rather than as ISO 8601, while the session’s own
timestamps are ISO:
{ "session": { "createdAt": "2026-09-22T11:41:59.838Z" }, "user": { "createdAt": "2026-05-28 07:24:54.849432" }}new Date(...) does not parse the first form consistently across browsers. If
you display a user timestamp from /api/auth/*, parse it explicitly — or read
it from GET /me, which is ISO everywhere.