Skip to content

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)

{
  "two_fa_required": true,
  "challenge_token": "tx_..."
}

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

{
  "access_token": "eyJhbGc...",
  "refresh_token": "eyJhbGc...",
  "expires_in": 86400
}

Wrong code: 401 + attempts_remaining in body. After 5 failures: 429 + Retry-After: 1800.

Step 3 — Requests with JWT

curl https://your-domain.tld/api/v1/hosts \
  -H "Authorization: Bearer eyJhbGc..."

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

curl -X POST https://your-domain.tld/api/v1/auth/logout \
  -H "Authorization: Bearer <access_token>"

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:

curl https://your-domain.tld/api/v1/hosts \
  -H "Authorization: Bearer <pat>"

PATs are functionally identical to user JWTs — same permissions, same tenant binding.

Permission failures

Missing permission:

{
  "detail": {
    "error": "permission_denied",
    "missing": "host.delete"
  }
}

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