The Calm API has two security schemes. The session endpoint takes a
Privy bearer token; every wallet endpoint takes the
calm_session cookie.
Schemes
Authorization: Bearer <privy_identity_token> — Privy’s identity-token
JWT. Carries the Privy app id in aud and the user’s verified email +
wallet(s) in linked_accounts. Only POST /v1/session/{address} accepts
this.
calm_session — short-lived (1h) HS256 JWT issued by POST /v1/session/{address}. Sent automatically by the browser; the SDK uses
credentials: "include". All /v1/wallets/* endpoints require this.
Per-endpoint matrix
| Endpoint | Auth |
|---|
POST /v1/session/{address} | Bearer (Privy identity token) |
GET /v1/wallets/{address} | Cookie |
POST /v1/wallets/{address} | Cookie |
POST /v1/wallets/{address}/terms-of-service | Cookie |
POST /v1/wallets/{address}/identity-verification | Cookie |
POST /v1/wallets/{address}/virtual-account | Cookie |
GET /v1/wallets/{address}/virtual-account | Cookie |
Refresh
There’s no separate refresh endpoint. To extend an expiring session, call
POST /v1/session/{address} again with a fresh Privy identity token —
the cookie is overwritten.
Address binding
Every URL {address} is cross-checked against the auth artifact:
- With bearer: the address must appear in the JWT’s
linked_accounts
as a wallet, else wallet_not_linked (403).
- With cookie: the address must equal the cookie’s bound wallet, else
wallet_token_mismatch (403). Defense-in-depth — a stolen cookie can’t
be replayed against a different wallet’s URL.
Errors you’ll see at the auth layer
| Code | Status | When |
|---|
token_required | 401 | Missing Authorization: Bearer … on POST /v1/session/{address}. |
invalid_token | 401 | Privy JWT signature/claims invalid, malformed, or missing aud. |
token_expired | 401 | Privy JWT past its exp. |
wallet_not_linked | 403 | URL {address} isn’t in the Privy user’s linked_accounts. |
session_required | 401 | Cookie missing on a wallet route. |
session_invalid | 401 | Cookie failed verification or expired. |
wallet_token_mismatch | 403 | URL {address} doesn’t match the cookie’s bound wallet. |
See Errors for the full table including non-auth codes.