Skip to main content

Overview

The authentication system provides secure user login with optional OTP (One-Time Password) for enhanced security. After successful authentication, users receive an access token valid for 6 hours that authorizes all authenticated API calls.
All authentication endpoints require the x-client-key header. For US environment routing, include x-us-env: true header or region=us query parameter.

Authentication Methods

The platform supports multiple authentication patterns:

Standard Login

Username/password authentication for direct API access

OTP-Enhanced Login

Multi-factor authentication with SMS verification codes

OAuth 2.0

Delegated authorization for third-party applications

Session Management

Token-based sessions with 6-hour expiration

Login Flow Diagram

Standard Login

Direct authentication using email and password credentials.

Endpoint

POST /v1/auth/login
API Reference: POST /v1/auth/login

Request

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": "[email protected]",
    "password": "SecurePassword123!"
  }'

Response (Standard Login)

When OTP is not required:
{
  "accessToken": "US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9",
  "userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
  "isOtpRequired": false,
  "phoneNumber": null,
  "phase": null,
  "verificationState": "VERIFIED",
  "isLinked": false
}

Response (OTP Required)

When OTP is required:
{
  "accessToken": "US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9",
  "userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
  "isOtpRequired": true,
  "phoneNumber": "+445*****225",
  "phase": null,
  "verificationState": "VERIFIED",
  "isLinked": false
}
When isOtpRequired: true, you must complete the OTP flow before the access token becomes valid for API calls. See the OTP Authentication section below.

Response Fields

Each field in the login response provides critical information about the user’s authentication state and onboarding progress:
FieldTypeDescription
accessTokenstring | nullBearer token for authenticated API calls (valid 6 hours). null when user is onboarding or OTP is required
userIdstringUnique user identifier in UUID format. Always returned
isOtpRequiredbooleanIndicates if 2FA is enabled. When true, call /v1/auth/login/otp to send OTP, then retry login with otpCode
phoneNumberstring | nullMasked phone number (e.g., “+445*****225”). Only returned when isOtpRequired is true
phasestring | nullUser’s onboarding progress. Non-null values (ACCOUNT, PHONE_NUMBER, PERSONAL_INFORMATION, PHYSICAL_ADDRESS, MAILING_ADDRESS) mean registration is incomplete. null when onboarding is complete
verificationStatestring | nullKYC verification status: UNVERIFIED (0), PENDING (1), VERIFIED (3), REJECTED (2)
isLinkedbooleanIndicates if your OAuth client already has permission to access this user’s account
phase (string | null)
  • Indicates where the user is in the onboarding process
  • If null: User has completed all registration steps
  • If non-null: User needs to complete registration at the indicated phase
    • ACCOUNT: Basic account creation step
    • PHONE_NUMBER: Phone verification step
    • PERSONAL_INFORMATION: Personal details collection
    • PHYSICAL_ADDRESS: Physical address information
    • MAILING_ADDRESS: Mailing address information
userId (string)
  • User’s unique identifier in UUID format
  • Consistent across all sessions and API calls
  • Use this to track user sessions and for OTP requests
isOtpRequired (boolean)
  • When true: User has 2FA enabled and must verify with OTP code
  • When false: Login is complete, use the accessToken for API calls
  • OTP flow: Call /v1/auth/login/otp → User receives SMS → Retry /login with otpCode
phoneNumber (string | null)
  • Only present when isOtpRequired is true
  • Masked for security (e.g., “+445*****225”)
  • Shows where the OTP code will be sent
  • Display this to users so they know which device to check
accessToken (string | null)
  • Bearer token for Authorization header: Authorization: Bearer {token}
  • Valid for 6 hours (21,600 seconds)
  • null in these scenarios:
    • User is still onboarding (phase is non-null)
    • OTP verification is required (isOtpRequired is true)
    • Initial login step before OTP completion
  • non-null when authentication is complete and user can access API
verificationState (string | null)
  • Maps to KYC verification status:
    • UNVERIFIED (0): User has not yet been verified
    • PENDING (1): Verification is in progress
    • VERIFIED (3): User is successfully verified
    • REJECTED (2): Verification was rejected
  • Check this before allowing access to features requiring verified users
isLinked (boolean)
  • Indicates OAuth connection status between your client and this user
  • false: Your OAuth client needs to complete OAuth flow for long-lived access
  • true: Your client already has OAuth tokens and can refresh them
  • Important for determining whether to initiate OAuth flow after login

Edge Cases and Response Scenarios

Understanding different response scenarios helps you handle all authentication states correctly:
When a user hasn’t completed registration, the response indicates their current phase:
{
  "phase": "PHONE_NUMBER",
  "userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
  "isOtpRequired": false,
  "phoneNumber": null,
  "accessToken": null,
  "verificationState": null,
  "isLinked": false
}
What this means:
  • User is stuck at the PHONE_NUMBER verification phase
  • No accessToken provided because onboarding is incomplete
  • verificationState is null because KYC hasn’t started yet
What to do:
  • Direct user to continue registration flow
  • Guide them to the phone verification step
  • Don’t attempt to use the API until phase is null

Using the Access Token

After successful login, use the access token in the Authorization header for all authenticated API calls:
curl -X GET https://dev.api.baanx.com/v1/user \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "Authorization: Bearer US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9"
Access tokens expire after 6 hours. Implement token refresh logic or require re-authentication when tokens expire.

Common Errors

{
  "message": "Invalid email or password"
}
Resolution: Verify credentials are correct. Implement password recovery if user has forgotten password.
{
  "message": "User not found"
}
Resolution: User may need to complete registration first.
{
  "message": "Account is temporarily locked"
}
Resolution: Account may be locked due to multiple failed login attempts. User should contact support or wait for automatic unlock.

OTP Authentication

For users with OTP enabled, an additional verification step is required after initial login.

OTP Flow

Step 1: Check OTP Requirement

After initial login, check the isOtpRequired field:
const loginResponse = await login(email, password);

if (loginResponse.isOtpRequired) {
  console.log('OTP required. Phone:', loginResponse.phoneNumber);
  await sendOtpCode(loginResponse.userId);
}

Step 2: Send OTP Code

Request an OTP code to be sent via SMS to the user’s registered phone number.

Endpoint

POST /v1/auth/login/otp
API Reference: POST /v1/auth/login/otp

Request

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"
  }'

Response

{
  "success": true
}
OTP codes are typically valid for 5-10 minutes. Users should enter the code promptly after receiving it.

Step 3: Submit OTP Code

Complete login by submitting the OTP code along with the original credentials.

Request

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": "[email protected]",
    "password": "SecurePassword123!",
    "otpCode": "123456"
  }'

Response

{
  "accessToken": "US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9",
  "userId": "b6b9168c-bb56-4c6a-9c0d-4650ea74f5f9",
  "isOtpRequired": false,
  "phoneNumber": null,
  "phase": null,
  "verificationState": "VERIFIED",
  "isLinked": false
}
After successful OTP verification, isOtpRequired will be false and the accessToken is fully valid for API calls.

Common OTP Errors

{
  "message": "Invalid OTP code",
  "isOtpRequired": true
}
Resolution: Ask user to check the code and try again, or request a new OTP code
{
  "message": "OTP code has expired",
  "isOtpRequired": true
}
Resolution: Request a new OTP code via POST /v1/auth/login/otp
{
  "message": "Too many failed OTP attempts. Please try again later."
}
Resolution: Wait for rate limit to reset (typically 15-30 minutes) or contact support

Complete OTP Login Example

Here’s a complete implementation of the OTP login flow:
class AuthService {
  private apiBase = 'https://dev.api.baanx.com';
  private clientKey = 'YOUR_CLIENT_KEY';

  async login(email: string, password: string): Promise<LoginResponse> {
    const response = await fetch(`${this.apiBase}/v1/auth/login`, {
      method: 'POST',
      headers: {
        'x-client-key': this.clientKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password })
    });

    if (!response.ok) {
      throw new Error(`Login failed: ${response.statusText}`);
    }

    return response.json();
  }

  async sendOtpCode(userId: string): Promise<void> {
    const response = await fetch(`${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) {
      throw new Error('Failed to send OTP code');
    }
  }

  async loginWithOtp(
    email: string,
    password: string,
    otpCode: string
  ): Promise<string> {
    const response = await fetch(`${this.apiBase}/v1/auth/login`, {
      method: 'POST',
      headers: {
        'x-client-key': this.clientKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password, otpCode })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'OTP login failed');
    }

    const data = await response.json();
    return data.accessToken;
  }

  async handleLogin(email: string, password: string): Promise<string> {
    const loginResult = await this.login(email, password);

    if (loginResult.isOtpRequired) {
      console.log('OTP required. Sending code to:', loginResult.phoneNumber);
      await this.sendOtpCode(loginResult.userId);

      const otpCode = await this.promptUserForOtpCode();

      return await this.loginWithOtp(email, password, otpCode);
    }

    return loginResult.accessToken;
  }

  private async promptUserForOtpCode(): Promise<string> {
    return new Promise((resolve) => {
      console.log('Please enter the OTP code:');
    });
  }
}

interface LoginResponse {
  accessToken: string;
  userId: string;
  isOtpRequired: boolean;
  phoneNumber?: string;
  verificationState: string;
}

const authService = new AuthService();

authService.handleLogin('[email protected]', 'SecurePassword123!')
  .then(accessToken => {
    console.log('Login successful! Token:', accessToken);
    localStorage.setItem('accessToken', accessToken);
  })
  .catch(error => {
    console.error('Login failed:', error);
  });

Session Management

Token Expiration

Access tokens are valid for 6 hours from the time of issuance. After expiration, users must re-authenticate.
Store the token creation time and implement automatic re-authentication when approaching expiration.

Token Storage

Store access tokens securely based on your application type: Web Applications:
sessionStorage.setItem('accessToken', token);

localStorage.setItem('accessToken', token);
Mobile Applications:
  • Use secure storage mechanisms (iOS Keychain, Android Keystore)
  • Never store tokens in plain text files or shared preferences
Backend Applications:
  • Store in memory or secure session stores (Redis, database)
  • Use encryption for persistent storage

Token Validation

Before making API calls, validate the token hasn’t expired:
function isTokenValid(token: string, issuedAt: number): boolean {
  const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
  const now = Date.now();
  return (now - issuedAt) < SIX_HOURS_MS;
}

const issuedAt = Date.now();
localStorage.setItem('tokenIssuedAt', issuedAt.toString());

if (!isTokenValid(token, parseInt(issuedAt))) {
  await reauthenticate();
}

Logout

Terminate the current user session and invalidate the access token.

Endpoint

POST /v1/auth/logout
API Reference: POST /v1/auth/logout

Request

curl -X POST https://dev.api.baanx.com/v1/auth/logout \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "Authorization: Bearer US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9"

Response

{
  "success": true
}
After logout, the access token is immediately invalidated and cannot be used for further API calls.

Login Access Token vs OAuth Tokens

Understanding the difference between login access tokens and OAuth tokens helps you choose the right authentication method for your use case.

Why Exchange Tokens at the OAuth Authorize Endpoint?

Even though the login accessToken works for API calls, OAuth tokens provide significant advantages for production applications:

Login Access Token

Short-Term Direct AccessLimitations:
  • Expires in 6 hours (21,600 seconds)
  • Cannot be refreshed
  • Cannot be revoked independently
  • No granular permission scopes
Best for:
  • Quick authentication for first-party apps
  • Short user sessions
  • Testing and development
  • Simple integrations

OAuth Tokens

Long-Lived Delegated AccessAdvantages:
  • Refresh tokens last 7 days (604,800 seconds) for security compliance with financial operations
  • Can obtain new access tokens without re-login
  • Can be revoked via /oauth/revoke endpoint
  • PKCE security (S256 code challenge) prevents interception
  • Proper delegation model for third-party apps
  • Granular permission management
Best for:
  • Long-lived access
  • Third-party integrations
  • Mobile applications
  • Multi-tenant applications

Token Comparison Table

FeatureLogin Access TokenOAuth Access Token
Lifetime6 hours6 hours
Obtained FromPOST /v1/auth/loginPOST /v1/auth/oauth/token
Refresh CapabilityNoneVia refresh token
Refresh Token LifetimeN/A7 days
RevocationExpires onlyCan be revoked
Security ModelDirect credentialsPKCE + OAuth 2.0
Use CaseFirst-party, short sessionsThird-party, long-lived access
Best PracticeDevelopment & testingProduction applications

Use Case Examples

Scenario 1: First-Party Web App
// Simple login for your own application
const { accessToken } = await login(email, password);

// Use for the next 6 hours
const userData = await fetch('/v1/users/me', {
  headers: { 'Authorization': `Bearer ${accessToken}` }
});
Scenario 2: Short Testing Session
# Quick API testing
curl -X GET "https://api.example.com/v1/wallets" \
  -H "Authorization: Bearer US_b6b9168a-bb56-4c6a-9c0d-4650ea74f5f9"
Scenario 3: Admin Tools
  • Internal admin dashboards
  • Short-lived debugging sessions
  • One-time data exports
Important Future Change: The platform will eventually require OAuth tokens for all API endpoints. The login accessToken will only work for initial OAuth authorization steps. Plan your implementation accordingly.

Migration Strategy

If you’re currently using login access tokens:
1

Identify Long-Lived Sessions

Determine which parts of your application need access beyond 6 hours
2

Implement OAuth Flow

Add OAuth 2.0 authorization for those long-lived sessions
3

Implement Token Refresh

Use refresh tokens to maintain sessions without re-authentication
4

Test Revocation

Ensure your app handles token revocation gracefully
5

Monitor Token Expiry

Track refresh token expiration (7 days) and prompt re-authentication

OAuth 2.0 Authentication

For third-party applications, OAuth 2.0 provides delegated authorization without exposing user credentials.

OAuth Flow Overview

When to Use OAuth

Use OAuth 2.0 when:
  • Building third-party integrations
  • Providing API access to external partners
  • Creating multi-tenant applications
  • Implementing “Login with [Your Service]” buttons

Quick Start

OAuth 2.0 implementation involves 4 steps:
1

Initiate Authorization

Request authorization from the user via GET /v1/auth/oauth/authorize/initiate
2

User Authentication

User authenticates on hosted UI (or via API-mode login)
3

Generate Authorization Code

Receive authorization code after user approval
4

Exchange for Tokens

Exchange code for access and refresh tokens via POST /v1/auth/oauth/token
For complete OAuth 2.0 implementation details, see the OAuth 2.0 Guide and API reference documentation.

OAuth vs Standard Login

FeatureStandard LoginOAuth 2.0
Use CaseFirst-party appsThird-party integrations
User CredentialsHandled by your appNever exposed to your app
Token Lifetime6 hoursAccess: 6 hours, Refresh: 7 days
Token RefreshRe-authenticateUse refresh token
DelegationDirect accessScoped permissions

Best Practices

Secure Storage

Always store access tokens securely. Never expose tokens in URLs, logs, or client-side code repositories.

HTTPS Only

All authentication requests must use HTTPS in production. Never send credentials over unencrypted connections.

Rate Limiting

Implement exponential backoff for failed login attempts to prevent brute force attacks.

Token Rotation

Rotate tokens periodically and after sensitive operations. Invalidate tokens on password changes.

Error Handling

Don’t leak sensitive information in error messages. Use generic messages like “Invalid credentials” instead of “User not found” or “Wrong password”.

OTP Timeout

Implement countdown timers for OTP codes to improve UX. Show “Resend code” option after timeout.

Security Considerations

Password Security

  • Never store passwords in plain text
  • Use secure password hashing (bcrypt, Argon2)
  • Implement password strength requirements
  • Enforce password rotation policies
  • Prevent password reuse

Multi-Factor Authentication

OTP provides an additional security layer:
  • SMS-based codes (current implementation)
  • Consider supporting authenticator apps (TOTP)
  • Implement backup codes for account recovery
  • Allow users to enable/disable MFA

Account Protection

  • Lock accounts after multiple failed login attempts
  • Implement CAPTCHA after failed attempts
  • Send security alerts for suspicious logins
  • Log all authentication events for audit

Token Security

  • Use short-lived access tokens (6 hours)
  • Implement token rotation for refresh tokens
  • Invalidate all tokens on password change
  • Provide “Log out all devices” functionality

Troubleshooting

Login Failures

Cause: Trying to use production credentials in sandbox or vice versaResolution: Ensure you’re using the correct x-us-env header and credentials for each environment
Cause: Token expired, invalid format, or wrong environmentResolution:
  • Check token expiration (6-hour limit)
  • Verify token format includes environment prefix (e.g., “US_…”)
  • Ensure x-client-key matches the environment that issued the token
Cause: Phone number issues, carrier blocking, or delayResolution:
  • Verify phone number is correct and can receive SMS
  • Check spam/blocked messages on device
  • Wait 2-3 minutes for delivery delays
  • Request a new code if needed

Integration Issues

Common issues when integrating authentication: Missing Headers:
# Wrong
curl -X POST https://dev.api.baanx.com/v1/auth/login

# Correct
curl -X POST https://dev.api.baanx.com/v1/auth/login \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "Content-Type: application/json"
Wrong Token Format:
// Wrong
headers: { 'Authorization': accessToken }

// Correct
headers: { 'Authorization': `Bearer ${accessToken}` }
Environment Mismatch:
// Production credentials with sandbox endpoint
const response = await fetch('https://dev.api.baanx.com/v1/auth/login', {
  headers: { 'x-client-key': 'PRODUCTION_KEY' }
});

// Ensure credentials match environment

Complete Authentication Example

Full implementation with error handling and token management:
class AuthManager {
  private apiBase: string;
  private clientKey: string;
  private tokenKey = 'accessToken';
  private tokenTimeKey = 'tokenIssuedAt';

  constructor(apiBase: string, clientKey: string) {
    this.apiBase = apiBase;
    this.clientKey = clientKey;
  }

  private get headers() {
    return {
      'x-client-key': this.clientKey,
      'Content-Type': 'application/json'
    };
  }

  private get authHeaders() {
    const token = this.getToken();
    return {
      ...this.headers,
      'Authorization': `Bearer ${token}`
    };
  }

  async login(email: string, password: string): Promise<string> {
    try {
      const response = await fetch(`${this.apiBase}/v1/auth/login`, {
        method: 'POST',
        headers: this.headers,
        body: JSON.stringify({ email, password })
      });

      if (!response.ok) {
        throw new Error('Login failed');
      }

      const data = await response.json();

      if (data.isOtpRequired) {
        await this.sendOtpCode(data.userId);
        const otpCode = await this.getOtpFromUser();
        return await this.loginWithOtp(email, password, otpCode);
      }

      this.storeToken(data.accessToken);
      return data.accessToken;
    } catch (error) {
      console.error('Login error:', error);
      throw error;
    }
  }

  async sendOtpCode(userId: string): Promise<void> {
    const response = await fetch(`${this.apiBase}/v1/auth/login/otp`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ userId })
    });

    if (!response.ok) {
      throw new Error('Failed to send OTP');
    }
  }

  async loginWithOtp(
    email: string,
    password: string,
    otpCode: string
  ): Promise<string> {
    const response = await fetch(`${this.apiBase}/v1/auth/login`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ email, password, otpCode })
    });

    if (!response.ok) {
      throw new Error('OTP verification failed');
    }

    const data = await response.json();
    this.storeToken(data.accessToken);
    return data.accessToken;
  }

  async logout(): Promise<void> {
    try {
      await fetch(`${this.apiBase}/v1/auth/logout`, {
        method: 'POST',
        headers: this.authHeaders
      });
    } finally {
      this.clearToken();
    }
  }

  storeToken(token: string): void {
    localStorage.setItem(this.tokenKey, token);
    localStorage.setItem(this.tokenTimeKey, Date.now().toString());
  }

  getToken(): string | null {
    const token = localStorage.getItem(this.tokenKey);
    if (!token) return null;

    if (!this.isTokenValid()) {
      this.clearToken();
      return null;
    }

    return token;
  }

  clearToken(): void {
    localStorage.removeItem(this.tokenKey);
    localStorage.removeItem(this.tokenTimeKey);
  }

  isTokenValid(): boolean {
    const issuedAt = localStorage.getItem(this.tokenTimeKey);
    if (!issuedAt) return false;

    const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
    const elapsed = Date.now() - parseInt(issuedAt);
    return elapsed < SIX_HOURS_MS;
  }

  isAuthenticated(): boolean {
    return this.getToken() !== null;
  }

  private async getOtpFromUser(): Promise<string> {
    return prompt('Enter OTP code:') || '';
  }
}

const authManager = new AuthManager(
  'https://dev.api.baanx.com',
  'YOUR_CLIENT_KEY'
);

authManager.login('[email protected]', 'SecurePassword123!')
  .then(() => console.log('Login successful'))
  .catch(error => console.error('Login failed:', error));

Next Steps

After successful authentication:
  1. Make Authenticated Requests: Use the access token for all API calls
  2. Check Verification Status: Verify user’s KYC status via Profile Management
  3. Implement Token Refresh: Plan for token expiration and re-authentication