<!-- section: Auth API · status: building · source: docs/auth-api/15-tokens.md -->

> A short-lived JWT your API verifies locally, so we are not a hop on every request you serve.


# Tokens and JWKS

## GET /v1/auth/token

Returns a short-lived JWT for the current session. **Publishable** key and a
session cookie.

```bash Request
curl "$BASE/v1/auth/token" \
  -H "authorization: Bearer $PK" \
  -b "session cookie"
```

```json The claims
{
  "sub": "NrzmwycxFVpxrD8RSZqieT4iqpW24W9M",
  "sid": "h0bsPL9wBPaRfvslAcU2q0w8VPeF4wJu",
  "org_id": null,
  "role": null,
  "email": "ada@example.com",
  "email_verified": false,
  "iss": "https://<your-deployment>",
  "exp": 1786977488
}
```

Signed with **EdDSA/Ed25519**, fifteen minute expiry.

## GET /v1/auth/jwks

The public keys, so your API verifies tokens locally and **we are not a network
hop on every request you serve**.

```bash Request
curl "$BASE/v1/auth/jwks" -H "authorization: Bearer $PK"
```

The signing keys live in **your project's own database**, so your tokens are
signed with a keypair nobody else holds — a token minted for another project
cannot verify against yours.

## org_id and role

**Both are in the payload from day one**, and were null before organizations
shipped. This is deliberate: writing your API against these claims now means it
keeps working when teams arrive, and retrofitting them after you have built a
flat user model is the part that hurts.

`org_id` is the **active** organization on the session — one person can belong to
several and switch without their identity changing.

`role` is read from the membership at the moment the token is minted, not copied
onto the session at sign-in, so demoting an admin takes effect on their next
token rather than their next sign-in.

## Expiry is a design decision, not a limitation

Fifteen minutes is short enough that a revoked session stops working quickly and
long enough that a client is not fetching a token per request. Ask for a new one
when it expires; `get-session` tells you whether the underlying session is still
alive.

## Errors

| Status | Code | Means |
|---|---|---|
| `401` | — | No session on the request |
| `403` | — | Publishable key used from an unregistered origin |
