Auth & Accounts
Registration, email/phone OTP verification, login, JWT refresh rotation, password reset, OAuth linking, and API key management endpoints.
One accounts row backs both access modes. New accounts receive a 6-digit email OTP automatically; login is blocked until the email is verified (403 email_not_verified) unless the server sets RELAX_EMAIL_VERIFICATION_FOR_LOGIN=true.
#Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /v1/auth/register | Public | Create account (sends OTP email) |
| POST | /v1/auth/login | Public | Get JWT token pair |
| POST | /v1/auth/refresh | Public | Rotate refresh → new access token |
| POST | /v1/auth/send-verification-email | Public | Trigger email OTP |
| POST | /v1/auth/verify-email | Public | Confirm email OTP |
| POST | /v1/auth/reset-password | Public | Request password-reset OTP |
| POST | /v1/auth/reset-password-confirm | Public | Confirm reset with OTP + new password |
| GET | /v1/auth/google, /github | Public | Start OAuth login/register (302) |
| GET | /v1/auth/google/callback | Public | Provider redirect target |
| POST | /v1/auth/oauth/exchange | Public | Redeem one-time code for JWT pair |
| GET | /v1/auth/me | JWT | Current user profile |
| PUT | /v1/auth/me | JWT | Update profile / phone |
| POST | /v1/auth/logout | JWT | Blacklist token + revoke refresh |
| POST | /v1/auth/change-password | JWT | Change password (revokes other sessions) |
| POST | /v1/auth/send-phone-otp | JWT | Phone verification via SMS |
| POST | /v1/auth/verify-phone | JWT | Confirm phone OTP |
| POST | /v1/auth/google/link | JWT | Link Google identity to account |
| POST | /v1/auth/google/contacts-connect | JWT | Google Contacts sync consent flow |
| GET/POST | /v1/auth/api-keys | JWT | List / create developer API keys |
| DELETE | /v1/auth/api-keys/{keyId} | JWT | Revoke an API key |
#Register
/v1/auth/registerPubliccurl -s -X POST https://api.sendafrica.online/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"password": "a-strong-password-12plus",
"company_name": "Acme Ltd",
"phone": "0712345678"
}'{
"success": true,
"data": {
"account_id": "486f8a6e-ea75-47ea-b176-c8e931aed058",
"email": "john@example.com",
"message": "Registration successful. Please verify your email."
}
}| Field | Type | Required | Rules |
|---|---|---|---|
first_name | string | Yes | Max 100 chars |
last_name | string | Yes | Max 100 chars |
email | string | Yes | Valid email; unique |
password | string | Yes | 12–128 characters |
company_name | string | No | Max 255 chars |
phone | string | No | Valid Tanzania mobile if provided |
#Verify email
/v1/auth/verify-emailPubliccurl -s -X POST https://api.sendafrica.online/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{ "email": "john@example.com", "otp": "482910" }'{
"success": true,
"data": {
"message": "Email successfully verified",
"email_verified": true,
"can_login": true
}
}send-verification-email always returns 200 whether or not the address exists — this prevents email enumeration. OTPs expire after 15 minutes.
#Login
/v1/auth/loginPublic{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 900,
"email_verified": true,
"phone_verified": false,
"profile_complete": true
}
}expires_inis seconds (900 = 15 min). Refresh before expiry — see Authentication.email_verified/phone_verified/profile_completelet clients render onboarding banners without an extraGET /mecall.
#OAuth exchange flow
After the provider redirects to your frontend with ?exchange_code=..., redeem it server-side within its short TTL. The code is single-use:
curl -s -X POST https://api.sendafrica.online/v1/auth/oauth/exchange \
-H "Content-Type: application/json" \
-d '{"exchange_code": "from-the-redirect"}'Response is the same JWT pair as login. Errors: 401 invalid_exchange_code when expired or already used. To link a provider to an existing logged-in account use POST /v1/auth/google/link (or /github/link) with JWT instead.
#API keys
/v1/auth/api-keysJWTListing returns metadata only — never raw keys. Creating returns the full key exactly once; deleting revokes immediately. See Authentication for the creation example and storage guidance.