Authentication
Overview
Section titled “Overview”Whatomate supports two authentication methods:
- Session cookies - For user sessions and frontend applications
- API Keys - For server-to-server integrations and automation
Logging in (or registering, refreshing, or switching org) sets three cookies and returns only
{ expires_in, user } in the body. No token is ever returned in the response body.
| Cookie | httpOnly | Path | Max-Age | Purpose |
|---|---|---|---|---|
whm_access | yes | <base_path>/api | jwt.access_expiry_mins | The access JWT |
whm_refresh | yes | <base_path>/api/auth/refresh | jwt.refresh_expiry_days | The refresh JWT |
whm_csrf | no | <base_path>/ | jwt.refresh_expiry_days | Read by JS and echoed back in X-CSRF-Token |
All three are SameSite=Lax, and Secure when cookie.secure is set (forced on in production).
Because the two auth cookies are httpOnly, JavaScript cannot read them; the browser attaches them
automatically.
Cookie-authenticated mutating requests must also send the whm_csrf value in an
X-CSRF-Token header — see CSRF.
For non-browser clients, use an API key in the X-API-Key
header — those requests skip the CSRF check. An Authorization: Bearer <token> header is accepted
only as a fallback.
Register
Section titled “Register”Create a new user account within an existing organization. Registration requires an organization_id, which is typically provided via an invitation link.
POST /api/auth/registerRequest Body
Section titled “Request Body”{ "email": "user@example.com", "password": "securepassword123", "full_name": "John Doe", "organization_id": "uuid"}| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
password | string | Yes | Must be non-empty. The API enforces no minimum length here — see the note below |
full_name | string | Yes | Display name |
organization_id | string | Yes | UUID of the organization to join |
Response
Section titled “Response”Sets the whm_access and whm_refresh httpOnly cookies. The body contains the expiry hint and the full user object — no tokens.
{ "status": "success", "data": { "expires_in": 900, "user": { "id": "uuid", "email": "user@example.com", "full_name": "John Doe", "organization_id": "uuid", "role": { "id": "uuid", "name": "agent" } } }}Authenticate and start a session. On success, the whm_access and whm_refresh httpOnly cookies are set and the body returns the full user object (note the field is full_name, not name).
POST /api/auth/loginRequest Body
Section titled “Request Body”{ "email": "user@example.com", "password": "securepassword12"}Response
Section titled “Response”{ "status": "success", "data": { "expires_in": 900, "user": { "id": "uuid", "email": "user@example.com", "full_name": "John Doe", "organization_id": "uuid", "role": { "id": "uuid", "name": "agent" } } }}Refresh Token
Section titled “Refresh Token”Rotate the access token. The endpoint reads the refresh token from the whm_refresh cookie first, falling back to a refresh_token field in the JSON body. On success, both cookies are re-issued.
POST /api/auth/refreshRequest Body
Section titled “Request Body”The body is optional when the whm_refresh cookie is present. Provide it only for non-browser clients:
{ "refresh_token": "eyJhbGciOiJIUzI1NiIs..."}Response
Section titled “Response”{ "status": "success", "data": { "expires_in": 900, "user": { "id": "uuid", "email": "user@example.com", "full_name": "John Doe" } }}Switch Organization
Section titled “Switch Organization”Switch the current user's active organization. Re-issues the auth cookies scoped to the target organization with the user's org-specific role and permissions.
POST /api/auth/switch-orgRequest Body
Section titled “Request Body”{ "organization_id": "uuid"}Response
Section titled “Response”{ "status": "success", "data": { "expires_in": 900, "user": { "id": "uuid", "email": "user@example.com", "full_name": "John Doe", "organization_id": "uuid", "role": { "id": "uuid", "name": "agent", "permissions": [ { "resource": "contacts", "action": "read" }, { "resource": "chat", "action": "read" } ] } } }}Logout
Section titled “Logout”Clear the session. Revokes the refresh token (read from the whm_refresh cookie, or the body as a fallback) and clears both auth cookies.
POST /api/auth/logoutResponse
Section titled “Response”{ "status": "success", "data": { "status": "logged_out" }}WebSocket Token
Section titled “WebSocket Token”Because httpOnly cookies cannot be read by JavaScript, the real-time WebSocket connection is authenticated with a short-lived token obtained here.
GET /api/auth/ws-tokenResponse
Section titled “Response”{ "status": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIs..." }}The token is a JWT with subject ws and a 30-second lifetime — fetch a fresh one for every
connection attempt, including reconnects.
Connecting
Section titled “Connecting”/ws upgrades without authentication. The token is never put in the URL; instead the client
sends it as the first message on the open socket, and the server registers the connection only
after validating it:
{ "type": "auth", "payload": { "token": "eyJhbGciOiJIUzI1NiIs..." } }SSO (Single Sign-On)
Section titled “SSO (Single Sign-On)”List Public SSO Providers
Section titled “List Public SSO Providers”Returns the enabled SSO providers (deduplicated by type) so a login page can render the available buttons. Public — no authentication required.
GET /api/auth/sso/providersResponse
Section titled “Response”{ "status": "success", "data": [ { "provider": "google", "name": "Google" }, { "provider": "custom", "name": "Custom" } ]}Initiate SSO
Section titled “Initiate SSO”Redirects the browser to the identity provider's OAuth authorization page. Open this URL directly in the browser (not via XHR).
GET /api/auth/sso/{provider}/initSSO Callback
Section titled “SSO Callback”The provider redirects back here after the user authenticates. On success the endpoint sets the auth cookies and redirects into the app.
GET /api/auth/sso/{provider}/callbackUsing the Session
Section titled “Using the Session”Browser clients need no extra headers — the httpOnly cookies are sent automatically. For programmatic access, send an API key instead:
curl -X GET "http://your-server:8080/api/contacts" \ -H "X-API-Key: whm_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"Token Expiration
Section titled “Token Expiration”| Token Type | Config key | Default |
|---|---|---|
| Access token | jwt.access_expiry_mins | 15 minutes |
| Refresh token | jwt.refresh_expiry_days | 1 day |
expires_in in the response body is access_expiry_mins × 60 — 900 with the defaults.
When the access token expires, call POST /api/auth/refresh to rotate it. Both durations are configurable in config.toml.