Auth & Profile API

Customer registration, login, 2FA, password management, and address CRUD.

Base: /api/v1/auth  |  X-Tenant-ID header required
POST /api/v1/auth/register Public

Create a new customer account. Sends a verification email.

Request Body

{
  "name": "Jane Doe",        // string, required
  "email": "jane@email.com", // string, required, unique
  "password": "Secure@123",  // string, required, min 8 chars
  "phone": "0712345678"      // string, optional
}

Response

{
  "success": true,
  "data": {
    "user": { "id": "uuid", "name": "Jane Doe", "email": "jane@email.com", "phone": "0712345678", "email_verified": false },
    "access_token": "eyJ..."
  },
  "message": "Check your email to verify your account."
}

Possible Errors

CodeHTTPWhen
USER_EXISTS409Email already registered
VALIDATION_ERROR422Missing required fields
POST /api/v1/auth/login Public

Authenticate. Returns access token and sets HTTP-only refresh cookie. If OTP enabled, returns otp_token instead.

Request Body

{
  "email": "jane@email.com", // required
  "password": "Secure@123"   // required
}

Response

// Without OTP:
{ "success": true, "data": { "requires_otp": false, "access_token": "eyJ...", "user": { "id", "name", "email", "role": null } } }

// With OTP enabled:
{ "success": true, "data": { "requires_otp": true, "otp_token": "eyJ..." } }

Possible Errors

CodeHTTPWhen
INVALID_CREDENTIALS401Wrong email or password
POST /api/v1/auth/otp/validate Public

Exchange OTP token + 6-digit code for a full access token (2FA login step 2).

Request Body

{
  "otp_token": "eyJ...", // from login response when requires_otp: true
  "code": "123456"       // 6-digit TOTP code from authenticator app
}

Response

{ "success": true, "data": { "access_token": "eyJ...", "user": { ... } } }

Possible Errors

CodeHTTPWhen
INVALID_OTP401Wrong TOTP code
INVALID_TOKEN401Expired otp_token
POST /api/v1/auth/logout Public

Clear the HTTP-only refresh token cookie.

Response

{ "success": true, "message": "Logged out successfully." }
POST /api/v1/auth/refresh Public

Get a new access token using the refresh cookie.

Response

{ "success": true, "data": { "access_token": "eyJ..." } }

Possible Errors

CodeHTTPWhen
AUTH_REQUIRED401No refresh cookie present
INVALID_TOKEN401Cookie expired
GET /api/v1/auth/me 🔒 Auth Required

Get the current authenticated user profile.

Response

{ "success": true, "data": { "id": "uuid", "name": "Jane", "email": "...", "phone": "...", "avatar_url": null, "email_verified": true, "otp_enabled": false, "role": null, "preferences": {} } }
PUT /api/v1/auth/me 🔒 Auth Required

Update name, phone, or avatar_url.

Request Body

{ "name": "Jane Smith", "phone": "0700000000", "avatar_url": "https://..." }

Response

{ "success": true, "data": { "id", "name", "email", "phone", "avatar_url" } }
PUT /api/v1/auth/me/password 🔒 Auth Required

Change password. Requires current password for verification.

Request Body

{ "current_password": "OldPass@1", "new_password": "NewPass@2" }

Response

{ "success": true, "message": "Password updated successfully." }

Possible Errors

CodeHTTPWhen
INVALID_CREDENTIALS401Current password wrong
VALIDATION_ERROR422Missing fields
POST /api/v1/auth/forgot-password Public

Send a password reset link to the given email. Always returns success to prevent enumeration.

Request Body

{ "email": "jane@email.com" }

Response

{ "success": true, "message": "If that email exists, a password reset link has been sent." }
POST /api/v1/auth/reset-password Public

Reset password using the token from the email link.

Request Body

{ "token": "hex-token", "email": "jane@email.com", "new_password": "NewPass@2" }

Response

{ "success": true, "message": "Password reset successfully. Please log in." }

Possible Errors

CodeHTTPWhen
INVALID_TOKEN400Token expired or incorrect
POST /api/v1/auth/otp/setup 🔒 Auth Required

Generate a TOTP secret and QR code URL. Save the secret in your authenticator app.

Response

{ "success": true, "data": { "secret": "BASE32SECRET", "qr_code_url": "otpauth://totp/ALOPAY:jane@email.com?secret=...&issuer=ALOPAY" } }
POST /api/v1/auth/otp/verify 🔒 Auth Required

Confirm OTP setup by submitting the first 6-digit code. Enables 2FA on account.

Request Body

{ "code": "123456" }

Response

{ "success": true, "message": "Two-factor authentication is now enabled." }

Possible Errors

CodeHTTPWhen
INVALID_OTP401Wrong code
DELETE /api/v1/auth/otp/disable 🔒 Auth Required

Disable 2FA. Requires password confirmation.

Request Body

{ "password": "Secure@123" }

Response

{ "success": true, "message": "Two-factor authentication disabled." }

Possible Errors

CodeHTTPWhen
INVALID_CREDENTIALS401Wrong password

Addresses /api/v1/addresses

GET /api/v1/addresses 🔒 Auth Required

List all saved addresses for the current user.

Response

{ "success": true, "data": { "addresses": [ { "id", "label", "recipient_name", "phone", "street", "county", "town", "is_default" } ] } }
POST /api/v1/addresses 🔒 Auth Required

Add a new delivery address.

Request Body

{
  "label": "Home",                              // required
  "recipient_name": "Jane Doe",                 // required
  "phone": "0712345678",                        // required
  "street": "Kimathi Street",                   // required
  "county": "nairobi",                          // required — must be valid Kenya county
  "town": "Nairobi CBD",                        // optional
  "landmark": "Next to KFC",                    // optional
  "coordinates": { "lat": -1.286, "lng": 36.817 }, // optional
  "is_default": true                            // optional
}

Response

{ "success": true, "data": { "address": { "id", "label", ... } } }
PUT /api/v1/addresses/:id 🔒 Auth Required

Update a saved address. Same fields as POST.

Response

{ "success": true, "data": { "address": { ... } } }
DELETE /api/v1/addresses/:id 🔒 Auth Required

Delete a saved address.

Response

{ "success": true, "data": null }
PATCH /api/v1/addresses/:id/default 🔒 Auth Required

Set this address as the default. Clears default from all other addresses.

Response

{ "success": true, "data": { "address": { "id", "is_default": true } } }