API Reference
Auth Endpoints

Auth Endpoints

Account lifecycle: registration, login, verification, password reset, deletion. All bodies are JSON.


POST /auth/register

Create an account with either an email or a phone, plus a password.

Auth: none · Rate limit: 5/min/IP

{
  "email": "amina@example.com",
  "phone": null,
  "password": "correct-horse-battery"
}
FieldTypeRules
emailstring | nullProvide one of email/phone; no format validation beyond presence
phonestring | nullOptional leading +, digits only, total length 7–16
passwordstring≥ 8 characters
curl -s -X POST "$ISSUER/auth/register" -H "Content-Type: application/json" \
  -d '{"email":"amina@example.com","password":"correct-horse"}'

201 Created — and a 6-digit verification OTP is emailed automatically when registering by email:

{
  "success": true,
  "data": {
    "id": "b3f1c92e-4d5a-4f6b-8a7c-9e0d1f2a3b4c",
    "email": "amina@example.com",
    "phone": null,
    "email_verified": false,
    "phone_verified": false
  }
}

Errors:

StatusMessage
400password must be at least 8 characters · email or phone is required · invalid request body
409an account with that email or phone already exists

Emails are matched case-sensitively and are not format-checked server-side — normalize on your side if you care.


POST /auth/login

Log in with email or phone + password.

Auth: none · Rate limit: 10/min/IP

curl -s -c cookies.txt -X POST "$ISSUER/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"identifier":"amina@example.com","password":"correct-horse"}'

200 — normal success (sets the camel_session cookie):

{ "success": true, "data": { "id": "…", "email": "amina@example.com", "phone": null, "email_verified": true, "phone_verified": false } }

200 — MFA challenge (account has TOTP enabled; no cookie set yet):

{
  "success": true,
  "data": {
    "mfa_required": true,
    "mfa_token": "6f0e3f2a-1c4b-4d5e-9f80-a7b6c5d4e3f2",
    "user_id": "b3f1c92e-…"
  }
}

Finish via POST /auth/mfa/verify. The mfa_token lives ~5 minutes.

Errors: 401email/phone or password incorrect.


GET /auth/me

Current user from the session cookie. Auth: session.

{ "success": true, "data": { "id": "…", "email": "…", "phone": null, "email_verified": true, "phone_verified": false } }

401 not authenticated (no cookie) / session expired or invalid (stale).


POST /auth/logout

Revokes the current session server-side and clears the cookie. Auth: session.

{ "success": true, "message": "logged out" }

POST /auth/verify-email/send

Send a 6-digit OTP to the account's email address. No body.

Auth: session · Rate limit: otp-send (3/hour/IP) + per-account cap (3/hour)

{ "success": true, "message": "verification code sent" }

Errors: 400 no email address on account · 429 too many codes requested, try again later.

OTP facts: hashed at rest, TTL 5 minutes, single-use after ≤5 attempts.

POST /auth/verify-email/check

{ "code": "483920" }
{ "success": true, "message": "email verified" }

Errors: 400 incorrect code · 400 code expired, request a new one.


POST /auth/verify-phone/send

Attach (unverified) + verify a phone number in one flow. Auth: session · Rate limit: otp-send caps apply.

{ "phone": "+255712345678" }

Validation error text is explicit: please enter a valid phone number (e.g. +255712345678).

Success: { "success": true, "message": "verification code sent" }. If the number belongs to another account: 400 an account with that email or phone already exists.

POST /auth/verify-phone/check

{ "code": "104928" }

{ "success": true, "message": "phone verified" } (same errors as the email check).


POST /auth/forgot-password

Request a reset. Always answers 200 regardless of account existence — no user enumeration.

Auth: none · No IP rate limiter (per-account delivery costs act as the natural brake)

{ "identifier": "amina@example.com" }
{ "success": true, "message": "if an account exists, a reset link has been sent" }

Delivery depends on what matched:

Identifier matchedDeliveryTTL
EmailLink: <issuer>/reset-password?token=<64 hex chars>1 hour
Verified phoneSMS with 6-digit code15 min

Empty identifier → 400 identifier is required.

POST /auth/reset-password

{ "token": "<from email link>", "new_password": "new-secret-123" }
{ "success": true, "message": "password reset successfully" }

Side effect: all sessions of the user are revoked — every device must log in again. Errors: 400 token and new_password are required · 400 password must be at least 8 characters · 400 reset link expired or already used.


DELETE /auth/account

Delete the account permanently — foreign keys cascade all associated data; session cleared. Auth: session.

{ "success": true, "message": "account deleted" }