# Sign in, sessions and account security

Most scripts should use an [API token](/api-tokens/getting-started), 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](/guides/api/overview); 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**

```bash
curl -sS -X POST "$ZAROZ_API/auth/login" -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "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**

```ts
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: "you@example.com", 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**

```python
import os, requests

API = os.environ.get("ZAROZ_API", "https://kernel.zaroz.cloud/api/v2")
r = requests.post(f"{API}/auth/login", json={"email": "you@example.com", "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**

```bash
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**

```ts
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**

```python
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

```bash
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
```

```ts
const sessions = await call("GET", "/me/sessions");
await call("DELETE", `/me/sessions/${sessions.body.data[0].id}`);
```

```python
_, 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](/api-tokens/getting-started#list-rotate-and-revoke) it could have created.

## Password and profile

**curl**

```bash
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"}'
```

```ts
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" });
```

```python
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.

```bash
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
```

```ts
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
}
```

```python
_, 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](/api-tokens/getting-started) instead.
