Okouro authenticates with JSON Web Tokens (JWT). You exchange credentials for an access token and a refresh token, send the access token on every request, and use the refresh token to get a new pair when the access token expires. Tokens are signed with RS256.
The token pair
A successful login, signup, or refresh returns an AuthResponse:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"user": { "...": "see /auth/me below" }
}
- The access token authenticates normal API calls. It is short-lived (1 hour by default).
- The refresh token mints a fresh pair. It is longer-lived (24 hours by default).
The response does not include an expires_in field. Token lifetimes are fixed by server configuration, so treat an access token as valid for roughly an hour and refresh proactively (see Refresh).
Sign up
Registration is two steps: submit your work email to receive a link, then open the link to create your company and its admin account.
Step 1 - request a registration link.
POST /auth/signup HTTP/1.1
Content-Type: application/json
{
"email": "you@acme.com"
}
Always returns 200 OK, whether or not the email is available, so it cannot be used to probe which addresses are registered. When the email is available, a registration link is emailed to it.
Step 2 - complete registration from the link.
POST /auth/complete-signup HTTP/1.1
Content-Type: application/json
{
"token": "<token from the emailed link>",
"account_name": "Acme Consulting",
"first_name": "Alex",
"last_name": "Martin",
"password": "a-strong-password"
}
Returns 200 OK with an AuthResponse and signs the new admin in. The password must be at least 8 characters. An invalid or expired token returns 400 Bad Request; an email that is already registered returns 409 Conflict.
Log in
POST /auth/login HTTP/1.1
Content-Type: application/json
{
"email": "you@acme.com",
"password": "your-password"
}
Returns 200 OK with an AuthResponse. Wrong credentials return 401 Unauthorized.
Alongside the body, the server sets an HttpOnly cookie named okouro_refresh (scoped to Path=/auth) that carries the refresh token. Browsers rely on this cookie for refresh; server-to-server integrations can ignore it and keep the refresh_token from the body instead.
Present the access token
Send the access token as a bearer token on every protected request:
GET /users HTTP/1.1
Authorization: Bearer <access_token>
A missing or invalid token returns 401 Unauthorized. A valid token whose role is not permitted for the endpoint returns 403 Forbidden (see Roles and permissions).
Who am I
GET /auth/me returns the current user as a bare UserResponse (not wrapped in an AuthResponse):
{
"id": "0190a1c2-3d4e-7f80-9a1b-2c3d4e5f6a7b",
"account_id": "0190a1c2-0000-7f80-9a1b-2c3d4e5f6a7b",
"email": "you@acme.com",
"first_name": "Alex",
"last_name": "Martin",
"role": "admin",
"created_at": "2026-01-05T09:00:00Z",
"consultant_id": null,
"settings": { "language": "en" },
"is_manager": false
}
consultant_id is set when the user is linked to a consultant profile; that link is what unlocks the self-service timesheet, assignment, time-off, and expense surfaces. is_manager is true when the user manages at least one consultant (see Roles and permissions).
Refresh
When the access token expires, request a new pair. The server reads the refresh token from the okouro_refresh cookie first, then falls back to a JSON body:
POST /auth/refresh HTTP/1.1
Content-Type: application/json
{
"refresh_token": "<refresh_token>"
}
Returns 200 OK with a fresh AuthResponse. An invalid, expired, or already-too-old refresh token returns 401 Unauthorized.
The refresh token is stateless: it cannot be revoked individually, and its exposure window is bounded only by its own TTL. Store it as carefully as a password.
Refresh strategy. Call /auth/refresh shortly before the access token expires rather than waiting for a 401. A simple approach: refresh when a request returns 401, retry it once with the new token, and treat a second 401 as a hard logout.
Log out
POST /auth/logout HTTP/1.1
Authorization: Bearer <access_token>
Returns 204 No Content and clears the okouro_refresh cookie. Because access tokens are stateless, logging out does not invalidate an access token that is still within its TTL; it only stops you from refreshing. Drop the tokens client-side as well.
Two other ways to get a session
Two public endpoints also return a full AuthResponse and set the refresh cookie, because they create or reset credentials:
POST /invitations/accept- accepting an invitation signs the new user in. See Manage people.POST /password-changes/redeem- redeeming a password-change link signs the user in with the new password.
Summary
| Action | Method | Path | Auth | Success |
|---|---|---|---|---|
| Start signup | POST | /auth/signup | none | 200 |
| Complete signup | POST | /auth/complete-signup | none | 200 |
| Log in | POST | /auth/login | none | 200 |
| Who am I | GET | /auth/me | bearer | 200 |
| Refresh | POST | /auth/refresh | cookie or body | 200 |
| Log out | POST | /auth/logout | bearer | 204 |