Skip to content

Authentication & Tokens

Every API request (except a small number of public endpoints) must include an Authorization header. Two token types are accepted:

Authorization: Bearer <jwt-or-static-token>

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:

POST /login
Content-Type: application/json

{ "login": "you@example.com", "password": "..." }

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.

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.

Cookie-authenticated mutating requests must include the CSRF token as a header:

X-CSRF-Token: <value of the _hq_csrf cookie>

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

DELETE /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 Bearer prefix 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 /tokens endpoints reject static tokens with 403 Forbidden.

Scopes follow <resource>:<level>, where level is read, write, manage, or * (wildcard). Examples:

dns:read
dns:write
rgw_users:*
projects:read

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:

GET /projects?organization_id=<uuid>

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:

{
  "metadata": {
    "request_id": "req_...",
    "organization_id": "a1b2c3d4-..."
  },
  "data": { ... }
}

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)