NextGate PONA Auth — EndPoint Doc (ACTIVE)
For more details on the full flow design: PONA Auth v3 Design Doc
What is PONA Auth?
Progressive · Onboarding · Native · Access
PONA Auth is NextGate's unified, phone-first authentication system. It replaces all legacy auth flows with a single coherent pipeline. Here is the core philosophy:
- Phone is the primary identifier — always. Every account starts with a verified phone number. No exceptions.
- Passwordless by default — users authenticate via OTP. Password and OAuth are optional enhancements added post-registration.
- Progressive onboarding — only the bare minimum is collected upfront (phone + name + birthdate). Everything else (username, email, bio, interests, profile pic) is collected lazily when the feature needs it.
- One flow, two outcomes — the same endpoints serve both new and returning users. The server decides what happens based on the account state.
Token types at a glance
| Token | Expiry | Purpose |
|---|---|---|
checkToken |
10 min | Proves a phone check was made. Single-use. |
tempToken |
15 min | Carries the OTP session. Single-use after verify. |
onboardingToken |
1 hour | Issued after OTP verify for new users. Unlocks primary onboarding. |
accessToken |
1 hour | Standard bearer token. Attached to every protected request. |
refreshToken |
30 days | Rotates on use. Used to get a new accessToken silently. |
Secure storage — frontend requirements
This is critical. Store tokens incorrectly and the whole auth system is compromised.
| Token | Where to store | Why |
|---|---|---|
accessToken |
In-memory only (React state, Zustand, etc.) | Never localStorage — XSS can steal it |
refreshToken |
HttpOnly cookie (web) / Secure Keychain (mobile) | Never localStorage or AsyncStorage directly |
onboardingToken |
In-memory only | Short-lived, no need to persist |
checkToken |
In-memory only | Single-use, discard after consuming |
tempToken |
In-memory only | Single-use, discard after OTP verify |
Standard Response Format
Success response
{
"success": true,
"httpStatus": "OK",
"message": "Human-readable message",
"action": "ACTION_CODE",
"action_time": "2026-04-03T10:30:45",
"data": {}
}
Error response
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-04-03T10:30:45",
"data": "Error description"
}
Action codes — what the frontend should do next
| Code | Meaning | Next step |
|---|---|---|
REGISTER |
Phone not found — new user | Show registration UI, proceed to channels |
LOGIN |
Phone found — existing user | Show login UI, proceed to channels |
CONTINUE_ONBOARDING |
Phone found but primary incomplete | Proceed to channels → onboarding |
PROCEED_TO_OTP |
Only one channel available | Skip channel picker, send OTP automatically |
SELECT_CHANNEL |
Multiple channels available | Show channel picker to user |
COLLECT_PRIMARY |
OTP verified, primary data needed | Show name + birthdate form |
ACCOUNT_BLOCKED |
User is underage or blocked | Show blocked message with unblock date |
VERIFY_DEVICE |
Unknown device on password login | Show device OTP verification |
HTTP Method Badges
- GET — Read only
- POST — Create / action
- DELETE — Remove
Endpoints
1. Check Phone
Purpose: The entry point for every auth flow. Checks if a phone number is registered and returns a checkToken plus the available auth methods.
Endpoint: POST {base_url}/auth/check
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"identifier": "+255745051250",
"deviceId": "android-uuid-abc123"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
identifier |
string | Yes | Phone number in international format | Must match ^\+[1-9]\d{6,14}$ |
deviceId |
string | Yes | Unique device identifier from the client | Non-empty string |
Success Response — New User (REGISTER):
{
"success": true,
"httpStatus": "OK",
"message": "Phone number not registered",
"action": "REGISTER",
"action_time": "2026-04-03T10:30:45",
"data": {
"exists": false,
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"primaryComplete": false,
"maskedPhone": null,
"authMethods": null
}
}
Success Response — Existing User (LOGIN):
{
"success": true,
"httpStatus": "OK",
"message": "Welcome back",
"action": "LOGIN",
"action_time": "2026-04-03T10:30:45",
"data": {
"exists": true,
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"primaryComplete": true,
"maskedPhone": "••• ••• ••50",
"authMethods": {
"passwordless": true,
"password": false,
"google": true,
"apple": false
}
}
}
Success Response — Existing User, Primary Incomplete:
{
"success": true,
"httpStatus": "OK",
"message": "Continue setting up your account",
"action": "CONTINUE_ONBOARDING",
"action_time": "2026-04-03T10:30:45",
"data": {
"exists": true,
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"primaryComplete": false,
"maskedPhone": "••• ••• ••50",
"authMethods": {
"passwordless": true,
"password": false,
"google": false,
"apple": false
}
}
}
Response Fields:
| Field | Description |
|---|---|
exists |
Whether the phone is registered |
checkToken |
Short-lived token to proceed. Always present regardless of exists. |
primaryComplete |
Whether the user has completed name + birthdate setup |
maskedPhone |
Masked phone for display e.g. ••• ••• ••50. Null for new users. |
authMethods.passwordless |
Always true — OTP is always available |
authMethods.password |
True if user has set a password |
authMethods.google |
True if Google is linked |
authMethods.apple |
True if Apple is linked |
Frontend handling:
action = REGISTER → store checkToken in memory
show "Continue" or proceed directly to channels
do NOT show a password field
do NOT show Google button
action = LOGIN → store checkToken in memory
show auth method buttons based on authMethods
show Google button ONLY if authMethods.google = true
show Password button ONLY if authMethods.password = true
always show OTP button
action = CONTINUE_ONBOARDING → same as LOGIN
user will be redirected to primary onboarding
after OTP verify
Standard Error Types:
422 UNPROCESSABLE_ENTITY— invalid phone format or missing deviceId
2. Get Passwordless Channels
Purpose: Returns the available OTP channels (phone, email) for an existing user. Used to let the user choose where to receive their OTP.
Endpoint: POST {base_url}/auth/passwordless/channels
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"deviceId": "android-uuid-abc123"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkToken |
string | Yes | Token from /auth/check |
Non-empty |
deviceId |
string | Yes | Must match the deviceId used in /auth/check |
Non-empty |
Success Response — Single Channel:
{
"success": true,
"httpStatus": "OK",
"message": "Sending code to your phone",
"action": "PROCEED_TO_OTP",
"action_time": "2026-04-03T10:30:45",
"data": {
"channels": [
{
"type": "PHONE",
"masked": "••• ••• ••50",
"isPrimary": true
}
]
}
}
Success Response — Multiple Channels:
{
"success": true,
"httpStatus": "OK",
"message": "Choose where to receive your code",
"action": "SELECT_CHANNEL",
"action_time": "2026-04-03T10:30:45",
"data": {
"channels": [
{ "type": "PHONE", "masked": "••• ••• ••50", "isPrimary": true },
{ "type": "EMAIL", "masked": "j••••••@g••••.com", "isPrimary": false }
]
}
}
Frontend handling:
action = PROCEED_TO_OTP → skip channel picker UI
immediately call /auth/passwordless-start
with channel = PHONE
action = SELECT_CHANNEL → show channel picker
display masked values so user knows where OTP goes
user taps their preferred channel
then call /auth/passwordless-start
Standard Error Types:
403 FORBIDDEN— invalid, expired, or already-used checkToken403 FORBIDDEN— deviceId mismatch
Note: This endpoint does NOT consume the checkToken. The checkToken stays valid for use in
/auth/passwordless-start.
3. Start Passwordless OTP
Purpose: Sends an OTP to the chosen channel and returns a tempToken for the verify step.
Endpoint: POST {base_url}/auth/passwordless-start
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"channel": "PHONE",
"deviceId": "android-uuid-abc123"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkToken |
string | Yes | Token from /auth/check |
Non-empty |
channel |
string | Yes | Where to send OTP | enum: PHONE, EMAIL |
deviceId |
string | Yes | Must match deviceId used in /auth/check |
Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Verification code sent",
"action_time": "2026-04-03T10:30:45",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiJ9...",
"maskedDestination": "••• ••• ••50",
"channel": "PHONE",
"expiresInSeconds": 120,
"resendAvailableAfterSeconds": 60
}
}
Response Fields:
| Field | Description |
|---|---|
tempToken |
Carry this to /auth/otp-verify. Store in memory only. |
maskedDestination |
Show this to the user so they know where OTP was sent |
expiresInSeconds |
OTP is valid for this many seconds (120 = 2 min) |
resendAvailableAfterSeconds |
Tell user to wait this long before hitting resend |
Frontend handling:
On success:
→ store tempToken in memory
→ show OTP input screen
→ display maskedDestination e.g. "Code sent to ••• ••• ••50"
→ start a countdown timer using resendAvailableAfterSeconds
→ when timer reaches 0, enable the resend button
On EMAIL channel failure (user has no verified email):
→ server returns 400
→ fall back to PHONE channel automatically
Standard Error Types:
403 FORBIDDEN— checkToken invalid, expired, or already consumed403 FORBIDDEN— EMAIL channel chosen but account has no verified email400 BAD_REQUEST— invalid channel value
4. Verify OTP
Purpose: Validates the OTP and returns either an accessToken (returning user with primary complete) or an onboardingToken (new or incomplete user).
Endpoint: POST {base_url}/auth/otp-verify
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"tempToken": "eyJhbGciOiJIUzI1NiJ9...",
"otp": "482910",
"deviceName": "Josh's Pixel 4a",
"platform": "ANDROID"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
tempToken |
string | Yes | Token from /auth/passwordless-start |
Non-empty |
otp |
string | Yes | 6-digit code from SMS or email | Exactly 6 numeric digits |
deviceName |
string | No | Human-readable device name | Optional, for device registry |
platform |
string | No | Client platform | enum: ANDROID, IOS, WEB |
Success Response — Primary Complete (Login):
{
"success": true,
"httpStatus": "OK",
"message": "Welcome back",
"action": null,
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"onboardingToken": null,
"primaryComplete": true,
"onboarding": {
"primaryComplete": true,
"username": true,
"email": false,
"profilePic": false,
"interests": false,
"bio": false
}
}
}
Success Response — Primary Incomplete (New User or Returning, Incomplete):
{
"success": true,
"httpStatus": "OK",
"message": "Phone verified. Let us set up your account.",
"action": "COLLECT_PRIMARY",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": null,
"refreshToken": null,
"onboardingToken": "eyJhbGciOiJIUzI1NiJ9...",
"primaryComplete": false,
"onboarding": {
"primaryComplete": false,
"username": false,
"email": false,
"profilePic": false,
"interests": false,
"bio": false
}
}
}
Frontend handling:
primaryComplete = true:
→ store accessToken in memory
→ store refreshToken in HttpOnly cookie (web) or Keychain (mobile)
→ discard tempToken
→ navigate to home / main app
→ check onboarding flags for secondary prompts
primaryComplete = false:
→ store onboardingToken in memory
→ discard tempToken
→ navigate to primary onboarding screen
→ do NOT store onboardingToken in localStorage
Standard Error Types:
403 FORBIDDEN— wrong OTP403 FORBIDDEN— OTP expired403 FORBIDDEN— max attempts exceeded (3 wrong OTPs)403 FORBIDDEN— tempToken already used
5. Resend OTP
Purpose: Resends the OTP to the same destination. Rate limited to 5 attempts per session with a 60-second cooldown between each.
Endpoint: POST {base_url}/auth/resend-otp
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"tempToken": "eyJhbGciOiJIUzI1NiJ9..."
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
tempToken |
string | Yes | Current tempToken from the OTP session | Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "OTP resent successfully",
"action_time": "2026-04-03T10:30:45",
"data": null
}
Frontend handling:
On success:
→ show "Code resent" confirmation
→ reset the countdown timer to resendAvailableAfterSeconds
→ disable resend button again
→ keep the same tempToken — it is still valid
On 400 — cooldown active:
→ show "Please wait X seconds"
→ do not clear the OTP input
On 400 — max attempts:
→ show "Too many attempts. Please start over."
→ clear tempToken from memory
→ navigate back to phone entry
Standard Error Types:
400 BAD_REQUEST— cooldown period not yet elapsed400 BAD_REQUEST— max resend attempts (5) reached400 BAD_REQUEST— tempToken expired or invalid
6. Primary Onboarding
Purpose: Collects the user's name and date of birth. This completes primary onboarding and issues the first accessToken. Must be called with a valid onboardingToken.
Endpoint: POST {base_url}/auth/onboarding/primary
Access Level: 🌐 Public
Authentication: None (uses onboardingToken in body)
Request JSON Sample:
{
"onboardingToken": "eyJhbGciOiJIUzI1NiJ9...",
"firstName": "Joshua",
"lastName": "Sakweli",
"birthDate": "1995-06-15"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
onboardingToken |
string | Yes | Token from /auth/otp-verify |
Non-empty |
firstName |
string | Yes | User's first name | 1–50 characters |
lastName |
string | Yes | User's last name | 1–50 characters |
birthDate |
string | Yes | Date of birth in ISO format | YYYY-MM-DD, must be in the past |
Success Response — Normal User:
{
"success": true,
"httpStatus": "OK",
"message": "Welcome to NextGate!",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"accountTier": "FULL",
"onboarding": {
"primaryComplete": true,
"username": false,
"email": false,
"profilePic": false,
"interests": false,
"bio": false
},
"blocked": false,
"unblockDate": null
}
}
Success Response — Underage User (blocked):
{
"success": true,
"httpStatus": "OK",
"message": "Account blocked",
"action": "ACCOUNT_BLOCKED",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": null,
"refreshToken": null,
"accountTier": null,
"onboarding": null,
"blocked": true,
"unblockDate": "2026-09-15"
}
}
Response Fields:
| Field | Description |
|---|---|
accessToken |
Bearer token. Store in memory. Null if blocked. |
refreshToken |
Rotation token. Store in HttpOnly cookie or Keychain. Null if blocked. |
accountTier |
FULL (18+), RESTRICTED (13–17), MINOR (under 13 — blocked) |
onboarding.primaryComplete |
Always true after this endpoint succeeds |
onboarding.* |
Flags for secondary onboarding steps |
blocked |
True if user is underage |
unblockDate |
Date when user turns 13. Show to user. |
Frontend handling:
blocked = false:
→ store accessToken in memory
→ store refreshToken securely
→ discard onboardingToken
→ navigate to home / main app
→ check onboarding flags:
if username = false → prompt for username (can be skipped)
if interests = false → prompt for interests (can be skipped)
etc.
blocked = true:
→ do NOT store any tokens
→ show age restriction screen
→ display unblockDate clearly
→ do NOT allow navigation to app
accountTier = RESTRICTED:
→ user is 13–17
→ some features may be restricted in your UI
→ check your feature flags per tier
Standard Error Types:
403 FORBIDDEN— onboardingToken invalid or expired (1 hour limit)403 FORBIDDEN— primary onboarding already completed422 UNPROCESSABLE_ENTITY— validation errors on name or birthDate
7. Password Login
Purpose: Authenticates a user with their phone + password combination. May require device verification if the device is unknown or risk is high.
Endpoint: POST {base_url}/auth/login/password
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"password": "MySecurePassword123",
"deviceId": "android-uuid-abc123",
"deviceName": "Josh's Pixel 4a",
"platform": "ANDROID"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkToken |
string | Yes | Token from /auth/check |
Non-empty |
password |
string | Yes | User's password | Non-empty |
deviceId |
string | Yes | Device identifier | Non-empty |
deviceName |
string | No | Human-readable device name | Optional |
platform |
string | No | Client platform | enum: ANDROID, IOS, WEB |
Success Response — Known Device:
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"onboarding": {
"primaryComplete": true,
"username": true,
"email": false,
"profilePic": false,
"interests": true,
"bio": false
},
"requiresDeviceVerification": false,
"deviceVerificationToken": null,
"maskedDestination": null
}
}
Success Response — Unknown / High Risk Device:
{
"success": true,
"httpStatus": "OK",
"message": "Device verification required",
"action": "VERIFY_DEVICE",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": null,
"refreshToken": null,
"requiresDeviceVerification": true,
"deviceVerificationToken": "eyJhbGciOiJIUzI1NiJ9...",
"maskedDestination": "••• ••• ••50"
}
}
Frontend handling:
requiresDeviceVerification = false:
→ store accessToken in memory
→ store refreshToken securely
→ navigate to home
requiresDeviceVerification = true:
→ store deviceVerificationToken in memory
→ show OTP input with maskedDestination
→ call POST /api/v1/account/device/verify with the OTP
→ on success you get accessToken + refreshToken
Standard Error Types:
403 FORBIDDEN— wrong password403 FORBIDDEN— checkToken invalid or expired403 FORBIDDEN— too many failed attempts — account temporarily locked403 FORBIDDEN— password not set on this account
8. OAuth Login
Purpose: Authenticates a user via Google or Apple. Only available if the user has previously linked the provider to their account. The client sends the idToken directly — no server-side code exchange.
Endpoint: POST {base_url}/auth/login/oauth
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"provider": "GOOGLE",
"idToken": "google-id-token-from-client-sdk",
"deviceId": "android-uuid-abc123",
"deviceName": "Josh's Pixel 4a",
"platform": "ANDROID",
"state": "optional-state-string"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkToken |
string | Yes | Token from /auth/check |
Non-empty |
provider |
string | Yes | OAuth provider | enum: GOOGLE, APPLE |
idToken |
string | Yes | ID token from Google/Apple client SDK | Non-empty |
deviceId |
string | Yes | Device identifier | Non-empty |
deviceName |
string | No | Human-readable device name | Optional |
platform |
string | No | Client platform | enum: ANDROID, IOS, WEB |
state |
string | No | Opaque state value passed back in response | Optional |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Login successful",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"onboarding": {
"primaryComplete": true,
"username": true,
"email": true,
"profilePic": false,
"interests": true,
"bio": false
},
"state": "optional-state-string"
}
}
Frontend handling:
Before calling this endpoint:
→ check authMethods.google from /auth/check response
→ ONLY show Google button if google = true
→ if google = false — do NOT show Google button at all
→ this prevents users from hitting the OAUTH_NOT_LINKED error
On success:
→ store accessToken in memory
→ store refreshToken securely
→ navigate to home
Getting the idToken on Android (Google Sign-In SDK):
val account = GoogleSignIn.getSignedInAccountFromIntent(data).result
val idToken = account.idToken
→ send idToken to this endpoint
Getting the idToken on iOS (Google Sign-In SDK):
let idToken = user.authentication.idToken
→ send idToken to this endpoint
Standard Error Types:
403 FORBIDDEN— Google/Apple not linked to this account (OAUTH_NOT_LINKED)403 FORBIDDEN— idToken invalid or expired403 FORBIDDEN— idToken email does not match linked provider email403 FORBIDDEN— checkToken invalid or expired
9. Forgot Password — Initiate
Purpose: Starts the forgot password flow. Sends an OTP to the user's phone. Requires a valid checkToken but does NOT consume it.
Endpoint: POST {base_url}/auth/password/forgot/initiate
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"checkToken": "eyJhbGciOiJIUzI1NiJ9...",
"deviceId": "android-uuid-abc123"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
checkToken |
string | Yes | Token from /auth/check |
Non-empty |
deviceId |
string | Yes | Must match deviceId from check | Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Password reset code sent to your phone",
"action_time": "2026-04-03T10:30:45",
"data": {
"tempToken": "eyJhbGciOiJIUzI1NiJ9...",
"resetToken": null,
"maskedPhone": "••• ••• ••50",
"accessToken": null,
"expiresInSeconds": 120
}
}
Frontend handling:
→ store tempToken in memory
→ show OTP input screen
→ display maskedPhone so user knows where OTP was sent
→ proceed to /auth/password/forgot/verify-otp
Standard Error Types:
403 FORBIDDEN— checkToken invalid or expired403 FORBIDDEN— account has no password set — nothing to reset404 NOT_FOUND— account not found
10. Forgot Password — Verify OTP
Purpose: Verifies the OTP from the forgot password flow and issues a short-lived resetToken.
Endpoint: POST {base_url}/auth/password/forgot/verify-otp
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"tempToken": "eyJhbGciOiJIUzI1NiJ9...",
"otp": "482910"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
tempToken |
string | Yes | Token from forgot password initiate | Non-empty |
otp |
string | Yes | 6-digit code from SMS | Exactly 6 numeric digits |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Identity confirmed. Set your new password.",
"action_time": "2026-04-03T10:30:45",
"data": {
"tempToken": null,
"resetToken": "eyJhbGciOiJIUzI1NiJ9...",
"maskedPhone": null,
"accessToken": null,
"expiresInSeconds": 0
}
}
Frontend handling:
→ discard tempToken from memory
→ store resetToken in memory
→ navigate to new password input screen
→ proceed to /auth/password/forgot/reset
Standard Error Types:
403 FORBIDDEN— wrong OTP403 FORBIDDEN— OTP expired403 FORBIDDEN— max OTP attempts exceeded
11. Forgot Password — Reset
Purpose: Sets the new password. Revokes all existing sessions and issues a fresh accessToken.
Endpoint: POST {base_url}/auth/password/forgot/reset
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"resetToken": "eyJhbGciOiJIUzI1NiJ9...",
"newPassword": "MyNewSecurePassword456",
"confirmPassword": "MyNewSecurePassword456"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
resetToken |
string | Yes | Token from verify OTP step | Non-empty |
newPassword |
string | Yes | New password | Min 8 characters |
confirmPassword |
string | Yes | Must match newPassword | Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Password updated. All other sessions signed out.",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"resetToken": null,
"maskedPhone": null,
"expiresInSeconds": 0
}
}
Frontend handling:
→ discard resetToken from memory
→ store accessToken in memory
→ clear any existing refreshToken from storage
(all sessions were revoked server-side)
→ navigate to home
→ show "Password updated successfully" confirmation
Standard Error Types:
400 BAD_REQUEST— passwords do not match403 FORBIDDEN— resetToken invalid or expired422 UNPROCESSABLE_ENTITY— password too short
12. Refresh Token
Purpose: Exchanges a refresh token for a new access token + refresh token pair. Implements token rotation — the old refresh token is invalidated immediately.
Endpoint: POST {base_url}/auth/token/refresh
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
refreshToken |
string | Yes | Current refresh token | Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Token refreshed",
"action_time": "2026-04-03T10:30:45",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 3600
}
}
Frontend handling:
Call this endpoint when:
→ accessToken is expired (you get 401 on any protected request)
→ proactively before expiry (check exp claim in JWT)
On success:
→ replace accessToken in memory with new one
→ replace refreshToken in secure storage with new one
→ retry the original failed request
On 401 — refresh token invalid/revoked:
→ clear all tokens from memory and storage
→ redirect to login screen
→ this is a forced logout
Token rotation means:
→ old refreshToken is immediately invalid after this call
→ do NOT reuse the old refreshToken
→ if two refresh calls happen simultaneously (race condition)
only one will succeed — the other will get 401
Standard Error Types:
13. Revoke Token
Purpose: Logs out the user by revoking their refresh token. All requests with the old accessToken will still work until it expires naturally (max 1 hour).
Endpoint: POST {base_url}/auth/token/revoke
Access Level: 🌐 Public
Authentication: None
Request JSON Sample:
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
refreshToken |
string | Yes | The refresh token to revoke | Non-empty |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Token revoked successfully",
"action_time": "2026-04-03T10:30:45",
"data": null
}
Frontend handling:
On logout:
→ call this endpoint with the stored refreshToken
→ clear accessToken from memory
→ clear refreshToken from secure storage
→ redirect to login screen
→ do NOT wait for 401 — revoke proactively
If call fails (network error):
→ still clear tokens locally
→ the refreshToken will expire naturally after 30 days
→ user is effectively logged out on the client
Quick Reference — Full Auth Flow
1. POST /auth/check
→ phone + deviceId → checkToken + action
2. POST /auth/passwordless/channels (use checkToken)
→ available channels
3. POST /auth/passwordless-start (consumes checkToken)
→ channel → tempToken + OTP sent
4. POST /auth/otp-verify (consumes tempToken)
→ otp → accessToken (returning) or onboardingToken (new)
5. POST /auth/onboarding/primary (if onboardingToken received)
→ name + birthDate → accessToken issued
─── User is now logged in ───
6. Secondary onboarding (optional, progressive)
→ username, email, interests, bio, profile pic
→ each returns new accessToken with updated flags
─── Token management ───
7. POST /auth/token/refresh → rotate tokens silently
8. POST /auth/token/revoke → logout
Error Handling Summary
| HTTP Status | When it happens | What to do |
|---|---|---|
400 BAD_REQUEST |
Invalid input, item exists, rate limit | Show error message to user |
401 UNAUTHORIZED |
Token expired or invalid | Refresh token or redirect to login |
403 FORBIDDEN |
Wrong OTP, wrong password, token mismatch | Show specific error, let user retry |
404 NOT_FOUND |
Account not found | Show "Account not found" |
422 UNPROCESSABLE_ENTITY |
Validation failed | Show field-level errors |
500 INTERNAL_SERVER_ERROR |
Server error | Show generic error, retry |

No comments to display
No comments to display