> ## Documentation Index
> Fetch the complete documentation index at: https://whitebit-mintlify-fix-broken-links-1776643999.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage Overview

> OAuth 2.0 authentication endpoints for obtaining and refreshing access tokens.

export const RegionBaseUrl = ({className = "", showBaseUrl = true}) => {
  const [region, setRegionState] = useState(() => {
    if (typeof window !== 'undefined') {
      return localStorage.getItem("api-region-preference") || "com";
    }
    return "com";
  });
  const [mounted, setMounted] = useState(false);
  const observerRef = useRef(null);
  const isSyncingRef = useRef(false);
  const updateAllContentOnPage = targetRegion => {
    try {
      const domainFrom = targetRegion === "eu" ? "whitebit.com" : "whitebit.eu";
      const domainTo = targetRegion === "eu" ? "whitebit.eu" : "whitebit.com";
      const links = document.querySelectorAll('a');
      links.forEach(link => {
        let href = link.getAttribute('href');
        if (href && href.includes(domainFrom)) {
          link.setAttribute('href', href.replace(domainFrom, domainTo));
        }
      });
      const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
        acceptNode: node => {
          if (node.parentElement?.closest('.region-toggle-component')) {
            return NodeFilter.FILTER_REJECT;
          }
          return node.textContent.includes(domainFrom) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
        }
      });
      let currentNode;
      while (currentNode = walker.nextNode()) {
        currentNode.textContent = currentNode.textContent.replace(new RegExp(domainFrom, 'g'), domainTo);
      }
      console.log(`[RegionSync] Global content updated to ${domainTo}`);
    } catch (e) {
      console.error("[RegionSync] Error updating content:", e);
    }
  };
  const updateRegion = (newRegion, source) => {
    if (region === newRegion) return;
    console.log(`[RegionBaseUrl] Updating to "${newRegion}" (Source: ${source})`);
    if (source === 'observer') {
      isSyncingRef.current = true;
      setTimeout(() => isSyncingRef.current = false, 1000);
    }
    setRegionState(newRegion);
    localStorage.setItem("api-region-preference", newRegion);
    updateAllContentOnPage(newRegion);
    if (source === 'user-click') {
      window.dispatchEvent(new CustomEvent("regionChange", {
        detail: newRegion
      }));
      attemptToUpdateNativeDropdown(newRegion, 0);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 1), 500);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 2), 1500);
    }
  };
  const attemptToUpdateNativeDropdown = (targetRegion, attempt) => {
    if (isSyncingRef.current) return;
    try {
      const targetUrl = targetRegion === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
      const targetDesc = targetRegion === "eu" ? "EU Server" : "Production Server";
      const selects = document.querySelectorAll('select');
      for (const select of selects) {
        if (select.innerHTML.includes('whitebit.com') || select.innerHTML.includes('whitebit.eu')) {
          select.value = targetUrl;
          select.dispatchEvent(new Event('change', {
            bubbles: true
          }));
          return;
        }
      }
      const buttons = Array.from(document.querySelectorAll('button, [role="combobox"]'));
      const serverSelector = buttons.find(btn => {
        if (btn.closest('a') || btn.closest('[class*="card"]') || btn.closest('nav')) {
          return false;
        }
        const txt = btn.textContent || "";
        const isServerDropdown = (txt.includes('Production Server') || txt.includes('EU Server') || txt.includes('WhiteBIT Global Server') || txt.includes('WhiteBIT EU Server')) && !txt.includes('Run') && !txt.includes('Send') || btn.getAttribute('role') === 'combobox';
        return isServerDropdown;
      });
      if (serverSelector) {
        const currentText = serverSelector.textContent || "";
        if (currentText.includes(targetDesc)) return;
        serverSelector.click();
        setTimeout(() => {
          const options = document.querySelectorAll('[role="option"], li, button');
          for (const opt of options) {
            const optText = opt.textContent || "";
            if (optText.includes(targetDesc) || optText.includes(targetUrl)) {
              opt.click();
              return;
            }
          }
        }, 100);
      }
    } catch (e) {
      console.error("[Sync] Error:", e);
    }
  };
  useEffect(() => {
    setMounted(true);
    updateAllContentOnPage(region);
    const handleStorageChange = e => {
      if (e.key === "api-region-preference" && e.newValue) {
        updateRegion(e.newValue, 'storage');
      }
    };
    const handleRegionChange = e => {
      if (e.detail !== region) {
        updateRegion(e.detail, 'event');
      }
    };
    window.addEventListener("storage", handleStorageChange);
    window.addEventListener("regionChange", handleRegionChange);
    observerRef.current = new MutationObserver(mutations => {
      if (isSyncingRef.current) return;
      updateAllContentOnPage(region);
      for (const mutation of mutations) {
        if (mutation.type !== 'childList' && mutation.type !== 'characterData') continue;
        const target = mutation.target;
        const el = target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
        if (el && (el.getAttribute('role') === 'option' || el.closest('[role="listbox"]'))) continue;
        const text = target.textContent || "";
        if (text.includes('WhiteBIT EU Server') || text.includes('https://whitebit.eu') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'eu') updateRegion('eu', 'observer');
          }
        } else if (text.includes('WhiteBIT Global Server') || text.includes('https://whitebit.com') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'com') updateRegion('com', 'observer');
          }
        }
      }
    });
    observerRef.current.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true
    });
    if (typeof window !== 'undefined') {
      const current = localStorage.getItem("api-region-preference");
      if (current) attemptToUpdateNativeDropdown(current, 'init');
    }
    return () => {
      window.removeEventListener("storage", handleStorageChange);
      window.removeEventListener("regionChange", handleRegionChange);
      if (observerRef.current) observerRef.current.disconnect();
    };
  }, [region]);
  const apiBaseUrl = region === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
  if (!mounted) return null;
  return <div className={`flex items-center gap-2 flex-wrap my-4 region-toggle-component ${className}`}>
            <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
                Base URL
            </span>
            <span className="text-sm text-gray-400">(</span>
            <div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5 border border-gray-200 dark:border-gray-700">
                <button onClick={() => updateRegion("com", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "com" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .com
                </button>
                <button onClick={() => updateRegion("eu", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "eu" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .eu
                </button>
            </div>
            <span className="text-sm text-gray-400">)</span>
            {showBaseUrl && <>
                    <span className="text-sm text-gray-400">:</span>
                    <a href={apiBaseUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-primary dark:text-primary-light hover:underline">
                        {apiBaseUrl}
                    </a>
                </>}
        </div>;
};

<RegionBaseUrl />

## Introduction

WhiteBIT OAuth 2.0 implementation uses the standard **Authorization Code Grant** flow. This flow is suitable for server-side applications where the client secret can be securely stored.

<CodeGroup>
  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"context"
  	"crypto/rand"
  	"encoding/base64"
  	"encoding/json"
  	"fmt"
  	"io"
  	"log"
  	"net/http"
  	"net/url"
  	"time"
  )

  // TokenResponse represents the OAuth token response
  type TokenResponse struct {
  	Data struct {
  		AccessToken  string `json:"access_token"`
  		ExpiresIn    int    `json:"expires_in"`
  		RefreshToken string `json:"refresh_token"`
  		Scope        string `json:"scope"`
  		TokenType    string `json:"token_type"`
  	} `json:"data"`
  }

  const BASE_URL = "https://whitebit.com"
  const CLIENT_ID = "YOUR_CLIENT_ID"
  const CLIENT_SECRET = "YOUR_CLIENT_SECRET"

  // Initiate OAuth flow
  func initiateOAuthFlowHandler(w http.ResponseWriter, r *http.Request) {
  	b := make([]byte, 32)
  	rand.Read(b)
  	state := base64.RawURLEncoding.EncodeToString(b)

  	// In production, store state in session

  	authURL, _ := url.Parse(BASE_URL + "/auth/login")
  	q := authURL.Query()
  	q.Set("clientId", CLIENT_ID)
  	q.Set("state", state)
  	authURL.RawQuery = q.Encode()

  	http.Redirect(w, r, authURL.String(), http.StatusFound)
  }

  // Handle OAuth callback
  func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
  	code := r.URL.Query().Get("code")
  	state := r.URL.Query().Get("state")

  	// Verify state here...

  	token, err := exchangeCodeForToken(r.Context(), code)
  	if err != nil {
  		http.Error(w, "Authentication failed", http.StatusInternalServerError)
  		return
  	}

  	fmt.Fprintf(w, "Token received: %s", token.Data.AccessToken)
  }

  func exchangeCodeForToken(ctx context.Context, code string) (*TokenResponse, error) {
  	data := url.Values{}
  	data.Set("client_id", CLIENT_ID)
  	data.Set("client_secret", CLIENT_SECRET)
  	data.Set("code", code)

  	resp, err := http.PostForm(BASE_URL+"/oauth2/token", data)
  	if err != nil {
  		return nil, err
  	}
  	defer resp.Body.Close()

  	var tokenResponse TokenResponse
  	json.NewDecoder(resp.Body).Decode(&tokenResponse)
  	return &tokenResponse, nil
  }
  ```

  ```python Python theme={null}
  import http.server
  import http.cookies
  import json
  import secrets
  import socketserver
  import time
  import urllib.error
  import urllib.parse
  import urllib.request
  from typing import Dict, Any, Optional, Tuple

  # Region-aware base URL - set based on the region:
  # Global: "https://whitebit.com"
  # EU: "https://whitebit.eu"
  BASE_URL = "https://whitebit.com"

  # Simple in-memory session store (use Redis or similar for production)
  sessions: Dict[str, Dict[str, Any]] = {}

  class OAuthRequestHandler(http.server.BaseHTTPRequestHandler):
      """HTTP request handler for OAuth flow"""

      def do_GET(self):
          """Handle GET requests"""
          parsed_path = urllib.parse.urlparse(self.path)
          path = parsed_path.path

          if path == '/auth/login':
              self._handle_login()
          elif path == '/auth/callback':
              self._handle_callback()
          else:
              self._send_response(404, b'Not Found')

      def _handle_login(self):
          """Handle OAuth login initiation"""
          # Generate secure state and session ID
          state = generate_secure_token(32)
          session_id = generate_secure_token(16)

          # Create session with state and expiry
          sessions[session_id] = {
              'oauth_state': state,
              'oauth_state_expiry': time.time() + 600  # 10 minutes
          }

          # Set session cookie
          cookie = http.cookies.SimpleCookie()
          cookie['session_id'] = session_id
          cookie['session_id']['path'] = '/'
          cookie['session_id']['httponly'] = True
          cookie['session_id']['samesite'] = 'Lax'
          cookie['session_id']['max-age'] = 600

          if self.headers.get('X-Forwarded-Proto') == 'https':
              cookie['session_id']['secure'] = True

          # Build redirect URL
          redirect_url = (
              f"{BASE_URL}/auth/login?"
              f"clientId={urllib.parse.quote('YOUR_CLIENT_ID')}&"
              f"state={urllib.parse.quote(state)}"
          )

          # Send redirect response
          self.send_response(302)
          self.send_header('Location', redirect_url)
          self.send_header('Set-Cookie', cookie.output(header=''))
          self.end_headers()

      def _handle_callback(self):
          """Handle OAuth callback"""
          # Parse query parameters
          parsed_path = urllib.parse.urlparse(self.path)
          query_params = urllib.parse.parse_qs(parsed_path.query)

          # Get received state
          received_state = query_params.get('state', [''])[0]
          if not received_state:
              self._send_response(400, b'Missing state parameter')
              return

          # Get session ID from cookie
          session_id = self._get_session_id_from_cookie()
          if not session_id or session_id not in sessions:
              self._send_response(400, b'Invalid session')
              return

          # Get session data
          session = sessions[session_id]

          # Verify state expiry
          expiry_time = session.get('oauth_state_expiry', 0)
          if time.time() > expiry_time:
              del sessions[session_id]
              self._send_response(400, b'State expired')
              return

          # Get stored state
          stored_state = session.get('oauth_state', '')

          # Clear stored state
          session.pop('oauth_state', None)
          session.pop('oauth_state_expiry', None)

          # Verify state
          if not stored_state or stored_state != received_state:
              self._send_response(400, b'State validation failed')
              return

          # Get authorization code
          code = query_params.get('code', [''])[0]
          if not code:
              self._send_response(400, b'Missing authorization code')
              return

          try:
              # Exchange code for token
              token_response = exchange_code_for_token(code)

              # Store tokens in session
              session['access_token'] = token_response['access_token']
              session['refresh_token'] = token_response['refresh_token']
              session['token_expires_at'] = time.time() + token_response['expires_in']

              # Redirect to dashboard
              self.send_response(302)
              self.send_header('Location', '/dashboard')
              self.end_headers()
          except Exception as e:
              print(f"Token exchange failed: {e}")
              self._send_response(500, b'Authentication failed')

      def _get_session_id_from_cookie(self) -> Optional[str]:
          """Extract session ID from cookies"""
          cookie_str = self.headers.get('Cookie')
          if not cookie_str:
              return None

          cookie = http.cookies.SimpleCookie()
          cookie.load(cookie_str)

          if 'session_id' not in cookie:
              return None

          return cookie['session_id'].value

      def _send_response(self, status_code: int, content: bytes):
          """Send HTTP response with content"""
          self.send_response(status_code)
          self.send_header('Content-Type', 'text/plain')
          self.send_header('Content-Length', str(len(content)))
          self.end_headers()
          self.wfile.write(content)

  def generate_secure_token(length: int) -> str:
      """Generate a cryptographically secure random token"""
      return secrets.token_urlsafe(length)

  def exchange_code_for_token(code: str) -> Dict[str, Any]:
      """Exchange authorization code for access token"""
      # Prepare request data
      data = urllib.parse.urlencode({
          'client_id': 'YOUR_CLIENT_ID',
          'client_secret': 'YOUR_CLIENT_SECRET',
          'code': code
      }).encode('ascii')

      # Prepare request
      headers = {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json'
      }

      # Create request
      req = urllib.request.Request(
          f'{BASE_URL}/oauth2/token',
          data=data,
          headers=headers,
          method='POST'
      )

      try:
          # Send request and get response
          with urllib.request.urlopen(req, timeout=10) as response:
              response_data = response.read()
              response_json = json.loads(response_data)

              # Extract token data
              return {
                  'access_token': response_json['data']['access_token'],
                  'refresh_token': response_json['data']['refresh_token'],
                  'expires_in': response_json['data']['expires_in'],
                  'token_type': response_json['data']['token_type'],
                  'scope': response_json['data']['scope']
              }
      except urllib.error.HTTPError as e:
          print(f"HTTP error: {e.code} - {e.reason}")
          raise
      except urllib.error.URLError as e:
          print(f"URL error: {e.reason}")
          raise
      except Exception as e:
          print(f"Unexpected error: {e}")
          raise

  def main():
      """Run the server"""
      port = 3000
      handler = OAuthRequestHandler

      with socketserver.TCPServer(("", port), handler) as httpd:
          print(f"Server running on port {port}")
          httpd.serve_forever()

  if __name__ == "__main__":
      main()
  ```

  ```typescript TypeScript theme={null}
  import crypto from 'crypto';
  import http from 'http';
  import https from 'https';
  import { URL, URLSearchParams } from 'url';
  import { parse as parseUrl } from 'url';
  import { parse as parseQueryString } from 'querystring';

  // Region-aware base URL - set based on the region:
  // Global: "https://whitebit.com"
  // EU: "https://whitebit.eu"
  const BASE_URL = "https://whitebit.com";

  // Simple in-memory session store (for production use a proper store)
  const sessions = new Map<string, Record<string, any>>();

  // Generate a secure random state value
  function generateSecureState(): string {
    return crypto.randomBytes(32)
      .toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');
  }

  // Generate session ID
  function generateSessionId(): string {
    return crypto.randomBytes(16).toString('hex');
  }

  // Create HTTP server
  const server = http.createServer((req, res) => {
    // Get or create session
    let sessionId = '';
    const cookies = req.headers.cookie?.split(';').map(c => c.trim());
    const sessionCookie = cookies?.find(c => c.startsWith('sessionId='));

    if (sessionCookie) {
      sessionId = sessionCookie.split('=')[1];
      if (!sessions.has(sessionId)) {
        sessionId = '';
      }
    }

    if (!sessionId) {
      sessionId = generateSessionId();
      res.setHeader('Set-Cookie', `sessionId=${sessionId}; HttpOnly; Path=/; Max-Age=600`);
      sessions.set(sessionId, {});
    }

    const session = sessions.get(sessionId) || {};

    // Parse URL and path
    const parsedUrl = parseUrl(req.url || '');
    const pathname = parsedUrl.pathname || '';

    // Handle routes
    if (pathname === '/auth/login') {
      // Initiate OAuth flow
      const state = generateSecureState();
      session.oauth_state = state;

      res.writeHead(302, {
        'Location': `${BASE_URL}/auth/login?clientId=YOUR_CLIENT_ID&state=${state}`
      });
      res.end();
    }
    else if (pathname === '/auth/callback') {
      // Handle OAuth callback
      const query = parseQueryString(parsedUrl.query || '');
      const receivedState = query.state as string;
      const storedState = session.oauth_state;

      // Clear the stored state immediately
      delete session.oauth_state;

      // Verify the state parameter
      if (!receivedState || receivedState !== storedState) {
        res.writeHead(400);
        res.end('State validation failed');
        return;
      }

      // State is valid, proceed with code exchange
      const code = query.code as string;
      if (code) {
        // Exchange code for token
        exchangeCodeForToken(code, sessionId, res);
      } else {
        res.writeHead(400);
        res.end('Missing authorization code');
      }
    }
    else {
      // Handle other routes or 404
      res.writeHead(404);
      res.end('Not found');
    }
  });

  // Exchange the authorization code for access token
  function exchangeCodeForToken(
    code: string,
    sessionId: string,
    res: http.ServerResponse
  ): void {
    const params = new URLSearchParams({
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET',
      code: code
    }).toString();

    // Construct full URL and parse for https.request()
    const tokenUrl = new URL(`${BASE_URL}/oauth2/token`);
    const options = {
      hostname: tokenUrl.hostname,
      port: tokenUrl.port ? parseInt(tokenUrl.port, 10) : 443,
      path: tokenUrl.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(params)
      }
    };

    const request = https.request(options, (tokenRes) => {
      let data = '';

      tokenRes.on('data', (chunk) => {
        data += chunk;
      });

      tokenRes.on('end', () => {
        try {
          const tokenData = JSON.parse(data);

          // Store tokens in session
          const session = sessions.get(sessionId);
          if (session) {
            session.access_token = tokenData.data.access_token;
            session.refresh_token = tokenData.data.refresh_token;
          }

          // Redirect to dashboard
          res.writeHead(302, {
            'Location': '/dashboard'
          });
          res.end();
        } catch (error) {
          console.error('Failed to parse token response:', error);
          res.writeHead(500);
          res.end('Authentication failed');
        }
      });
    });

    request.on('error', (error) => {
      console.error('Token exchange failed:', error);
      res.writeHead(500);
      res.end('Authentication failed');
    });

    // Write data to request body
    request.write(params);
    request.end();
  }

  const PORT = process.env.PORT || 3000;
  server.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
  ```

  ```java Java theme={null}
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.net.HttpURLConnection;
  import java.net.InetSocketAddress;
  import java.net.URI;
  import java.net.URL;
  import java.net.URLEncoder;
  import java.nio.charset.StandardCharsets;
  import java.security.SecureRandom;
  import java.time.Instant;
  import java.util.Base64;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.stream.Collectors;

  import com.sun.net.httpserver.HttpExchange;
  import com.sun.net.httpserver.HttpHandler;
  import com.sun.net.httpserver.HttpServer;

  public class OAuthServer {
      // Region-aware base URL - set based on the region:
      // Global: "https://whitebit.com"
      // EU: "https://whitebit.eu"
      private static final String BASE_URL = "https://whitebit.com";

      // Simple in-memory session store (use Redis or similar for production)
      private static final Map<String, Map<String, Object>> sessions = new ConcurrentHashMap<>();
      private static final SecureRandom secureRandom = new SecureRandom();
      private static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();

      public static void main(String[] args) throws IOException {
          // Create HTTP server
          HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);

          // Register handlers
          server.createContext("/auth/login", new InitiateOAuthFlowHandler());
          server.createContext("/auth/callback", new OAuthCallbackHandler());

          // Start server
          server.setExecutor(null);
          server.start();
          System.out.println("Server started on port 3000");
      }

      /**
       * Generate a cryptographically secure random string
       */
      private static String generateSecureToken(int byteLength) {
          byte[] bytes = new byte[byteLength];
          secureRandom.nextBytes(bytes);
          return encoder.encodeToString(bytes);
      }

      /**
       * Handler for initiating OAuth flow
       */
      static class InitiateOAuthFlowHandler implements HttpHandler {
          @Override
          public void handle(HttpExchange exchange) throws IOException {
              // Generate secure state and session ID
              String state = generateSecureToken(32);
              String sessionId = generateSecureToken(16);

              // Store state in session with expiry
              Map<String, Object> sessionData = new HashMap<>();
              sessionData.put("oauth_state", state);
              sessionData.put("oauth_state_expiry", Instant.now().plusSeconds(600).getEpochSecond());
              sessions.put(sessionId, sessionData);

              // Set session cookie
              String cookie = String.format(
                  "session_id=%s; Path=/; HttpOnly; SameSite=Lax; Max-Age=600", sessionId);
              if (exchange.getRequestHeaders().containsKey("X-Forwarded-Proto") &&
                  exchange.getRequestHeaders().getFirst("X-Forwarded-Proto").equals("https")) {
                  cookie += "; Secure";
              }

              exchange.getResponseHeaders().add("Set-Cookie", cookie);

              // Build authorization URL
              String redirectUrl = String.format(
                  BASE_URL + "/auth/login?clientId=%s&state=%s",
                  URLEncoder.encode("YOUR_CLIENT_ID", StandardCharsets.UTF_8),
                  URLEncoder.encode(state, StandardCharsets.UTF_8)
              );

              // Redirect to authorization endpoint
              exchange.getResponseHeaders().add("Location", redirectUrl);
              exchange.sendResponseHeaders(302, -1);
              exchange.close();
          }
      }

      /**
       * Handler for OAuth callback
       */
      static class OAuthCallbackHandler implements HttpHandler {
          @Override
          public void handle(HttpExchange exchange) throws IOException {
              try {
                  // Parse query parameters
                  String query = exchange.getRequestURI().getQuery();
                  Map<String, String> queryParams = parseQueryString(query);

                  // Get received state
                  String receivedState = queryParams.get("state");
                  if (receivedState == null || receivedState.isEmpty()) {
                      sendErrorResponse(exchange, 400, "Missing state parameter");
                      return;
                  }

                  // Get session ID from cookie
                  String sessionId = getSessionIdFromCookie(exchange);
                  if (sessionId == null) {
                      sendErrorResponse(exchange, 400, "Invalid session");
                      return;
                  }

                  // Get session data
                  Map<String, Object> session = sessions.get(sessionId);
                  if (session == null) {
                      sendErrorResponse(exchange, 400, "Session not found");
                      return;
                  }

                  // Verify state expiry
                  Long expiryTime = (Long) session.get("oauth_state_expiry");
                  if (expiryTime == null || Instant.now().getEpochSecond() > expiryTime) {
                      sessions.remove(sessionId);
                      sendErrorResponse(exchange, 400, "State expired");
                      return;
                  }

                  // Get stored state
                  String storedState = (String) session.get("oauth_state");

                  // Clear stored state
                  session.remove("oauth_state");
                  session.remove("oauth_state_expiry");

                  // Verify state
                  if (storedState == null || !storedState.equals(receivedState)) {
                      sendErrorResponse(exchange, 400, "State validation failed");
                      return;
                  }

                  // Get authorization code
                  String code = queryParams.get("code");
                  if (code == null || code.isEmpty()) {
                      sendErrorResponse(exchange, 400, "Missing authorization code");
                      return;
                  }

                  // Exchange code for token
                  TokenResponse tokenResponse = exchangeCodeForToken(code);

                  // Store tokens in session
                  session.put("access_token", tokenResponse.accessToken);
                  session.put("refresh_token", tokenResponse.refreshToken);
                  session.put("token_expires_at", Instant.now().plusSeconds(tokenResponse.expiresIn).getEpochSecond());

                  // Redirect to dashboard
                  exchange.getResponseHeaders().add("Location", "/dashboard");
                  exchange.sendResponseHeaders(302, -1);
              } catch (Exception e) {
                  e.printStackTrace();
                  sendErrorResponse(exchange, 500, "Internal server error");
              } finally {
                  exchange.close();
              }
          }

          /**
           * Get session ID from cookie
           */
          private String getSessionIdFromCookie(HttpExchange exchange) {
              String cookiesHeader = exchange.getRequestHeaders().getFirst("Cookie");
              if (cookiesHeader == null) {
                  return null;
              }

              // Parse cookies
              String[] cookies = cookiesHeader.split(";");
              for (String cookie : cookies) {
                  String[] parts = cookie.trim().split("=", 2);
                  if (parts.length == 2 && parts[0].equals("session_id")) {
                      return parts[1];
                  }
              }

              return null;
          }

          /**
           * Send error response
           */
          private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException {
              byte[] response = message.getBytes(StandardCharsets.UTF_8);
              exchange.sendResponseHeaders(statusCode, response.length);
              try (OutputStream os = exchange.getResponseBody()) {
                  os.write(response);
              }
          }
      }

      /**
       * Parse query string into map
       */
      private static Map<String, String> parseQueryString(String query) {
          Map<String, String> params = new HashMap<>();
          if (query == null || query.isEmpty()) {
              return params;
          }

          String[] pairs = query.split("&");
          for (String pair : pairs) {
              String[] keyValue = pair.split("=", 2);
              if (keyValue.length == 2) {
                  params.put(keyValue[0], keyValue[1]);
              }
          }

          return params;
      }

      /**
       * Exchange authorization code for access token
       */
      private static TokenResponse exchangeCodeForToken(String code) throws IOException {
          // Prepare request data
          String requestBody = String.format(
              "client_id=%s&client_secret=%s&code=%s",
              URLEncoder.encode("YOUR_CLIENT_ID", StandardCharsets.UTF_8),
              URLEncoder.encode("YOUR_CLIENT_SECRET", StandardCharsets.UTF_8),
              URLEncoder.encode(code, StandardCharsets.UTF_8)
          );

          // Create connection
          URL url = new URL(BASE_URL + "/oauth2/token");
          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
          connection.setRequestMethod("POST");
          connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
          connection.setRequestProperty("Accept", "application/json");
          connection.setDoOutput(true);
          connection.setConnectTimeout(5000);
          connection.setReadTimeout(5000);

          // Send request
          try (OutputStream os = connection.getOutputStream()) {
              os.write(requestBody.getBytes(StandardCharsets.UTF_8));
          }

          // Handle error response
          int responseCode = connection.getResponseCode();
          if (responseCode != 200) {
              throw new IOException("HTTP error code: " + responseCode);
          }

          // Read response
          try (BufferedReader br = new BufferedReader(new InputStreamReader(
                  connection.getInputStream(), StandardCharsets.UTF_8))) {
              String responseText = br.lines().collect(Collectors.joining("\n"));
              return parseTokenResponse(responseText);
          }
      }

      /**
       * Parse token response
       */
      private static TokenResponse parseTokenResponse(String json) {
          // In a real application, use a proper JSON library like Jackson or Gson
          // Simplified example assuming valid JSON
          TokenResponse response = new TokenResponse();

          // Extract access_token
          int accessTokenStart = json.indexOf("\"access_token\":\"") + 15;
          int accessTokenEnd = json.indexOf("\"", accessTokenStart);
          response.accessToken = json.substring(accessTokenStart, accessTokenEnd);

          // Extract refresh_token
          int refreshTokenStart = json.indexOf("\"refresh_token\":\"") + 16;
          int refreshTokenEnd = json.indexOf("\"", refreshTokenStart);
          response.refreshToken = json.substring(refreshTokenStart, refreshTokenEnd);

          // Extract expires_in
          int expiresInStart = json.indexOf("\"expires_in\":") + 13;
          int expiresInEnd = json.indexOf(",", expiresInStart);
          if (expiresInEnd == -1) {
              expiresInEnd = json.indexOf("}", expiresInStart);
          }
          response.expiresIn = Integer.parseInt(json.substring(expiresInStart, expiresInEnd).trim());

          return response;
      }

      /**
       * Token response object
       */
      static class TokenResponse {
          String accessToken;
          String refreshToken;
          int expiresIn;
      }
  }
  }
  ```

  ```php PHP theme={null}
  <?php
  declare(strict_types=1);

  // Region-aware base URL - set based on the region:
  // Global: "https://whitebit.com"
  // EU: "https://whitebit.eu"
  define('BASE_URL', 'https://whitebit.com');

  /**
  * Generate a cryptographically secure random state value
  *
  * @return string The secure random state value
  */
  function generateSecureState(): string {
      return bin2hex(random_bytes(32)); // 256 bits of entropy
  }

  /**
  * Initiate OAuth flow and redirect user to authorization endpoint
  *
  * @return void
  */
  function initiateOAuthFlow(): void {
      // Start secure session
      session_start([
          'cookie_httponly' => true,
          'cookie_secure' => true,
          'cookie_samesite' => 'Lax',
          'use_strict_mode' => true
      ]);

      // Generate and store state
      $state = generateSecureState();
      $_SESSION['oauth_state'] = $state;
      $_SESSION['oauth_state_created_at'] = time();

      // Set short expiration time for state
      $_SESSION['oauth_state_expires_at'] = time() + 600; // 10 minutes

      // Redirect to authorization endpoint
      $authUrl = BASE_URL . '/auth/login?clientId=' .
          urlencode('YOUR_CLIENT_ID') .
          '&state=' . urlencode($state);

      // Prevent header injection
      if (!headers_sent()) {
          header('Location: ' . $authUrl);
          exit;
      }
  }

  /**
  * Handle OAuth callback and validate state parameter
  *
  * @return void
  */
  function handleOAuthCallback(): void {
      // Start secure session
      session_start([
          'cookie_httponly' => true,
          'cookie_secure' => true,
          'cookie_samesite' => 'Lax',
          'use_strict_mode' => true
      ]);

      // Retrieve states
      $receivedState = $_GET['state'] ?? '';
      $storedState = $_SESSION['oauth_state'] ?? '';
      $expiresAt = $_SESSION['oauth_state_expires_at'] ?? 0;

      // Clear the stored state immediately
      unset($_SESSION['oauth_state'], $_SESSION['oauth_state_expires_at'], $_SESSION['oauth_state_created_at']);

      // Verify the state parameter
      if (empty($receivedState) ||
          $receivedState !== $storedState ||
          time() > $expiresAt) {

          // Potential CSRF attack or expired state - abort authentication
          error_log('OAuth state validation failed');
          http_response_code(400);
          echo 'State validation failed';
          exit;
      }

      // State is valid, proceed with code exchange
      $code = $_GET['code'] ?? '';
      if (!empty($code)) {
          exchangeCodeForToken($code);
      } else {
          http_response_code(400);
          echo 'Missing authorization code';
          exit;
      }
  }

  /**
  * Exchange authorization code for access token
  *
  * @param string $code The authorization code to exchange
  * @return void
  */
  function exchangeCodeForToken(string $code): void {
      // Prepare request parameters
      $params = [
          'client_id' => 'YOUR_CLIENT_ID',
          'client_secret' => 'YOUR_CLIENT_SECRET',
          'code' => $code
      ];

      // Initialize cURL session
      $ch = curl_init();

      // Set cURL options
      curl_setopt_array($ch, [
          CURLOPT_URL => BASE_URL . '/oauth2/token',
          CURLOPT_POST => true,
          CURLOPT_POSTFIELDS => http_build_query($params),
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => [
              'Content-Type: application/x-www-form-urlencoded',
              'Accept: application/json'
          ],
          CURLOPT_TIMEOUT => 30,
          CURLOPT_SSL_VERIFYPEER => true
      ]);

      // Execute request
      $response = curl_exec($ch);
      $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);

      // Check for errors
      if ($httpCode !== 200 || $response === false) {
          error_log('Token exchange failed with HTTP code: ' . $httpCode);
          http_response_code(500);
          echo 'Authentication failed';
          exit;
      }

      // Parse response
      $tokenData = json_decode($response, true);

      // Store tokens securely in session
      $_SESSION['access_token'] = $tokenData['data']['access_token'] ?? null;
      $_SESSION['refresh_token'] = $tokenData['data']['refresh_token'] ?? null;
      $_SESSION['token_expires_at'] = time() + ($tokenData['data']['expires_in'] ?? 300);

      // Redirect to dashboard
      header('Location: /dashboard');
      exit;
  }
  ?>
  ```
</CodeGroup>

## Scopes

Available Scopes (requested during client setup):

* `general`: General API access
* `show.userinfo`: Access to basic user information
* `users.read`: Read user data
* `users.email.read`: Read user email information
* `users.kyc.read`: Information about whether a user has passed KYC verification
* `orders.read`: Read trading orders
* `orders.create`: Create trading orders
* `orders.delete`: Delete trading orders
* `balances.read`: Read account balances
* `markets.read`: Read market information
* `deals.read`: Read trading deals
* `orders_history.read`: Read order history
* `users.transactions.read`: Read user transactions
* `users.converts.read`: Read currency conversion history
* `users.balances.read`: Read user account balances
* `users.orders.read`: Read user orders
* `users.deals.read`: Read user deals
