Zaroz
Using the API

Sign in, sessions and account security

Most scripts should use an API token, not a login. You still need a session (what the console uses) for the few things a token may not do: creating your first personal token, revoking one, or changing account security. This page shows the session side. The samples use the helper from Using the Zaroz API; sign-in itself is unauthenticated.

Sign in with email and password

A correct password does not open a session by itself: it earns a one-time code, emailed to the address. Submit the code to open the session.

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/auth/login" -H "Content-Type: application/json" \ -d '{"email": "[email protected]", "password": "…"}' # {"challenge_id": "…", "status": "…"} the code is now in your inbox curl -sS -X POST "$ZAROZ_API/auth/login/verify" -H "Content-Type: application/json" \ -d '{"challenge_id": "…", "code": "123456"}' # {"token": "<access token>", "refresh_token": "…", "session_id": "…", "user": {…}}

TypeScript

TypeScriptCode
const API = process.env.ZAROZ_API ?? "https://kernel.zaroz.cloud/api/v2"; const post = async (path: string, body: unknown) => (await fetch(`${API}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })).json(); const { challenge_id } = await post("/auth/login", { email: "[email protected]", password: process.env.PASSWORD }); const code = await promptForCodeFromEmail(); // you type it const session = await post("/auth/login/verify", { challenge_id, code }); console.log(session.token, session.refresh_token, session.session_id);

Python

Code
import os, requests API = os.environ.get("ZAROZ_API", "https://kernel.zaroz.cloud/api/v2") r = requests.post(f"{API}/auth/login", json={"email": "[email protected]", "password": os.environ["PASSWORD"]}, timeout=20) challenge_id = r.json()["challenge_id"] code = input("Code from your email: ") session = requests.post(f"{API}/auth/login/verify", json={"challenge_id": challenge_id, "code": code}, timeout=20).json() print(session["token"], session["refresh_token"], session["session_id"])

The code expires quickly and the challenge is burned after a few wrong attempts (start again with /auth/login). Use token as the bearer token. Accounts that sign in with a provider (the list is GET /auth/oauth/providers) use the browser flow instead; password sign-up is POST /auth/password/signup then POST /auth/password/verify, and POST /auth/password/forgot with POST /auth/password/reset recovers a password. These answer identically whether or not the address has an account, so they cannot be used to find out who is registered.

Keep the session alive

The access token is short-lived. Exchange the refresh token for a new pair and use the new refresh token next time: it rotates.

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/auth/refresh" -H "Content-Type: application/json" -d '{"refresh_token": "…"}' # {"token": "…", "refresh_token": "<the NEW one>", "session_id": "…"} curl -sS "$ZAROZ_API/auth/session" -H "$AUTH" # 401 once it is no longer valid curl -sS -X POST "$ZAROZ_API/auth/logout" -H "$AUTH" -H "Content-Type: application/json" -d '{"refresh_token": "…"}' # 204

TypeScript

TypeScriptCode
const next = await post("/auth/refresh", { refresh_token: session.refresh_token }); session.token = next.token; session.refresh_token = next.refresh_token; // keep the rotated one; the old one is spent

Python

Code
nxt = requests.post(f"{API}/auth/refresh", json={"refresh_token": session["refresh_token"]}, timeout=20).json() session["token"], session["refresh_token"] = nxt["token"], nxt["refresh_token"] # the old one is spent

Always store the rotated refresh token and never reuse a spent one.

Your sessions

TerminalCode
curl -sS "$ZAROZ_API/me/sessions" -H "$AUTH" curl -sS -X DELETE "$ZAROZ_API/me/sessions/$SESSION" -H "$AUTH" # 204; ends every token pair of that session
TypeScriptCode
const sessions = await call("GET", "/me/sessions"); await call("DELETE", `/me/sessions/${sessions.body.data[0].id}`);
Code
_, sessions = call("GET", "/me/sessions") call("DELETE", f"/me/sessions/{sessions['data'][0]['id']}")

id is the session family, stable across every rotation (it is the session_id returned at sign-in). Revoking is immediate and never reveals whether an id exists: another person's id is a silent no-op. If a laptop is lost, revoke its session, then revoke the API tokens it could have created.

Password and profile

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/auth/password/change" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"current_password": "…", "new_password": "…", "current_session_id": "<this session>"}' curl -sS "$ZAROZ_API/me" -H "$AUTH" curl -sS -X PATCH "$ZAROZ_API/me" -H "$AUTH" -H "Content-Type: application/json" -d '{"name": "Sam", "language": "es"}'
TypeScriptCode
await call("POST", "/auth/password/change", { current_password: OLD, new_password: NEW, current_session_id: session.session_id, // omit to be signed out everywhere }); await call("PATCH", "/me", { name: "Sam", language: "es" });
Code
call("POST", "/auth/password/change", {"current_password": OLD, "new_password": NEW, "current_session_id": session["session_id"]}) call("PATCH", "/me", {"name": "Sam", "language": "es"})

Changing the password signs every other session out; pass current_session_id to keep this one, or omit it to be signed out everywhere. Every field of PATCH /me is optional (name, language, discord_notifications_enabled, billing_country_code); the billing country is refused once onboarding is complete and a country is already stored, because it decides tax treatment.

Elevated (sudo) sessions

Some actions on the account (staff actions, for example, and the sensitive ones you chose to protect) need an elevated session: a fresh window in which you proved a second factor. Ask whether you need one, then elevate with your PIN.

TerminalCode
curl -sS "$ZAROZ_API/auth/sudo/status" -H "$AUTH" # {"is_elevated": false, "elevated_until": null, "available_methods": [...], "enrolled_methods": [...]} curl -sS -X POST "$ZAROZ_API/auth/sudo/elevate" -H "$AUTH" -H "Content-Type: application/json" -d '{"pin": "…"}' # {"token": "<new access token>", "elevated_until": "…"} curl -sS -X POST "$ZAROZ_API/auth/sudo/exit" -H "$AUTH" # drop it early; answers a new, un-elevated token
TypeScriptCode
const status = await call("GET", "/auth/sudo/status"); if (!status.body.is_elevated) { const el = await call("POST", "/auth/sudo/elevate", { pin: process.env.SUDO_PIN }); // use el.body.token from now on }
Code
_, status = call("GET", "/auth/sudo/status") if not status["is_elevated"]: _, el = call("POST", "/auth/sudo/elevate", {"pin": os.environ["SUDO_PIN"]}) # use el["token"] from now on

Elevation returns a new access token carrying the window; the token you presented stays valid but un-elevated. A wrong PIN and an account with no PIN are answered identically (five attempts per fifteen minutes). Methods are set up with the /auth/sudo/setup, /auth/sudo/totp/* and /auth/sudo/webauthn/* routes; adding a second method requires an already-elevated session. A scripted job should never hold an elevated session: use a narrowly scoped API token instead.

Last modified on