Authentication & Tokens¶
Every API request (except a small number of public endpoints) must include an Authorization header. Two token types are accepted:
See Getting Started: Authentication for how to obtain tokens and which type to use. This page covers the technical contract.
JWT bearer tokens¶
JWT tokens are issued by the Rodauth login endpoint:
The JWT is returned in the response Authorization header, not the body.
Token claims:
| Claim | Value |
|---|---|
account_id | UUID of the authenticated account |
authenticated_by | Array of methods used (e.g. ["password"]) |
JWTs are short-lived. When a token expires, re-authenticate via POST /login.
Cookie authentication (browser / dashboard clients)¶
POST /login also sets two cookies for browser-based clients:
| Cookie | Flags | Purpose |
|---|---|---|
_hq_session | HttpOnly; Secure; SameSite=Lax | Session JWT (inaccessible to JavaScript) |
_hq_csrf | Secure; SameSite=Lax | Double-submit CSRF token (readable by JavaScript) |
The API accepts either Authorization: Bearer <token> or the _hq_session cookie — both authenticate the same request. The dashboard uses the cookie path automatically; the CLI and Terraform provider use Bearer.
CSRF protection for cookie-authenticated requests¶
Cookie-authenticated mutating requests must include the CSRF token as a header:
This applies to POST, PUT, PATCH, and DELETE requests made with the session cookie. Requests authenticated via Authorization: Bearer (CLI, Terraform provider, static tokens) are exempt — Bearer auth is not subject to CSRF.
Mutating cookie requests that omit X-CSRF-Token are rejected with 403 Forbidden.
Logout¶
Clears both the _hq_session and _hq_csrf cookies and returns 200 OK.
Static API tokens¶
Static tokens are long-lived, org-scoped, and carry explicit permission scopes. You create and revoke them in the dashboard (Account → Settings → API Tokens) or via the API. Static tokens:
- Do not expire automatically (optional expiry timestamp available on create).
- Carry the
Bearerprefix exactly like JWTs — the API determines the token type from the token format. - Are scoped to one organization; cross-org requests with a static token are rejected.
- Require at least one scope; empty-scope tokens are rejected (deny-by-default).
- Are exempt from CSRF checks (Bearer path).
- Can only be managed via an interactive session (JWT/cookie). A static token cannot be used to create, list, or revoke other tokens — the
/tokensendpoints reject static tokens with403 Forbidden.
Scopes follow <resource>:<level>, where level is read, write, manage, or * (wildcard). Examples:
Token management endpoints¶
| Method | Path | Description |
|---|---|---|
GET | /tokens | List all tokens for the current account (metadata only, no raw value) |
POST | /tokens | Create a token — raw value returned once in the 201 response |
DELETE | /tokens/{id} | Revoke a token permanently |
All three require an interactive session (JWT bearer or cookie). The 201 create response includes the raw token value in data.token; subsequent GET /tokens responses return only token_last4 for identification — the raw value is never stored in plaintext.
See the Interactive Reference for full request/response schemas.
Auth method summary¶
| Client | Auth method | CSRF required |
|---|---|---|
| Browser / dashboard | _hq_session cookie | Yes — X-CSRF-Token on mutations |
CLI (bnerd) | Authorization: Bearer (JWT) | No |
| Terraform provider | Authorization: Bearer (static token) | No |
| CI/CD pipelines | Authorization: Bearer (static token) | No |
Unauthenticated endpoints¶
These endpoints do not require a token:
| Endpoint | Description |
|---|---|
GET /invite/{token} | Show invitation by token |
PATCH /invite/{token}/accept | Accept invitation |
PATCH /invite/{token}/decline | Decline invitation |
GET /invitations/details?token=... | Invitation details |
POST /webhooks/{id}/receive | Inbound webhook payload |
All other endpoints return 401 Unauthorized when no valid token is supplied.
Organization context¶
When an account belongs to more than one organization, every request must declare which organization it targets via the ?organization_id query parameter:
The server validates that the token owner is a member of the named organization. Requests that name an organization the caller does not belong to are rejected with 403 Forbidden (fail-closed). There is no server-side default for multi-org accounts — the parameter is mandatory for them.
Accounts with exactly one membership continue to work without the parameter; the server resolves the single membership automatically and the parameter is optional.
The resolved organization_id is echoed in every response envelope's metadata field and in current_organization_id on GET /accounts/me:
Accepting an invitation into an additional organization¶
An existing account receives an invitation email with a token. The accept flow is unauthenticated (no Bearer token required):
# 1. Inspect the invitation
curl "https://api.bnerd.cloud/invite/{token}"
# 2. Accept it — the account gains a new membership immediately
curl -X PATCH "https://api.bnerd.cloud/invite/{token}/accept"
After a successful accept, GET /accounts/me returns the new membership in memberships[]. All requests to the new organization must carry ?organization_id=<new-org-uuid>.
Error responses¶
| Status | code | Cause |
|---|---|---|
401 | unauthorized | No token or invalid token |
403 | forbidden | Valid token but insufficient permissions, non-member organization_id, or missing X-CSRF-Token on a cookie-authenticated mutation |
429 | rate_limited | Too many requests (300 req/60 s per token) |