Auth flow¶
Step 1 — Login¶
curl -X POST https://your-domain.tld/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"..."}'
Response (without 2FA)¶
{
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"token_type": "bearer",
"expires_in": 86400
}
Response (with 2FA)¶
Server has sent a 2FA code email. Continue with challenge_token:
Step 2 — Verify 2FA¶
curl -X POST https://your-domain.tld/api/v1/auth/2fa/verify \
-H "Content-Type: application/json" \
-d '{"challenge_token":"tx_...","code":"12345678"}'
Response¶
Wrong code: 401 + attempts_remaining in body. After 5 failures: 429 + Retry-After: 1800.
Step 3 — Requests with JWT¶
Refresh¶
curl -X POST https://your-domain.tld/api/v1/auth/refresh \
-H "Authorization: Bearer <refresh_token>"
Returns a new access_token (and new refresh_token). Refresh token is single-use.
Logout¶
Server invalidates the refresh token, frontend discards the access token.
Personal access tokens (PATs)¶
Alternative for CLI/scripts without browser login:
curl -X POST https://your-domain.tld/api/v1/auth/api-keys \
-H "Authorization: Bearer <jwt>" \
-d '{
"name":"deploy-script",
"scope":"read_write",
"expires_at":"2027-01-01T00:00:00Z"
}'
Response contains the plaintext token exactly once. Save it.
Use:
PATs are functionally identical to user JWTs — same permissions, same tenant binding.
Permission failures¶
Missing permission:
HTTP 403.
Tenant scope¶
JWT contains tenant_id (own tenant) and tenant_scope (null for super admin, otherwise tenant UUID).
Cross-tenant queries (super admin) optional via query: ?tenant_id=<uuid>. Without query you get your own tenant.
Rate limits¶
- Login/2FA: 10 req/min per IP
- Other endpoints: 600 req/min per user
On exceed: 429 + Retry-After.
Time sync¶
JWT exp claim assumes client and server are time-synced (default 30 s tolerance). With server clock wrong (no NTP), phantom 401 errors appear.
Next¶
- API reference
- Cookbook — code snippets
- 2FA