Authenticate User
curl --request POST \
--url https://api.example.com/v1/auth/login \
--header 'Content-Type: application/json' \
--header 'x-client-key: <x-client-key>' \
--data '
{
"email": "<string>",
"password": "<string>",
"otpCode": "<string>"
}
'import requests
url = "https://api.example.com/v1/auth/login"
payload = {
"email": "<string>",
"password": "<string>",
"otpCode": "<string>"
}
headers = {
"x-client-key": "<x-client-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-client-key': '<x-client-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({email: '<string>', password: '<string>', otpCode: '<string>'})
};
fetch('https://api.example.com/v1/auth/login', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/auth/login",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => '<string>',
'password' => '<string>',
'otpCode' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-client-key: <x-client-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/auth/login"
payload := strings.NewReader("{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-client-key", "<x-client-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/auth/login")
.header("x-client-key", "<x-client-key>")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/auth/login")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-client-key"] = '<x-client-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"accessToken": "<string>",
"userId": "<string>",
"isOtpRequired": true,
"phoneNumber": "<string>",
"phase": "<string>",
"verificationState": "<string>",
"isLinked": true
}Authentication
Authenticate User
Authenticate user with email and password to obtain an access token
POST
/
v1
/
auth
/
login
Authenticate User
curl --request POST \
--url https://api.example.com/v1/auth/login \
--header 'Content-Type: application/json' \
--header 'x-client-key: <x-client-key>' \
--data '
{
"email": "<string>",
"password": "<string>",
"otpCode": "<string>"
}
'import requests
url = "https://api.example.com/v1/auth/login"
payload = {
"email": "<string>",
"password": "<string>",
"otpCode": "<string>"
}
headers = {
"x-client-key": "<x-client-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-client-key': '<x-client-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({email: '<string>', password: '<string>', otpCode: '<string>'})
};
fetch('https://api.example.com/v1/auth/login', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/auth/login",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'email' => '<string>',
'password' => '<string>',
'otpCode' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-client-key: <x-client-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/auth/login"
payload := strings.NewReader("{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-client-key", "<x-client-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/auth/login")
.header("x-client-key", "<x-client-key>")
.header("Content-Type", "application/json")
.body("{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/auth/login")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-client-key"] = '<x-client-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"email\": \"<string>\",\n \"password\": \"<string>\",\n \"otpCode\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"accessToken": "<string>",
"userId": "<string>",
"isOtpRequired": true,
"phoneNumber": "<string>",
"phase": "<string>",
"verificationState": "<string>",
"isLinked": true
}Overview
Authenticate a user with their credentials to obtain an access token. This endpoint is used in two contexts:- OAuth Step 2 (API Mode): When using
mode=apiin OAuth flow, this step authenticates the user to get an access token needed for Step 3 - Direct Login: For applications not using OAuth, this provides immediate access token for API authentication
Authorization: Bearer header for authenticated API requests.
When to Use
- Implementing OAuth 2.0 flow in API-mode (Step 2 of 4)
- Direct user authentication without OAuth
- Need to verify user credentials and get access token
- User login for internal applications
This step is NOT needed when using the hosted UI OAuth flow (without
mode=api). The hosted UI handles authentication automatically.Request
Headers
string
required
Your public API client key
boolean
Set to
true to route requests to the US backend environmentDefault: false (international environment)Body Parameters
string
required
User’s email addressFormat: Valid emailExample:
user@example.comstring
required
User’s passwordFormat: Password stringExample:
SecurePassword123!string
One-time password code (required only if user has OTP enabled)Call
POST /v1/auth/login/otp first to send the OTP code to the user’s phoneLength: 6 digitsExample: 123456Response
string
required
Access token for API authenticationUse this in
Authorization: Bearer header for authenticated endpointsExpiry: 6 hoursExample: US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9string
User’s unique identifierFormat: UUIDExample:
b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9boolean
Indicates if user requires OTP verificationIf
true, you must:- Call
POST /v1/auth/login/otpto send OTP code - Retry this endpoint with
otpCodeparameter
falsestring
Masked phone number (only returned if
isOtpRequired is true)Example: +445*****225string
User onboarding phase (only returned during onboarding)Possible values:
ACCOUNTPHONE_NUMBERPERSONAL_INFORMATIONPHYSICAL_ADDRESSMAILING_ADDRESS
nullstring
User’s KYC verification statusPossible values:
UNVERIFIED- No verification submittedPENDING- Verification in progressVERIFIED- Successfully verifiedREJECTED- Verification failed
VERIFIEDboolean
Indicates if client has valid permission to access user’s accountIn OAuth context,
false means authorization hasn’t been granted yetExample: falseSuccess Response
{
"accessToken": "US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9",
"userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
"isOtpRequired": false,
"phase": null,
"verificationState": "VERIFIED",
"isLinked": false
}
Error Responses
400 Bad Request - Invalid Credentials
400 Bad Request - Invalid Credentials
{
"message": "Invalid email or password"
}
- Incorrect email or password
- Account doesn’t exist
- Account is locked or disabled
400 Bad Request - OTP Required
400 Bad Request - OTP Required
{
"message": "OTP verification required",
"isOtpRequired": true
}
POST /v1/auth/login/otp first, then retry with otpCode422 Validation Error
422 Validation Error
{
"message": "email must be a valid email"
}
- Invalid email format
- Missing required parameters
- Invalid parameter types
Code Examples
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePassword123!"
}'
const response = await fetch('https://dev.api.baanx.com/v1/auth/login', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'user@example.com',
password: 'SecurePassword123!'
})
});
const data = await response.json();
if (data.isOtpRequired) {
console.log('OTP required. Send to:', data.phoneNumber);
// Call /v1/auth/login/otp and retry with otpCode
} else {
console.log('Access token:', data.accessToken);
// Store token for API requests
localStorage.setItem('access_token', data.accessToken);
}
import requests
response = requests.post(
'https://dev.api.baanx.com/v1/auth/login',
headers={
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
json={
'email': 'user@example.com',
'password': 'SecurePassword123!'
}
)
data = response.json()
if response.status_code == 200:
if data.get('isOtpRequired'):
print(f"OTP required. Send to: {data.get('phoneNumber')}")
# Call /v1/auth/login/otp and retry with otpCode
else:
print(f"Access token: {data['accessToken']}")
# Store token for API requests
session['access_token'] = data['accessToken']
else:
print(f"Login failed: {data.get('message')}")
interface LoginRequest {
email: string;
password: string;
otpCode?: string;
}
interface LoginResponse {
accessToken: string;
userId: string;
isOtpRequired: boolean;
phoneNumber?: string;
phase?: string | null;
verificationState?: string;
isLinked: boolean;
}
async function login(credentials: LoginRequest): Promise<LoginResponse> {
const response = await fetch('https://dev.api.baanx.com/v1/auth/login', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const data: LoginResponse = await response.json();
if (data.isOtpRequired && !credentials.otpCode) {
throw new Error('OTP_REQUIRED');
}
return data;
}
OTP Flow Example
If the user has OTP enabled, you need to handle a two-step login process:async function loginWithOTP(email, password) {
// Step 1: Initial login attempt
let response = await fetch('https://dev.api.baanx.com/v1/auth/login', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
let data = await response.json();
// Step 2: If OTP required, send OTP and retry
if (data.isOtpRequired) {
const userId = data.userId;
// Send OTP code to user's phone
await fetch('https://dev.api.baanx.com/v1/auth/login/otp', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
});
// Get OTP code from user (show input form)
const otpCode = await promptUserForOTP();
// Retry login with OTP code
response = await fetch('https://dev.api.baanx.com/v1/auth/login', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password, otpCode })
});
data = await response.json();
}
return data;
}
import requests
def login_with_otp(email: str, password: str) -> dict:
base_url = 'https://dev.api.baanx.com'
headers = {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
}
# Step 1: Initial login attempt
response = requests.post(
f'{base_url}/v1/auth/login',
headers=headers,
json={'email': email, 'password': password}
)
data = response.json()
# Step 2: If OTP required, send OTP and retry
if data.get('isOtpRequired'):
user_id = data['userId']
# Send OTP code to user's phone
requests.post(
f'{base_url}/v1/auth/login/otp',
headers=headers,
json={'userId': user_id}
)
# Get OTP code from user
otp_code = input(f"Enter OTP sent to {data.get('phoneNumber')}: ")
# Retry login with OTP code
response = requests.post(
f'{base_url}/v1/auth/login',
headers=headers,
json={'email': email, 'password': password, 'otpCode': otp_code}
)
data = response.json()
return data
OAuth Context: Step 2
When using this endpoint as Step 2 in OAuth API-mode flow:OAuth Step 2
// After Step 1: Initiate OAuth
const { token: jwtToken } = await initiateOAuth(); // Step 1
// Step 2: Authenticate user
const loginResponse = await fetch('https://dev.api.baanx.com/v1/auth/login', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'user@example.com',
password: 'SecurePassword123!'
})
});
const { accessToken } = await loginResponse.json();
// Step 3: Use accessToken to generate authorization code
const authResponse = await fetch('https://dev.api.baanx.com/v1/auth/oauth/authorize', {
method: 'POST',
headers: {
'x-client-key': 'your-client-key',
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: jwtToken })
});
const { code } = await authResponse.json();
// Continue to Step 4: Token exchange
Edge Cases and Important Notes
Token Expiry: Access tokens expire after 6 hours. In OAuth flow, you should exchange them for long-lived tokens in Step 4. For direct login, implement token refresh logic.
Onboarding Phase: If
phase is not null, the user hasn’t completed registration. Guide them through the remaining onboarding steps before allowing full access.Account Security: Implement rate limiting on your client side to prevent brute force attacks. The API has built-in protection, but client-side throttling improves UX.
Failed Login Attempts
After multiple failed login attempts, accounts may be temporarily locked:async function loginWithRetry(email, password, maxAttempts = 3) {
let attempts = 0;
while (attempts < maxAttempts) {
try {
return await login(email, password);
} catch (error) {
attempts++;
if (error.message === 'Account temporarily locked') {
throw new Error('Too many failed attempts. Please try again later.');
}
if (attempts >= maxAttempts) {
throw new Error('Maximum login attempts exceeded');
}
}
}
}
Token Storage Security
Always store access tokens securely:- Web apps: Use
httpOnlycookies or secure sessionStorage - Mobile apps: Use secure device storage (Keychain/Keystore)
- Never: Store in localStorage or expose in URLs
Response Scenarios
Understanding different response scenarios helps you handle all authentication states correctly and build robust integrations.Response Field Decision Matrix
Use this table to determine what action to take based on the response field values:| accessToken | isOtpRequired | phase | Action Required |
|---|---|---|---|
| non-null | false | null | Success: Store token, proceed with API calls |
| null | true | null | OTP Flow: Call /v1/auth/login/otp, collect code, retry with otpCode |
| null | false | non-null | Onboarding: Direct user to complete registration at specified phase |
| non-null | false | non-null | Partial Onboarding: Token valid but registration incomplete |
For complete flow explanations with examples, see the Authentication Guide.
HTTP 200: Successful Login (No OTP)
User authenticated successfully with no additional steps required. Response:{
"accessToken": "US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9",
"userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
"isOtpRequired": false,
"phoneNumber": null,
"phase": null,
"verificationState": "VERIFIED",
"isLinked": false
}
- Store
accessTokensecurely (sessionStorage, secure storage) - Track token issuance time for expiration handling
- Use token in
Authorization: Bearerheader for all API calls - If
isLinked: falseand long-lived access needed, initiate OAuth flow
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"SecurePassword123!"}'
HTTP 200: OTP Required
User has 2FA enabled. Initial authentication succeeded but OTP verification required. Response:{
"accessToken": null,
"userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
"isOtpRequired": true,
"phoneNumber": "+445*****225",
"phase": null,
"verificationState": "VERIFIED",
"isLinked": false
}
accessToken is null:
The token won’t be issued until OTP verification completes. This prevents unauthorized access even if credentials are compromised.
Client Action:
- Call
POST /v1/auth/login/otpwithuserIdto send OTP - Display OTP input field to user
- Show masked
phoneNumberso user knows where to check - Retry
POST /v1/auth/loginwith credentials +otpCodeparameter - Handle OTP-specific errors (invalid code, expired code, rate limit)
# Step 1: Send OTP
curl -X POST "https://dev.api.baanx.com/v1/auth/login/otp" \
-H "x-client-key: your-client-key" \
-H "Content-Type: application/json" \
-d '{"userId":"b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9"}'
# Step 2: User receives SMS, enters code "123456"
# Step 3: Complete login with OTP
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-H "Content-Type: application/json" \
-d '{
"email":"user@example.com",
"password":"SecurePassword123!",
"otpCode":"123456"
}'
See Send OTP for detailed OTP endpoint documentation.
HTTP 200: User Onboarding Incomplete
User account exists but hasn’t completed registration. Response:{
"accessToken": null,
"userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
"isOtpRequired": false,
"phoneNumber": null,
"phase": "PHONE_NUMBER",
"verificationState": null,
"isLinked": false
}
ACCOUNT: Basic account creation (email/password set)PHONE_NUMBER: Phone verification requiredPERSONAL_INFORMATION: Name, DOB, SSN collectionPHYSICAL_ADDRESS: Residential address informationMAILING_ADDRESS: Mailing address (if different from physical)
- Direct user to continue registration at the specified phase
- Do NOT attempt API calls -
accessTokenis null - Guide user through onboarding steps
- After completion,
phasewill benullandaccessTokenwill be provided
API endpoints requiring authentication will fail with 401 until onboarding completes and a valid
accessToken is issued.HTTP 401: Invalid Credentials
Incorrect email/password combination or account doesn’t exist. Response:{
"message": "Invalid email or password"
}
- Typo in email or password
- User hasn’t registered yet
- Password changed but old credentials used
- Testing with wrong environment credentials
- Display generic error message to user (don’t specify which field is wrong)
- Implement rate limiting on client side (exponential backoff)
- Offer “Forgot Password” option after 2-3 failed attempts
- After 5 failed attempts, suggest account recovery
// Don't reveal which field is incorrect
if (error.status === 401) {
showError('Invalid email or password');
// NOT: 'Email not found' or 'Wrong password'
}
HTTP 403: Account Locked
Account temporarily locked due to security concerns. Response:{
"message": "Account is temporarily locked. Please try again later or contact support."
}
- Multiple failed login attempts (brute force protection)
- Suspicious activity detected
- Manual lock by admin/support
- Security policy violation
- Display error message with support contact information
- Do NOT retry immediately - will extend lock duration
- Implement exponential backoff (start with 1 hour delay)
- Provide link to account recovery/support
HTTP 422: Validation Error
Request parameters failed validation. Response:{
"message": "email must be a valid email",
"field": "email"
}
email must be a valid email: Invalid email formatpassword is required: Missing password fieldotpCode must be 6 digits: Invalid OTP code format
- Validate input on client side before submission
- Display field-specific error messages
- Highlight invalid fields in UI
- Prevent submission until validation passes
function validateLoginForm(email: string, password: string): string | null {
if (!email || !/\S+@\S+\.\S+/.test(email)) {
return 'Please enter a valid email address';
}
if (!password || password.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
}
Edge Cases
Multiple Failed OTP Attempts
After 3-5 failed OTP verification attempts, the account may be temporarily locked. Response:{
"message": "Too many failed OTP attempts. Please try again later.",
"retryAfter": 1800
}
if (error.message.includes('Too many failed OTP attempts')) {
const waitMinutes = Math.ceil(error.retryAfter / 60);
showError(`Please wait ${waitMinutes} minutes before trying again`);
// Disable OTP input and show countdown timer
startCountdown(error.retryAfter);
}
Expired OTP Code
OTP codes typically expire after 5-10 minutes. Response:{
"message": "OTP code has expired",
"isOtpRequired": true
}
if (error.message.includes('expired')) {
// Automatically request new OTP
await sendNewOtp(userId);
showMessage('Code expired. New code sent to your phone.');
}
Environment Mismatch
Using production credentials in sandbox or vice versa. Symptoms:- 401 errors despite correct credentials
- User not found errors
- Token format mismatch errors
// Ensure environment consistency
const environment = 'production'; // or 'sandbox'
const headers = {
'x-client-key': environment === 'production'
? PROD_CLIENT_KEY
: SANDBOX_CLIENT_KEY,
'x-us-env': environment === 'production' ? 'true' : 'false'
};
const apiBase = 'https://dev.api.baanx.com';
Token Already Exists
User attempting to login while already having a valid session. Behavior:- New token is issued
- Previous token remains valid until expiration
- No error returned
async function login(email: string, password: string) {
const existingToken = getStoredToken();
if (existingToken && isTokenValid(existingToken)) {
// Ask user if they want to create new session
const createNew = await confirmNewSession();
if (!createNew) return existingToken;
}
const newToken = await performLogin(email, password);
storeToken(newToken);
return newToken;
}
Network Timeout During Authentication
Request timeout or network interruption. Handling:async function loginWithRetry(
email: string,
password: string,
maxRetries = 3
): Promise<string> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
const response = await fetch('/v1/auth/login', {
method: 'POST',
signal: controller.signal,
headers: { 'x-client-key': CLIENT_KEY },
body: JSON.stringify({ email, password })
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data.accessToken;
} catch (error) {
lastError = error;
if (error.name === 'AbortError') {
console.log(`Attempt ${attempt} timed out`);
}
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Login failed after ${maxRetries} attempts: ${lastError.message}`);
}
Race Condition: Multiple Simultaneous Login Requests
If multiple login requests are made simultaneously (e.g., from different tabs). Behavior:- Each request returns a valid but different access token
- Last token stored wins
- Previous tokens remain valid until expiration
let loginPromise: Promise<string> | null = null;
async function login(email: string, password: string): Promise<string> {
// If login already in progress, return existing promise
if (loginPromise) {
return loginPromise;
}
loginPromise = performLogin(email, password)
.finally(() => {
loginPromise = null;
});
return loginPromise;
}
Concurrent Onboarding and Login
User attempting login while still in registration flow. Scenario:{
"accessToken": "US_temp123",
"phase": "PERSONAL_INFORMATION",
"isOtpRequired": false
}
accessToken and phase:
if (response.phase !== null) {
// Redirect to onboarding, even if accessToken exists
navigateToOnboarding(response.phase);
} else if (response.accessToken) {
// Full access granted
navigateToApp();
}
Complete Integration Example
Full production-ready login implementation with comprehensive error handling.import { z } from 'zod';
const LoginResponseSchema = z.object({
accessToken: z.string().nullable(),
userId: z.string().uuid(),
isOtpRequired: z.boolean(),
phoneNumber: z.string().nullable(),
phase: z.string().nullable(),
verificationState: z.string().nullable(),
isLinked: z.boolean()
});
type LoginResponse = z.infer<typeof LoginResponseSchema>;
class AuthenticationError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number,
public retryable: boolean = false
) {
super(message);
this.name = 'AuthenticationError';
}
}
class LoginService {
private readonly apiBase: string;
private readonly clientKey: string;
private readonly maxRetries = 3;
private readonly timeoutMs = 30000;
constructor(apiBase: string, clientKey: string) {
this.apiBase = apiBase;
this.clientKey = clientKey;
}
async login(
email: string,
password: string,
otpCode?: string
): Promise<LoginResponse> {
const body = { email, password, ...(otpCode && { otpCode }) };
try {
const response = await this.fetchWithTimeout(
`${this.apiBase}/v1/auth/login`,
{
method: 'POST',
headers: {
'x-client-key': this.clientKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}
);
const data = await response.json();
if (!response.ok) {
throw this.handleErrorResponse(response.status, data);
}
return LoginResponseSchema.parse(data);
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
'Network error during login',
'NETWORK_ERROR',
0,
true
);
}
}
async sendOtp(userId: string): Promise<void> {
const response = await this.fetchWithTimeout(
`${this.apiBase}/v1/auth/login/otp`,
{
method: 'POST',
headers: {
'x-client-key': this.clientKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
}
);
if (!response.ok) {
const data = await response.json();
throw this.handleErrorResponse(response.status, data);
}
}
async loginWithOtpFlow(
email: string,
password: string,
otpCallback: () => Promise<string>
): Promise<LoginResponse> {
const initialResponse = await this.login(email, password);
if (!initialResponse.isOtpRequired) {
return initialResponse;
}
await this.sendOtp(initialResponse.userId);
const otpCode = await otpCallback();
return await this.login(email, password, otpCode);
}
private async fetchWithTimeout(
url: string,
options: RequestInit
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
private handleErrorResponse(
status: number,
data: any
): AuthenticationError {
switch (status) {
case 401:
return new AuthenticationError(
'Invalid credentials',
'INVALID_CREDENTIALS',
401,
false
);
case 403:
return new AuthenticationError(
data.message || 'Account locked',
'ACCOUNT_LOCKED',
403,
false
);
case 422:
return new AuthenticationError(
data.message || 'Validation error',
'VALIDATION_ERROR',
422,
false
);
case 429:
return new AuthenticationError(
'Too many attempts',
'RATE_LIMITED',
429,
true
);
default:
return new AuthenticationError(
data.message || 'Authentication failed',
'UNKNOWN_ERROR',
status,
status >= 500
);
}
}
}
// Usage Example
const loginService = new LoginService(
'https://dev.api.baanx.com',
'your-client-key'
);
async function handleLogin(email: string, password: string) {
try {
const response = await loginService.loginWithOtpFlow(
email,
password,
async () => {
return await promptUserForOtp();
}
);
if (response.phase !== null) {
console.log('User needs to complete onboarding:', response.phase);
redirectToOnboarding(response.phase);
return;
}
if (!response.accessToken) {
throw new Error('No access token received');
}
console.log('Login successful');
storeToken(response.accessToken);
redirectToApp();
} catch (error) {
if (error instanceof AuthenticationError) {
handleAuthError(error);
} else {
console.error('Unexpected error:', error);
}
}
}
function handleAuthError(error: AuthenticationError) {
switch (error.code) {
case 'INVALID_CREDENTIALS':
showError('Invalid email or password');
offerPasswordReset();
break;
case 'ACCOUNT_LOCKED':
showError('Account is locked. Please contact support.');
showSupportLink();
break;
case 'RATE_LIMITED':
showError('Too many attempts. Please wait and try again.');
break;
case 'NETWORK_ERROR':
if (error.retryable) {
showError('Connection issue. Retrying...');
retryLogin();
}
break;
default:
showError('Login failed. Please try again.');
}
}
function promptUserForOtp(): Promise<string> {
return new Promise((resolve) => {
// Show OTP input modal
const modal = showOtpModal();
modal.onSubmit((code: string) => resolve(code));
});
}
function storeToken(token: string) {
sessionStorage.setItem('accessToken', token);
sessionStorage.setItem('tokenIssuedAt', Date.now().toString());
}
function redirectToOnboarding(phase: string) {
window.location.href = `/onboarding/${phase.toLowerCase()}`;
}
function redirectToApp() {
window.location.href = '/dashboard';
}
function showError(message: string) {
console.error(message);
}
function offerPasswordReset() {
console.log('Showing password reset link');
}
function showSupportLink() {
console.log('Showing support contact information');
}
function retryLogin() {
console.log('Retrying login...');
}
function showOtpModal() {
return {
onSubmit: (callback: (code: string) => void) => {
setTimeout(() => callback('123456'), 1000);
}
};
}
import time
import requests
from typing import Optional, Callable, TypedDict
from enum import Enum
from dataclasses import dataclass
class LoginPhase(str, Enum):
ACCOUNT = "ACCOUNT"
PHONE_NUMBER = "PHONE_NUMBER"
PERSONAL_INFORMATION = "PERSONAL_INFORMATION"
PHYSICAL_ADDRESS = "PHYSICAL_ADDRESS"
MAILING_ADDRESS = "MAILING_ADDRESS"
class VerificationState(str, Enum):
UNVERIFIED = "UNVERIFIED"
PENDING = "PENDING"
VERIFIED = "VERIFIED"
REJECTED = "REJECTED"
class LoginResponse(TypedDict, total=False):
accessToken: Optional[str]
userId: str
isOtpRequired: bool
phoneNumber: Optional[str]
phase: Optional[str]
verificationState: Optional[str]
isLinked: bool
@dataclass
class AuthenticationError(Exception):
message: str
code: str
status_code: int
retryable: bool = False
class LoginService:
TOKEN_LIFETIME_SECONDS = 6 * 60 * 60
def __init__(self, api_base: str, client_key: str):
self.api_base = api_base
self.client_key = client_key
self.session = requests.Session()
self.session.headers.update({
'x-client-key': client_key,
'Content-Type': 'application/json'
})
def login(
self,
email: str,
password: str,
otp_code: Optional[str] = None
) -> LoginResponse:
body = {'email': email, 'password': password}
if otp_code:
body['otpCode'] = otp_code
try:
response = self.session.post(
f'{self.api_base}/v1/auth/login',
json=body,
timeout=30
)
data = response.json()
if not response.ok:
raise self._handle_error_response(response.status_code, data)
return data
except requests.exceptions.Timeout:
raise AuthenticationError(
'Request timed out',
'TIMEOUT',
0,
retryable=True
)
except requests.exceptions.ConnectionError:
raise AuthenticationError(
'Connection failed',
'CONNECTION_ERROR',
0,
retryable=True
)
def send_otp(self, user_id: str) -> None:
response = self.session.post(
f'{self.api_base}/v1/auth/login/otp',
json={'userId': user_id},
timeout=30
)
if not response.ok:
data = response.json()
raise self._handle_error_response(response.status_code, data)
def login_with_otp_flow(
self,
email: str,
password: str,
otp_callback: Callable[[], str]
) -> LoginResponse:
initial_response = self.login(email, password)
if not initial_response.get('isOtpRequired'):
return initial_response
self.send_otp(initial_response['userId'])
print(f"OTP sent to {initial_response.get('phoneNumber')}")
otp_code = otp_callback()
return self.login(email, password, otp_code)
def _handle_error_response(
self,
status_code: int,
data: dict
) -> AuthenticationError:
message = data.get('message', 'Authentication failed')
error_map = {
401: ('INVALID_CREDENTIALS', False),
403: ('ACCOUNT_LOCKED', False),
422: ('VALIDATION_ERROR', False),
429: ('RATE_LIMITED', True),
}
code, retryable = error_map.get(
status_code,
('UNKNOWN_ERROR', status_code >= 500)
)
return AuthenticationError(message, code, status_code, retryable)
# Usage Example
def main():
login_service = LoginService(
'https://dev.api.baanx.com',
'your-client-key'
)
try:
response = login_service.login_with_otp_flow(
'user@example.com',
'SecurePassword123!',
otp_callback=lambda: input('Enter OTP code: ')
)
# Check onboarding status
if response.get('phase'):
print(f"Complete onboarding phase: {response['phase']}")
return
# Check token
if not response.get('accessToken'):
raise Exception('No access token received')
print('Login successful!')
store_token(response['accessToken'])
except AuthenticationError as error:
handle_auth_error(error)
except Exception as error:
print(f'Unexpected error: {error}')
def handle_auth_error(error: AuthenticationError):
if error.code == 'INVALID_CREDENTIALS':
print('Invalid email or password')
offer_password_reset()
elif error.code == 'ACCOUNT_LOCKED':
print('Account is locked. Please contact support.')
show_support_link()
elif error.code == 'RATE_LIMITED':
print('Too many attempts. Please wait and try again.')
elif error.code in ('TIMEOUT', 'CONNECTION_ERROR') and error.retryable:
print('Connection issue. Please try again.')
else:
print(f'Login failed: {error.message}')
def store_token(token: str):
print(f'Storing token: {token[:20]}...')
def offer_password_reset():
print('Password reset link offered')
def show_support_link():
print('Support contact: support@example.com')
if __name__ == '__main__':
main()
Testing Different Scenarios
Use these test cases to verify your integration handles all scenarios correctly:Test Case 1: Successful Login
Test Case 1: Successful Login
Setup:
- Valid email and password
- No OTP enabled
- Onboarding complete
- HTTP 200
- Valid
accessTokenreturned isOtpRequired: falsephase: null
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"ValidPass123!"}'
# Should return accessToken immediately
Test Case 2: OTP Flow
Test Case 2: OTP Flow
Setup:
- Valid email and password
- OTP enabled on account
- HTTP 200 on initial login with
isOtpRequired: true - HTTP 200 on OTP send
- HTTP 200 on final login with valid
accessToken
# Step 1: Initial login
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-d '{"email":"otp-user@example.com","password":"ValidPass123!"}'
# Returns: isOtpRequired: true, userId, phoneNumber
# Step 2: Send OTP
curl -X POST "https://dev.api.baanx.com/v1/auth/login/otp" \
-H "x-client-key: your-client-key" \
-d '{"userId":"<userId from step 1>"}'
# Step 3: Complete with OTP
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-d '{
"email":"otp-user@example.com",
"password":"ValidPass123!",
"otpCode":"123456"
}'
# Returns: accessToken
Test Case 3: Invalid Credentials
Test Case 3: Invalid Credentials
Setup:
- Invalid email or password
- HTTP 401
- Error message: “Invalid email or password”
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-d '{"email":"wrong@example.com","password":"WrongPass123!"}'
# Should return 401 with error message
Test Case 4: User Onboarding
Test Case 4: User Onboarding
Setup:
- Valid credentials
- User has not completed onboarding
- HTTP 200
accessToken: nullphase: "PHONE_NUMBER"(or other phase)
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-d '{"email":"incomplete@example.com","password":"ValidPass123!"}'
# Should return phase indicating incomplete onboarding
Test Case 5: Account Locked
Test Case 5: Account Locked
Setup:
- Account locked due to failed attempts
- HTTP 403
- Error message about account lock
# Simulate by making multiple failed attempts
for i in {1..6}; do
curl -X POST "https://dev.api.baanx.com/v1/auth/login" \
-H "x-client-key: your-client-key" \
-d '{"email":"test@example.com","password":"WrongPassword"}'
done
# Final attempt should return 403
Related Endpoints
Send OTP
Send OTP verification code via SMS for 2FA authentication
Logout
Invalidate access token and end user session
OAuth Authorize
Generate authorization code for OAuth 2.0 token exchange
Get User Profile
Retrieve authenticated user’s profile and verification status
For detailed authentication flows and patterns, see the Authentication Guide.
Was this page helpful?