Authentication & Account Management
Public Auth Routes
POST /auth/register
Register a free owner account, organisation, and store. The API returns a session immediately; signup does not send a phone OTP.
Request Body:
{
"first_name": "John",
"last_name": "Doe",
"phone": "+255712345678",
"email": "john@example.com",
"password": "securepass",
"confirm_password": "securepass",
"occupation": "retailer",
"shop": {
"name": "My Shop",
"business_type": "retail",
"region": "Dar es Salaam",
"district": "Kinondoni",
"ward": "Msasani",
"street": "Sea View Rd",
"address": "P.O. Box 123",
"latitude": -6.7738,
"longitude": 39.2585
}
}Response (201):
{
"success": true,
"data": {
"user_id": "uuid",
"org_id": "uuid",
"store_id": "uuid",
"tokens": { "access_token": "...", "refresh_token": "...", "expires_in": 900, "token_type": "Bearer" },
"status": "active"
}
}Side Effects: Sends a welcome email and optional email verification link. No OTP or trial subscription is created.
POST /auth/otp/send
Send a 6-digit OTP via SMS.
Request Body:
{
"phone": "+255712345678",
"purpose": "login"
}Purpose values: login, reset. Phone OTP is not part of signup.
Response (200):
{
"success": true,
"message": "Code sent"
}POST /auth/otp/verify
Verify OTP and receive JWT tokens.
Request Body:
{
"phone": "+255712345678",
"purpose": "login",
"code": "123456"
}Response (200):
{
"success": true,
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_in": 900,
"token_type": "Bearer"
}
}Side Effects: Marks phone as verified (if purpose is registration).
POST /auth/login
Password-based login with account lockout protection.
Request Body:
{
"identifier": "john@example.com",
"password": "securepass"
}Response (200):
{
"success": true,
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_in": 900,
"token_type": "Bearer",
"user": {
"id": "uuid",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"role": "owner"
}
}
}Lockout: After 5 failed attempts, the account is locked for 15 minutes.
POST /auth/refresh
Get a new token pair using a refresh token.
Request Body:
{
"refresh_token": "eyJ..."
}Response: Same as /auth/otp/verify.
GET /auth/confirm-email
Email verification link (opened from email).
Query: ?token=<verification_token>
Response: Redirects to {SITE_URL}/login?verified=1
POST /auth/password/forgot
Request a password reset OTP. Always returns success (no account enumeration).
Request Body:
{
"identifier": "john@example.com"
}Response (200):
{
"success": true,
"message": "If the account exists, a reset code has been sent"
}POST /auth/password/reset
Reset password using OTP.
Request Body:
{
"identifier": "john@example.com",
"otp": "123456",
"new_password": "newsecurepass",
"confirm_password": "newsecurepass"
}Response (200):
{
"success": true,
"message": "Password reset successfully"
}Authenticated Auth Routes
POST /auth/logout
Stateless logout (client discards tokens).
Auth: Required
Response (200):
{
"success": true,
"message": "Logged out"
}POST /auth/resend-confirmation
Resend email verification link.
Auth: Required
Response (200):
{
"success": true,
"message": "Confirmation email sent"
}Account Management
GET /accounts/me
Get current user's full profile including organisation and store data.
Auth: Required
Response (200):
{
"success": true,
"data": {
"user": { "id": "...", "first_name": "...", "role": "owner", ... },
"organisation": { "id": "...", "name": "...", ... },
"store": { "id": "...", "name": "...", ... },
"stores": [ ... ]
}
}PATCH /accounts/me
Update own profile.
Auth: Required
Request Body (all optional):
{
"first_name": "John",
"last_name": "Doe",
"email": "new@example.com",
"occupation": "retailer",
"password": "newpassword"
}GET /accounts/organisation
Get organisation details.
Auth: Required + Owner
PATCH /accounts/organisation
Update organisation settings and integration keys.
Auth: Required + Owner
Request Body (all optional):
{
"name": "My Organisation",
"legal_name": "My Org Ltd",
"tin": "123456789",
"business_type": "retail",
"region": "Dar es Salaam",
"district": "Kinondoni",
"ward": "Msasani",
"street": "Sea View Rd",
"address": "P.O. Box 123",
"latitude": -6.7738,
"longitude": 39.2585,
"phone": "+255712345678",
"email": "org@example.com",
"logo": "https://...",
"sendafrica_api_key": "SA-...",
"sms_sender_id": "ZiadaPOS",
"ngamia_api_key": "..."
}POST /accounts/switch-store
Switch active store context.
Auth: Required
Request Body:
{
"store_id": "uuid"
}Response: StoreResponse object.
GET /accounts/users
List all users in the organisation.
Auth: Required + Owner
Response: Array of UserResponse objects.
POST /accounts/users
Create a staff user. Temporary password sent via SMS.
Auth: Required + Owner
Request Body:
{
"first_name": "Jane",
"last_name": "Smith",
"phone": "+255712345679",
"email": "jane@example.com",
"role": "staff",
"store_id": "uuid",
"can_refund": false,
"can_discount": true,
"can_view_reports": false
}Response: UserResponse object.
DELETE /accounts/users/:id
Soft-deactivate a user.
Auth: Required + Owner
Response (204): No Content
GET /accounts/ai-credits
Get monthly AI credit usage.
Auth: Required
Response (200):
{
"success": true,
"data": {
"used": 45,
"allocated": 100,
"remaining": 55,
"month": "2024-01",
"percentage": 45.0
}
}Onboarding metadata
Public business types, regions, districts, and pharmacy outlet types are documented in Onboarding metadata.
Admin authenticator MFA
Admin login can require a second step. When an enrolled admin signs in with a password, POST /auth/login returns mfa_required and an mfa_token instead of an access token. Submit that challenge and a six-digit authenticator code to POST /auth/mfa/totp/verify to complete login.
Admin authenticator management routes:
| Method | Path | Purpose |
|---|---|---|
GET | /accounts/mfa/totp/status | Read enrollment status. |
POST | /accounts/mfa/totp/setup | Begin setup and receive the enrollment details. |
POST | /accounts/mfa/totp/verify | Confirm setup with a code. |
POST | /accounts/mfa/totp/disable | Disable after password and authenticator verification. |
These management endpoints require an authenticated admin session. See the API-owned MFA route implementation (opens in a new tab) for current access checks.