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"
}| Field | Type | Rules |
|---|---|---|
email | string | null | Provide one of email/phone; no format validation beyond presence |
phone | string | null | Optional leading +, digits only, total length 7–16 |
password | string | ≥ 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:
| Status | Message |
|---|---|
| 400 | password must be at least 8 characters · email or phone is required · invalid request body |
| 409 | an 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: 401 — email/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 matched | Delivery | TTL |
|---|---|---|
Link: <issuer>/reset-password?token=<64 hex chars> | 1 hour | |
| Verified phone | SMS with 6-digit code | 15 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" }