> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baanx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Mode Flow

> Implement OAuth 2.0 with custom authentication UI

## Overview

API Mode gives you complete control over the authentication experience. Instead of redirecting users to a hosted UI, your application presents a custom login interface where users authenticate and authorize your application to act on their behalf. Your application then generates the authorization code through API calls. This is ideal for mobile apps, custom branded experiences, or headless systems.

<Warning>
  **More complexity, more control:** API mode requires 5 steps and additional security considerations. Only use this if you need full control over the user experience.
</Warning>

<Info>
  **User Authorization Model:** Even with custom UI, the user remains in control of the authentication process and grants your application permission to access their account.
</Info>

## When to Use API Mode

<CardGroup cols={2}>
  <Card title="Ideal For" icon="check">
    * Native mobile applications
    * Custom branded login experiences
    * Headless/API-only architectures
    * Embedded authentication flows
    * White-label solutions
  </Card>

  <Card title="Not Ideal For" icon="xmark">
    * Quick integrations
    * Standard web applications
    * Third-party integrations
    * Limited development resources
  </Card>
</CardGroup>

## Prerequisites

Before starting, ensure you have:

* ✅ API keys (`x-client-key` and `x-secret-key`)
* ✅ Secure credential storage mechanism
* ✅ Understanding of [PKCE implementation](/guides/oauth/security#pkce-implementation)
* ✅ Ability to handle user credentials securely
* ✅ Implementation of proper error handling

## Flow Diagram

```mermaid theme={null}
sequenceDiagram
    participant User as End User
    participant App as Your App
    participant API as API Gateway

    App->>API: Step 1: Initiate with mode=api
    API-->>App: JWT session token

    User->>App: Enter credentials
    App->>API: Step 2: Login user
    API-->>App: Access token (temp)

    App->>API: Step 3: Generate auth code
    Note over App,API: Headers: Bearer token (Step 2)<br/>Body: JWT token (Step 1)
    API-->>App: Authorization code + redirect URL

    App->>API: Step 4: Exchange code for tokens
    API-->>App: Access + Refresh tokens (long-lived)

    App->>API: Step 5: Use access token
    Note over App,API: Authorization: Bearer ACCESS_TOKEN
    API-->>App: Protected resources
```

## Implementation Guide

### Step 1: Initiate OAuth with API Mode

Start the OAuth flow with `mode=api` parameter to indicate you'll handle authentication directly.

<CodeGroup>
  ```typescript TypeScript/JavaScript theme={null}
  import crypto from 'crypto';

  interface OAuthSession {
    jwtToken: string;
    codeVerifier: string;
    state: string;
  }

  function generateCodeVerifier(): string {
    return crypto.randomBytes(32).toString('base64url');
  }

  function generateCodeChallenge(verifier: string): string {
    return crypto
      .createHash('sha256')
      .update(verifier)
      .digest('base64url');
  }

  async function initiateOAuthAPIMode(): Promise<OAuthSession> {
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = generateCodeChallenge(codeVerifier);
    const state = crypto.randomBytes(16).toString('hex');

    const params = new URLSearchParams({
      mode: 'api',  // Key difference from hosted UI
      response_type: 'code',
      client_id: 'your-client-id',
      redirect_uri: 'https://yourapp.com/oauth/callback',
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256'
    });

    const response = await fetch(
      `https://api.example.com/v1/auth/oauth/authorize/initiate?${params}`,
      {
        headers: {
          'x-client-key': process.env.CLIENT_KEY!,
          'x-secret-key': process.env.SECRET_KEY!
        }
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`OAuth initiation failed: ${error.error_description}`);
    }

    const data = await response.json();

    // Store session data for later steps
    return {
      jwtToken: data.token,  // JWT for authorization flow
      codeVerifier,
      state
    };
  }
  ```

  ```python Python theme={null}
  import secrets
  import hashlib
  import base64
  import requests
  from typing import TypedDict
  from urllib.parse import urlencode

  class OAuthSession(TypedDict):
      jwt_token: str
      code_verifier: str
      state: str

  def generate_code_verifier() -> str:
      return base64.urlsafe_b64encode(
          secrets.token_bytes(32)
      ).decode('utf-8').rstrip('=')

  def generate_code_challenge(verifier: str) -> str:
      digest = hashlib.sha256(verifier.encode('utf-8')).digest()
      return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')

  def initiate_oauth_api_mode() -> OAuthSession:
      code_verifier = generate_code_verifier()
      code_challenge = generate_code_challenge(code_verifier)
      state = secrets.token_urlsafe(16)

      params = {
          'mode': 'api',  # Key difference from hosted UI
          'response_type': 'code',
          'client_id': 'your-client-id',
          'redirect_uri': 'https://yourapp.com/oauth/callback',
          'state': state,
          'code_challenge': code_challenge,
          'code_challenge_method': 'S256'
      }

      response = requests.get(
          f'https://api.example.com/v1/auth/oauth/authorize/initiate?{urlencode(params)}',
          headers={
              'x-client-key': os.getenv('CLIENT_KEY'),
              'x-secret-key': os.getenv('SECRET_KEY')
          }
      )

      response.raise_for_status()
      data = response.json()

      # Store session data for later steps
      return {
          'jwt_token': data['token'],  # JWT for authorization flow
          'code_verifier': code_verifier,
          'state': state
      }
  ```

  ```bash cURL theme={null}
  # Generate PKCE parameters
  CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
  CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')
  STATE=$(openssl rand -hex 16)

  echo "Save these for later steps:"
  echo "CODE_VERIFIER=$CODE_VERIFIER"
  echo "STATE=$STATE"

  # Initiate OAuth in API mode
  curl -X GET "https://api.example.com/v1/auth/oauth/authorize/initiate?mode=api&response_type=code&client_id=your-client-id&redirect_uri=https://yourapp.com/oauth/callback&state=$STATE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256" \
    -H "x-client-key: your_public_key" \
    -H "x-secret-key: your_secret_key"
  ```
</CodeGroup>

**Request Parameters:**

| Parameter               | Required | Description                                      |
| ----------------------- | -------- | ------------------------------------------------ |
| `mode`                  | Yes      | Must be `api` (this enables API mode)            |
| `response_type`         | Yes      | Must be `code`                                   |
| `client_id`             | Yes      | Your application's client identifier             |
| `redirect_uri`          | Yes      | Callback URL (required but not used in API mode) |
| `state`                 | Yes      | Random string for CSRF protection                |
| `code_challenge`        | Yes      | BASE64URL(SHA256(code\_verifier))                |
| `code_challenge_method` | Yes      | Must be `S256`                                   |

**Response:**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 600
}
```

<Note>
  **Important:** The `token` returned here is a JWT for the OAuth flow session (valid 10 minutes). It's NOT an access token for API calls.
</Note>

### Step 2: Authenticate User

Present your custom login UI to the user and authenticate them through the API.

<CodeGroup>
  ```typescript TypeScript theme={null}
  interface LoginCredentials {
    email: string;
    password: string;
    otpCode?: string;  // If OTP required
  }

  interface LoginResponse {
    accessToken: string;
    user: {
      id: string;
      email: string;
      name: string;
    };
  }

  async function loginUser(credentials: LoginCredentials): Promise<string> {
    const response = await fetch('https://api.example.com/v1/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': process.env.CLIENT_KEY!
      },
      body: JSON.stringify({
        email: credentials.email,
        password: credentials.password,
        ...(credentials.otpCode && { otpCode: credentials.otpCode })
      })
    });

    if (!response.ok) {
      const error = await response.json();

      // Handle OTP requirement
      if (error.requiresOtp) {
        throw new Error('OTP_REQUIRED');
      }

      throw new Error(`Login failed: ${error.message}`);
    }

    const data: LoginResponse = await response.json();

    // Return access token for Step 3
    return data.accessToken;
  }

  // Example: Handle OTP flow
  async function sendOTP(email: string): Promise<void> {
    await fetch('https://api.example.com/v1/auth/login/otp', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': process.env.CLIENT_KEY!
      },
      body: JSON.stringify({ email })
    });
  }
  ```

  ```python Python theme={null}
  from typing import Optional
  from dataclasses import dataclass

  @dataclass
  class LoginCredentials:
      email: str
      password: str
      otp_code: Optional[str] = None

  def login_user(credentials: LoginCredentials) -> str:
      payload = {
          'email': credentials.email,
          'password': credentials.password
      }

      if credentials.otp_code:
          payload['otpCode'] = credentials.otp_code

      response = requests.post(
          'https://api.example.com/v1/auth/login',
          headers={
              'x-client-key': os.getenv('CLIENT_KEY')
          },
          json=payload
      )

      if not response.ok:
          error = response.json()

          # Handle OTP requirement
          if error.get('requiresOtp'):
              raise ValueError('OTP_REQUIRED')

          raise Exception(f"Login failed: {error.get('message')}")

      data = response.json()

      # Return access token for Step 3
      return data['accessToken']

  def send_otp(email: str) -> None:
      requests.post(
          'https://api.example.com/v1/auth/login/otp',
          headers={'x-client-key': os.getenv('CLIENT_KEY')},
          json={'email': email}
      )
  ```

  ```bash cURL theme={null}
  # Login user
  curl -X POST "https://api.example.com/v1/auth/login" \
    -H "Content-Type: application/json" \
    -H "x-client-key: your_public_key" \
    -d '{
      "email": "user@example.com",
      "password": "SecurePassword123"
    }'

  # If OTP required, send OTP first
  curl -X POST "https://api.example.com/v1/auth/login/otp" \
    -H "Content-Type: application/json" \
    -H "x-client-key: your_public_key" \
    -d '{
      "email": "user@example.com"
    }'

  # Then login with OTP
  curl -X POST "https://api.example.com/v1/auth/login" \
    -H "Content-Type: application/json" \
    -H "x-client-key: your_public_key" \
    -d '{
      "email": "user@example.com",
      "password": "SecurePassword123",
      "otpCode": "123456"
    }'
  ```
</CodeGroup>

**Request Body:**

| Field      | Required    | Description                         |
| ---------- | ----------- | ----------------------------------- |
| `email`    | Yes         | User's email address                |
| `password` | Yes         | User's password                     |
| `otpCode`  | Conditional | Required if OTP is enabled for user |

**Response:**

```json theme={null}
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "user_123",
    "email": "user@example.com",
    "name": "John Doe"
  }
}
```

<Warning>
  **Security:** Never log or expose user passwords. Always transmit over HTTPS in production.
</Warning>

### Step 3: Generate Authorization Code

Use both tokens from Steps 1 and 2 to generate the authorization code.

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function generateAuthCode(
    session: OAuthSession,
    accessToken: string
  ): Promise<{ code: string; state: string }> {
    const response = await fetch('https://api.example.com/v1/auth/oauth/authorize', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`,  // Access token from Step 2
        'x-client-key': process.env.CLIENT_KEY!
      },
      body: JSON.stringify({
        token: session.jwtToken  // JWT token from Step 1
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Authorization failed: ${error.error_description}`);
    }

    const data = await response.json();

    return {
      code: data.code,
      state: data.state
    };
  }
  ```

  ```python Python theme={null}
  from typing import Tuple

  def generate_auth_code(
      session: OAuthSession,
      access_token: str
  ) -> Tuple[str, str]:
      response = requests.post(
          'https://api.example.com/v1/auth/oauth/authorize',
          headers={
              'Authorization': f'Bearer {access_token}',  # Access token from Step 2
              'x-client-key': os.getenv('CLIENT_KEY')
          },
          json={
              'token': session['jwt_token']  # JWT token from Step 1
          }
      )

      response.raise_for_status()
      data = response.json()

      return data['code'], data['state']
  ```

  ```bash cURL theme={null}
  # Save ACCESS_TOKEN from Step 2
  ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

  curl -X POST "https://api.example.com/v1/auth/oauth/authorize" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "x-client-key: your_public_key" \
    -d '{
      "token": "'$JWT_TOKEN'"
    }'
  ```
</CodeGroup>

**Request Requirements:**

| Requirement  | Location             | Description                          |
| ------------ | -------------------- | ------------------------------------ |
| Access Token | Authorization header | Bearer token from Step 2 login       |
| JWT Token    | Request body         | Session token from Step 1 initiation |

**Response:**

```json theme={null}
{
  "code": "auth_code_xyz123",
  "state": "random_csrf_protection_string_12345",
  "url": "https://yourapp.com/oauth/callback?code=auth_code_xyz123&state=random_csrf_protection_string_12345"
}
```

<Note>
  **Token Confusion Prevention:**

  * JWT Token (Step 1): Used ONLY in this request body
  * Access Token (Step 2): Used ONLY in the Authorization header here
  * Neither of these are the final tokens you'll use for API calls
</Note>

### Step 4: Exchange Code for Long-Lived Tokens

Exchange the authorization code for access and refresh tokens that you'll use for actual API calls.

<CodeGroup>
  ```typescript TypeScript theme={null}
  interface TokenResponse {
    access_token: string;
    refresh_token: string;
    expires_in: number;
    refresh_token_expires_in: number;
    token_type: string;
  }

  async function exchangeCodeForTokens(
    code: string,
    session: OAuthSession
  ): Promise<TokenResponse> {
    // Verify state parameter
    if (session.state !== /* state from Step 3 response */) {
      throw new Error('State parameter mismatch');
    }

    const response = await fetch('https://api.example.com/v1/auth/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': process.env.CLIENT_KEY!,
        'x-secret-key': process.env.SECRET_KEY!
      },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: 'https://yourapp.com/oauth/callback',
        code_verifier: session.codeVerifier  // From Step 1
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Token exchange failed: ${error.error_description}`);
    }

    const tokens: TokenResponse = await response.json();

    // Store tokens securely
    await secureStorage.set('access_token', tokens.access_token);
    await secureStorage.set('refresh_token', tokens.refresh_token);
    await secureStorage.set('token_expiry', Date.now() + (tokens.expires_in * 1000));

    return tokens;
  }
  ```

  ```python Python theme={null}
  from typing import Dict

  def exchange_code_for_tokens(
      code: str,
      session: OAuthSession,
      state_from_response: str
  ) -> Dict:
      # Verify state parameter
      if session['state'] != state_from_response:
          raise ValueError('State parameter mismatch')

      response = requests.post(
          'https://api.example.com/v1/auth/oauth/token',
          headers={
              'x-client-key': os.getenv('CLIENT_KEY'),
              'x-secret-key': os.getenv('SECRET_KEY')
          },
          json={
              'grant_type': 'authorization_code',
              'code': code,
              'redirect_uri': 'https://yourapp.com/oauth/callback',
              'code_verifier': session['code_verifier']  # From Step 1
          }
      )

      response.raise_for_status()
      tokens = response.json()

      # Store tokens securely
      secure_storage.set('access_token', tokens['access_token'])
      secure_storage.set('refresh_token', tokens['refresh_token'])
      secure_storage.set('token_expiry', time.time() + tokens['expires_in'])

      return tokens
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.example.com/v1/auth/oauth/token" \
    -H "Content-Type: application/json" \
    -H "x-client-key: your_public_key" \
    -H "x-secret-key: your_secret_key" \
    -d '{
      "grant_type": "authorization_code",
      "code": "auth_code_xyz123",
      "redirect_uri": "https://yourapp.com/oauth/callback",
      "code_verifier": "'$CODE_VERIFIER'"
    }'
  ```
</CodeGroup>

**Request Body:**

| Field           | Required | Description                        |
| --------------- | -------- | ---------------------------------- |
| `grant_type`    | Yes      | Must be `authorization_code`       |
| `code`          | Yes      | Authorization code from Step 3     |
| `redirect_uri`  | Yes      | Must exactly match Step 1          |
| `code_verifier` | Yes      | Original PKCE verifier from Step 1 |

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 21600,
  "refresh_token": "def50200a1b2c3d4e5f6...",
  "refresh_token_expires_in": 604800,
  "scope": "read write"
}
```

<Info>
  **These are your final tokens:**

  * Access token: Use this for all API calls (6 hour expiry)
  * Refresh token: Use this to get new access tokens (7 day expiry)
</Info>

### Step 5: Use Access Token for API Calls

Now use the access token from Step 4 to make API requests on behalf of the user for all authenticated operations.

<Warning>
  **User Authorization Boundaries:** The access token enables your application to act on behalf of the user. Ensure all API calls align with the user's expectations and the permissions they granted.
</Warning>

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function callProtectedAPI(endpoint: string) {
    const accessToken = await secureStorage.get('access_token');
    const tokenExpiry = await secureStorage.get('token_expiry');

    // Check if token expired
    if (Date.now() >= tokenExpiry) {
      // Refresh token before making the call
      accessToken = await refreshAccessToken();
    }

    const response = await fetch(`https://api.example.com${endpoint}`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'x-client-key': process.env.CLIENT_KEY!
      }
    });

    if (response.status === 401) {
      // Token might be invalid, try refreshing
      const newToken = await refreshAccessToken();

      // Retry with new token
      return fetch(`https://api.example.com${endpoint}`, {
        headers: {
          'Authorization': `Bearer ${newToken}`,
          'x-client-key': process.env.CLIENT_KEY!
        }
      });
    }

    return response.json();
  }

  // Example usage
  const userData = await callProtectedAPI('/v1/users/me');
  const walletData = await callProtectedAPI('/v1/wallets');
  ```

  ```python Python theme={null}
  import time

  def call_protected_api(endpoint: str):
      access_token = secure_storage.get('access_token')
      token_expiry = secure_storage.get('token_expiry')

      # Check if token expired
      if time.time() >= token_expiry:
          access_token = refresh_access_token()

      response = requests.get(
          f'https://api.example.com{endpoint}',
          headers={
              'Authorization': f'Bearer {access_token}',
              'x-client-key': os.getenv('CLIENT_KEY')
          }
      )

      if response.status_code == 401:
          # Token might be invalid, try refreshing
          new_token = refresh_access_token()

          # Retry with new token
          response = requests.get(
              f'https://api.example.com{endpoint}',
              headers={
                  'Authorization': f'Bearer {new_token}',
                  'x-client-key': os.getenv('CLIENT_KEY')
              }
          )

      return response.json()

  # Example usage
  user_data = call_protected_api('/v1/users/me')
  wallet_data = call_protected_api('/v1/wallets')
  ```

  ```bash cURL theme={null}
  # Use the access token from Step 4
  FINAL_ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

  curl -X GET "https://api.example.com/v1/users/me" \
    -H "Authorization: Bearer $FINAL_ACCESS_TOKEN" \
    -H "x-client-key: your_public_key"

  curl -X GET "https://api.example.com/v1/wallets" \
    -H "Authorization: Bearer $FINAL_ACCESS_TOKEN" \
    -H "x-client-key: your_public_key"
  ```
</CodeGroup>

## Token Management

### Refresh Access Token

When the access token expires (6 hours), use the refresh token to obtain a new one:

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function refreshAccessToken(): Promise<string> {
    const refreshToken = await secureStorage.get('refresh_token');

    const response = await fetch('https://api.example.com/v1/auth/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': process.env.CLIENT_KEY!,
        'x-secret-key': process.env.SECRET_KEY!
      },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refresh_token: refreshToken
      })
    });

    if (!response.ok) {
      // Refresh token expired or invalid, need to re-authenticate
      throw new Error('REFRESH_FAILED');
    }

    const tokens = await response.json();

    // Update stored tokens
    await secureStorage.set('access_token', tokens.access_token);
    await secureStorage.set('refresh_token', tokens.refresh_token);
    await secureStorage.set('token_expiry', Date.now() + (tokens.expires_in * 1000));

    return tokens.access_token;
  }
  ```

  ```python Python theme={null}
  def refresh_access_token() -> str:
      refresh_token = secure_storage.get('refresh_token')

      response = requests.post(
          'https://api.example.com/v1/auth/oauth/token',
          headers={
              'x-client-key': os.getenv('CLIENT_KEY'),
              'x-secret-key': os.getenv('SECRET_KEY')
          },
          json={
              'grant_type': 'refresh_token',
              'refresh_token': refresh_token
          }
      )

      if not response.ok:
          # Refresh token expired or invalid, need to re-authenticate
          raise Exception('REFRESH_FAILED')

      tokens = response.json()

      # Update stored tokens
      secure_storage.set('access_token', tokens['access_token'])
      secure_storage.set('refresh_token', tokens['refresh_token'])
      secure_storage.set('token_expiry', time.time() + tokens['expires_in'])

      return tokens['access_token']
  ```
</CodeGroup>

Learn more in the [Token Management Guide](/guides/oauth/token-management).

## Complete Implementation Example

Here's a complete mobile app implementation using React Native:

<CodeGroup>
  ```typescript OAuth Service theme={null}
  // services/oauth.service.ts
  import * as SecureStore from 'expo-secure-store';
  import * as Crypto from 'expo-crypto';

  class OAuthService {
    private session: OAuthSession | null = null;

    async startOAuthFlow() {
      // Step 1: Initiate
      this.session = await this.initiateOAuthAPIMode();
      return this.session;
    }

    async authenticateUser(email: string, password: string) {
      if (!this.session) throw new Error('OAuth not initiated');

      // Step 2: Login
      const accessToken = await this.loginUser({ email, password });

      // Step 3: Generate code
      const { code, state } = await this.generateAuthCode(accessToken);

      // Step 4: Exchange for tokens
      const tokens = await this.exchangeCodeForTokens(code, state);

      // Clean up session
      this.session = null;

      return tokens;
    }

    private async initiateOAuthAPIMode(): Promise<OAuthSession> {
      const codeVerifier = await Crypto.digestStringAsync(
        Crypto.CryptoDigestAlgorithm.SHA256,
        Math.random().toString()
      );
      const codeChallenge = await Crypto.digestStringAsync(
        Crypto.CryptoDigestAlgorithm.SHA256,
        codeVerifier
      );
      const state = await Crypto.digestStringAsync(
        Crypto.CryptoDigestAlgorithm.SHA256,
        Math.random().toString()
      );

      const params = new URLSearchParams({
        mode: 'api',
        response_type: 'code',
        client_id: process.env.OAUTH_CLIENT_ID!,
        redirect_uri: 'myapp://oauth/callback',
        state,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256'
      });

      const response = await fetch(
        `${process.env.API_URL}/v1/auth/oauth/authorize/initiate?${params}`,
        {
          headers: {
            'x-client-key': process.env.CLIENT_KEY!,
            'x-secret-key': process.env.SECRET_KEY!
          }
        }
      );

      const data = await response.json();

      return {
        jwtToken: data.token,
        codeVerifier,
        state
      };
    }

    private async loginUser(credentials: LoginCredentials): Promise<string> {
      const response = await fetch(`${process.env.API_URL}/v1/auth/login`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-client-key': process.env.CLIENT_KEY!
        },
        body: JSON.stringify(credentials)
      });

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

    private async generateAuthCode(accessToken: string) {
      const response = await fetch(`${process.env.API_URL}/v1/auth/oauth/authorize`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${accessToken}`,
          'x-client-key': process.env.CLIENT_KEY!
        },
        body: JSON.stringify({
          token: this.session!.jwtToken
        })
      });

      const data = await response.json();
      return { code: data.code, state: data.state };
    }

    private async exchangeCodeForTokens(code: string, state: string) {
      if (state !== this.session!.state) {
        throw new Error('State mismatch');
      }

      const response = await fetch(`${process.env.API_URL}/v1/auth/oauth/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-client-key': process.env.CLIENT_KEY!,
          'x-secret-key': process.env.SECRET_KEY!
        },
        body: JSON.stringify({
          grant_type: 'authorization_code',
          code,
          redirect_uri: 'myapp://oauth/callback',
          code_verifier: this.session!.codeVerifier
        })
      });

      const tokens = await response.json();

      // Store securely
      await SecureStore.setItemAsync('access_token', tokens.access_token);
      await SecureStore.setItemAsync('refresh_token', tokens.refresh_token);
      await SecureStore.setItemAsync('token_expiry',
        String(Date.now() + tokens.expires_in * 1000)
      );

      return tokens;
    }

    async callAPI(endpoint: string) {
      let accessToken = await SecureStore.getItemAsync('access_token');
      const expiry = await SecureStore.getItemAsync('token_expiry');

      if (Date.now() >= Number(expiry)) {
        accessToken = await this.refreshAccessToken();
      }

      return fetch(`${process.env.API_URL}${endpoint}`, {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'x-client-key': process.env.CLIENT_KEY!
        }
      });
    }

    private async refreshAccessToken(): Promise<string> {
      const refreshToken = await SecureStore.getItemAsync('refresh_token');

      const response = await fetch(`${process.env.API_URL}/v1/auth/oauth/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-client-key': process.env.CLIENT_KEY!,
          'x-secret-key': process.env.SECRET_KEY!
        },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: refreshToken
        })
      });

      const tokens = await response.json();

      await SecureStore.setItemAsync('access_token', tokens.access_token);
      await SecureStore.setItemAsync('refresh_token', tokens.refresh_token);
      await SecureStore.setItemAsync('token_expiry',
        String(Date.now() + tokens.expires_in * 1000)
      );

      return tokens.access_token;
    }
  }

  export default new OAuthService();
  ```

  ```typescript Login Screen theme={null}
  // screens/LoginScreen.tsx
  import React, { useState } from 'react';
  import { View, TextInput, Button, Alert } from 'react-native';
  import oauthService from '../services/oauth.service';

  export default function LoginScreen({ navigation }) {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [loading, setLoading] = useState(false);

    const handleLogin = async () => {
      setLoading(true);
      try {
        // Start OAuth flow
        await oauthService.startOAuthFlow();

        // Authenticate user
        await oauthService.authenticateUser(email, password);

        // Navigate to app
        navigation.replace('Home');
      } catch (error) {
        Alert.alert('Login Failed', error.message);
      } finally {
        setLoading(false);
      }
    };

    return (
      <View>
        <TextInput
          placeholder="Email"
          value={email}
          onChangeText={setEmail}
          autoCapitalize="none"
          keyboardType="email-address"
        />
        <TextInput
          placeholder="Password"
          value={password}
          onChangeText={setPassword}
          secureTextEntry
        />
        <Button
          title={loading ? 'Logging in...' : 'Login'}
          onPress={handleLogin}
          disabled={loading}
        />
      </View>
    );
  }
  ```
</CodeGroup>

## Error Handling

<AccordionGroup>
  <Accordion title="Session expired (10 minutes)" icon="clock">
    **Cause:** Too much time between Step 1 initiation and Step 3 authorization.

    **Solution:**

    * Restart from Step 1
    * Implement session expiry monitoring
    * Show countdown timer to users

    ```typescript theme={null}
    if (Date.now() - sessionStartTime > 600000) {
      throw new Error('SESSION_EXPIRED');
    }
    ```
  </Accordion>

  <Accordion title="Invalid credentials" icon="user-lock">
    **Cause:** Wrong email/password in Step 2.

    **Solution:**

    * Display clear error message
    * Allow retry with rate limiting
    * Implement "forgot password" flow

    ```typescript theme={null}
    if (error.code === 'INVALID_CREDENTIALS') {
      setError('Invalid email or password. Please try again.');
    }
    ```
  </Accordion>

  <Accordion title="Token mismatch errors" icon="triangle-exclamation">
    **Cause:** Using wrong token in wrong step.

    **Solution:**

    * JWT Token: Only in Step 3 request body
    * Access Token (Step 2): Only in Step 3 Authorization header
    * Access Token (Step 4): For all subsequent API calls

    ```typescript theme={null}
    // Step 3: CORRECT
    headers: {
      'Authorization': `Bearer ${accessTokenFromStep2}`,
    },
    body: {
      token: jwtTokenFromStep1
    }
    ```
  </Accordion>

  <Accordion title="OTP required but not provided" icon="mobile">
    **Cause:** User has 2FA enabled.

    **Solution:**

    * Check `requiresOtp` in login response
    * Call `POST /v1/auth/login/otp` to send code
    * Prompt user for OTP
    * Retry login with `otpCode` field

    ```typescript theme={null}
    if (error.requiresOtp) {
      await sendOTP(email);
      const otpCode = await promptUserForOTP();
      await loginUser({ email, password, otpCode });
    }
    ```
  </Accordion>
</AccordionGroup>

## Security Considerations

<CardGroup cols={2}>
  <Card title="Credential Security" icon="lock">
    * Never log passwords
    * Clear password fields after use
    * Use HTTPS only
    * Implement rate limiting
  </Card>

  <Card title="Token Storage" icon="database">
    * Use platform-specific secure storage
    * Never store in AsyncStorage (RN)
    * Never store in localStorage (web)
    * Encrypt if possible
  </Card>

  <Card title="PKCE Validation" icon="shield-check">
    * Store code\_verifier securely
    * Don't reuse code\_verifier
    * Validate state parameter
    * Clear after exchange
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation">
    * Don't expose internal errors
    * Log security events
    * Implement retry limits
    * Clear sensitive data on error
  </Card>
</CardGroup>

Learn more in the [Security Guide](/guides/oauth/security).

## Testing Checklist

Before production:

* [ ] All 5 steps complete successfully
* [ ] PKCE code\_verifier validation works
* [ ] State parameter prevents CSRF
* [ ] Session timeout (10 min) is handled
* [ ] Invalid credentials show appropriate error
* [ ] OTP flow works if enabled
* [ ] Token refresh works automatically
* [ ] Expired refresh token triggers re-auth
* [ ] API calls use correct access token
* [ ] Secure storage is implemented
* [ ] Network errors are handled gracefully
* [ ] User can logout and revoke tokens

## Next Steps

<CardGroup cols={2}>
  <Card title="Token Management" icon="rotate" href="/guides/oauth/token-management">
    Master token lifecycle and refresh strategies
  </Card>

  <Card title="Security Best Practices" icon="shield" href="/guides/oauth/security">
    Essential security guidelines for production
  </Card>

  <Card title="Troubleshooting" icon="screwdriver-wrench" href="/guides/oauth/troubleshooting">
    Common issues and solutions
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Detailed endpoint documentation
  </Card>
</CardGroup>
