---
updatedAt: 2026-08-16T06:23:59.000Z
agentTools:
  projectIndex: https://docs.wirexapp.com/llms.txt
---

# Authentication

Authentication methods for Wirex BaaS API integration.

## Before You Start

Read the following guides before proceeding:

| Guide                                 | Why                                                    |
| ------------------------------------- | ------------------------------------------------------ |
| [Platform Overview](https://docs.wirexapp.com/docs/introduction) | Credentials, environments, and onboarding requirements |

***

## Overview

The retail API uses two bearer tokens. They are not interchangeable: the first represents your
company, the second represents one user.

|                            | S2S token                              | User token                                |
| -------------------------- | -------------------------------------- | ----------------------------------------- |
| **Obtained from**          | `POST /api/v1/token`                   | `POST /api/v1/user/authorize`             |
| **Proves**                 | Your company's identity                | One user's identity                       |
| **Carries**                | `company_id`, `azp`, `scope`           | The user the token was issued for         |
| **Reaches**                | Every endpoint                         | Every user-scoped endpoint, for that user |
| **Needs identity headers** | Yes — `X-User-Wallet` and `X-Chain-Id` | No — identity is in the token             |
| **Lives on**               | Your backend only                      | Safe to hand to a client app              |
| **Lifetime**               | 48 hours (172800 seconds)              | Read `expires_at` from the response       |

```
client_id + client_secret     →  S2S token
S2S token + user headers      →  user token
```

An identity token from Privy or Crossmint replaces the credentials in the first step when the user
signs in through one of those providers; the outcome is the same Wirex token.

Sensitive operations need a second, short-lived **action token** on top of the bearer token. All token
types, including action and KYC tokens, are inventoried in [Auth Tokens](https://docs.wirexapp.com/docs/retail-auth-tokens).

> **Naming across trees.** The Corporate API calls these the **partner token** and the **corporation
> token**. Same two-step shape, different names — see
> [Corporate Authentication](https://docs.wirexapp.com/docs/corporate-authentication).

***

## 1. Server-to-Server Authentication

S2S authentication is used when your backend needs to call Wirex APIs without a specific user context. Examples include user registration, system-level queries, and batch operations.

### Token Exchange

```
POST /api/v1/token
```

#### Request

```json
{
  "client_id": "your-client-uuid",
  "client_secret": "your-client-secret",
  "grant_type": "client_credentials"
}
```

#### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_at": 1739612400
}
```

### Token Characteristics

| Property  | Value                     |
| --------- | ------------------------- |
| Algorithm | HMAC-SHA256               |
| Validity  | 48 hours (172800 seconds) |
| Scope     | `partner:full`            |

### Rate Limiting

The `POST /api/v1/token` endpoint has rate limiting:

* Per-IP limit
* Per-client limit

Implement exponential backoff on 429 responses.

### Token Claims

The issued JWT contains:

```json
{
  "iss": "https://api-baas.wirexapp.com/srv",
  "sub": "your-client-uuid",
  "azp": "your-client-uuid",
  "company_id": "your-company-uuid",
  "scope": "partner:full",
  "iat": 1739525600,
  "exp": 1739612400
}
```

### Usage

Include the token in the `Authorization` header:

```
Authorization: Bearer <access_token>
```

### JavaScript

```javascript
class WirexAuthClient {
  constructor(clientId, clientSecret, baseUrl) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = baseUrl;
    this.token = null;
    this.expiresAt = 0;
  }

  async getToken() {
    // Return cached token if still valid (with 5 minute buffer)
    if (this.token && Date.now() / 1000 < this.expiresAt - 300) {
      return this.token;
    }

    const response = await fetch(`${this.baseUrl}/api/v1/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: this.clientId,
        client_secret: this.clientSecret,
        grant_type: 'client_credentials',
      }),
    });

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

    const data = await response.json();
    this.token = data.access_token;
    this.expiresAt = data.expires_at;

    return this.token;
  }
}

// Usage
const auth = new WirexAuthClient(
  'your-client-uuid',
  'your-client-secret',
  'https://api-baas.wirexapp.com'
);

const token = await auth.getToken();
```

### Python

```python
import time
import requests

class WirexAuthClient:
    def __init__(self, client_id: str, client_secret: str, base_url: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = base_url
        self._token = None
        self._expires_at = 0

    def get_token(self) -> str:
        # Return cached token if still valid (with 5 minute buffer)
        if self._token and time.time() < self._expires_at - 300:
            return self._token

        response = requests.post(
            f"{self.base_url}/api/v1/token",
            json={
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "grant_type": "client_credentials",
            },
        )
        response.raise_for_status()

        data = response.json()
        self._token = data["access_token"]
        self._expires_at = data["expires_at"]

        return self._token

auth = WirexAuthClient(
    "your-client-uuid",
    "your-client-secret",
    "https://api-baas.wirexapp.com"
)

token = auth.get_token()
```

### Go

```go
package wirex

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type AuthClient struct {
    clientID     string
    clientSecret string
    baseURL      string
    token        string
    expiresAt    int64
    mu           sync.RWMutex
}

func NewAuthClient(clientID, clientSecret, baseURL string) *AuthClient {
    return &AuthClient{
        clientID:     clientID,
        clientSecret: clientSecret,
        baseURL:      baseURL,
    }
}

func (c *AuthClient) GetToken() (string, error) {
    c.mu.RLock()
    // Return cached token if still valid (with 5 minute buffer)
    if c.token != "" && time.Now().Unix() < c.expiresAt-300 {
        token := c.token
        c.mu.RUnlock()
        return token, nil
    }
    c.mu.RUnlock()

    c.mu.Lock()
    defer c.mu.Unlock()

    // Double-check after acquiring write lock
    if c.token != "" && time.Now().Unix() < c.expiresAt-300 {
        return c.token, nil
    }

    reqBody, _ := json.Marshal(map[string]string{
        "client_id":     c.clientID,
        "client_secret": c.clientSecret,
        "grant_type":    "client_credentials",
    })

    resp, err := http.Post(
        c.baseURL+"/api/v1/token",
        "application/json",
        bytes.NewBuffer(reqBody),
    )
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("token exchange failed: %d", resp.StatusCode)
    }

    var tokenResp struct {
        AccessToken string `json:"access_token"`
        ExpiresAt   int64  `json:"expires_at"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
        return "", err
    }

    c.token = tokenResp.AccessToken
    c.expiresAt = tokenResp.ExpiresAt

    return c.token, nil
}
```

### User Identity Headers

When using S2S authentication, most endpoints require user identity headers to specify which user the request is for:

| Header          | Description          |
| --------------- | -------------------- |
| `X-User-Wallet` | User's EOA address   |
| `X-User-Email`  | User's email address |
| `X-User-Id`     | User's UUID          |

Provide **exactly one** of these headers. Providing more than one is rejected with `ErrorInvalidField` (`field: user_identifier`); providing none returns `ErrorMissingField` (`No valid user identifier provided`). `X-User-Wallet` is the user's EOA address (the value passed as `user_address` at registration) — not the Smart Wallet address.

### User-Agnostic Endpoints

The following endpoints do **not** require user identity headers (`X-User-Wallet`, `X-User-Email`, `X-User-Id`):

| Endpoint                       | Description               |
| ------------------------------ | ------------------------- |
| `POST /api/v1/token`           | S2S token exchange        |
| `POST /api/v1/user`            | User registration (V1)    |
| `POST /api/v2/user`            | User registration (V2)    |
| `GET /api/v1/config`           | Application configuration |
| `GET /api/v1/validation/rules` | Validation rules          |

For all other endpoints, user identity headers are **mandatory**.

***

## 2. User Token Issuance (Login as User)

This flow allows you to obtain a user-specific token from your backend, then transfer it to a client application. The client application uses this token for direct API calls without exposing your credentials.

### How It Works

1. Your backend authenticates with S2S credentials
2. Your backend calls `POST /api/v1/user/authorize` with user identity headers
3. Wirex returns a user-specific access token
4. You transfer this token to the client application
5. Client application makes direct API calls using the user token

### Login as User Endpoint

```
POST /api/v1/user/authorize
```

### Required Headers

| Header          | Description                                       |
| --------------- | ------------------------------------------------- |
| `Authorization` | `Bearer <s2s_token>` from S2S auth                |
| `X-User-Wallet` | User's EOA address (not the Smart Wallet address) |
| `X-Chain-Id`    | Target blockchain ID (137=Polygon, 8453=Base)     |

### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": 1739612400
}
```

### Token Flow

```
Your Backend                 Wirex API                    Client App
      |                          |                            |
      | 1. S2S Token Exchange    |                            |
      |------------------------->|                            |
      |<-------------------------|                            |
      |    { s2s_token }         |                            |
      |                          |                            |
      | 2. POST /user/authorize  |                            |
      |    + X-User-Wallet       |                            |
      |------------------------->|                            |
      |                          |                            |
      |<-------------------------|                            |
      |    { user_token }        |                            |
      |                          |                            |
      | 3. Transfer token        |                            |
      |------------------------------------------------------>|
      |                          |                            |
      |                          | 4. Direct API calls        |
      |                          |<---------------------------|
      |                          |    Authorization: Bearer   |
      |                          |    <user_token>            |
      |                          |--------------------------->|
      |                          |                            |
```

### JavaScript

```javascript
class WirexClient {
  constructor(authClient, baseUrl) {
    this.authClient = authClient;
    this.baseUrl = baseUrl;
  }

  // Get user-specific token (call from your backend)
  async getUserToken(userAddress, chainId) {
    const s2sToken = await this.authClient.getToken();

    const response = await fetch(`${this.baseUrl}/api/v1/user/authorize`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${s2sToken}`,
        'Content-Type': 'application/json',
        'X-User-Wallet': userAddress,
        'X-Chain-Id': chainId.toString(),
      },
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Login as user failed: ${error.description}`);
    }

    return response.json();
  }
}

// Backend usage
const auth = new WirexAuthClient(clientId, clientSecret, baseUrl);
const client = new WirexClient(auth, baseUrl);

// Get user token
const { access_token, expires_at } = await client.getUserToken(
  '0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE',
  137
);

// Transfer access_token to client application
```

```javascript
// Client application usage (after receiving token from backend)
class WirexUserClient {
  constructor(userToken, baseUrl, chainId) {
    this.userToken = userToken;
    this.baseUrl = baseUrl;
    this.chainId = chainId;
  }

  async request(method, path, body = null) {
    const headers = {
      'Authorization': `Bearer ${this.userToken}`,
      'Content-Type': 'application/json',
      'X-Chain-Id': this.chainId.toString(),
    };

    const options = { method, headers };
    if (body) {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(`${this.baseUrl}${path}`, options);

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

    return response.json();
  }

  getUser() {
    return this.request('GET', '/api/v1/user');
  }

  getCards() {
    return this.request('GET', '/api/v1/cards');
  }

  getWallet() {
    return this.request('GET', '/api/v1/wallet');
  }
}

// Client app receives token from backend
const userClient = new WirexUserClient(access_token, baseUrl, 137);

const user = await userClient.getUser();
const cards = await userClient.getCards();
```

### Python

```python
class WirexClient:
    def __init__(self, auth_client: WirexAuthClient, base_url: str):
        self.auth_client = auth_client
        self.base_url = base_url

    def get_user_token(self, user_address: str, chain_id: int) -> dict:
        """Get user-specific token (call from your backend)"""
        s2s_token = self.auth_client.get_token()

        response = requests.post(
            f"{self.base_url}/api/v1/user/authorize",
            headers={
                "Authorization": f"Bearer {s2s_token}",
                "Content-Type": "application/json",
                "X-User-Wallet": user_address,
                "X-Chain-Id": str(chain_id),
            },
        )
        response.raise_for_status()
        return response.json()

auth = WirexAuthClient(client_id, client_secret, base_url)
client = WirexClient(auth, base_url)

token_data = client.get_user_token(
    "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
    137
)
```

```python
class WirexUserClient:
    """Client application usage (after receiving token from backend)"""

    def __init__(self, user_token: str, base_url: str, chain_id: int):
        self.user_token = user_token
        self.base_url = base_url
        self.chain_id = chain_id

    def _request(self, method: str, path: str, body: dict = None) -> dict:
        response = requests.request(
            method,
            f"{self.base_url}{path}",
            headers={
                "Authorization": f"Bearer {self.user_token}",
                "Content-Type": "application/json",
                "X-Chain-Id": str(self.chain_id),
            },
            json=body,
        )
        response.raise_for_status()
        return response.json()

    def get_user(self) -> dict:
        return self._request("GET", "/api/v1/user")

    def get_cards(self) -> dict:
        return self._request("GET", "/api/v1/cards")

    def get_wallet(self) -> dict:
        return self._request("GET", "/api/v1/wallet")

user_client = WirexUserClient(access_token, base_url, 137)

user = user_client.get_user()
cards = user_client.get_cards()
```

### Go

```go
type Client struct {
    auth    *AuthClient
    baseURL string
    http    *http.Client
}

func NewClient(auth *AuthClient, baseURL string) *Client {
    return &Client{
        auth:    auth,
        baseURL: baseURL,
        http:    &http.Client{Timeout: 30 * time.Second},
    }
}

type UserTokenResponse struct {
    AccessToken string `json:"access_token"`
    ExpiresAt   int64  `json:"expires_at"`
}

// GetUserToken obtains a user-specific token (call from your backend)
func (c *Client) GetUserToken(userAddress string, chainID int) (*UserTokenResponse, error) {
    s2sToken, err := c.auth.GetToken()
    if err != nil {
        return nil, err
    }

    req, _ := http.NewRequest("POST", c.baseURL+"/api/v1/user/authorize", nil)
    req.Header.Set("Authorization", "Bearer "+s2sToken)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-User-Wallet", userAddress)
    req.Header.Set("X-Chain-Id", fmt.Sprintf("%d", chainID))

    resp, err := c.http.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("login as user failed: %d", resp.StatusCode)
    }

    var tokenResp UserTokenResponse
    json.NewDecoder(resp.Body).Decode(&tokenResp)
    return &tokenResp, nil
}
```

```go
// UserClient for client application (after receiving token from backend)
type UserClient struct {
    userToken string
    baseURL   string
    chainID   int
    http      *http.Client
}

func NewUserClient(userToken, baseURL string, chainID int) *UserClient {
    return &UserClient{
        userToken: userToken,
        baseURL:   baseURL,
        chainID:   chainID,
        http:      &http.Client{Timeout: 30 * time.Second},
    }
}

func (c *UserClient) request(method, path string, body interface{}) ([]byte, error) {
    var reqBody io.Reader
    if body != nil {
        jsonBody, _ := json.Marshal(body)
        reqBody = bytes.NewBuffer(jsonBody)
    }

    req, _ := http.NewRequest(method, c.baseURL+path, reqBody)
    req.Header.Set("Authorization", "Bearer "+c.userToken)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Chain-Id", fmt.Sprintf("%d", c.chainID))

    resp, err := c.http.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func (c *UserClient) GetUser() ([]byte, error) {
    return c.request("GET", "/api/v1/user", nil)
}

func (c *UserClient) GetCards() ([]byte, error) {
    return c.request("GET", "/api/v1/cards", nil)
}
```

***

## 3. Privy-Based Authentication

Privy-based authentication is used when your frontend or mobile application integrates Privy SDK for wallet management. Privy tokens are exchanged for Wirex-issued tokens, which are then used for all subsequent API calls.

### Prerequisites

1. Set up a Privy application at [privy.io](https://privy.io)
2. Provide your Privy App ID to Wirex during onboarding
3. Wirex configures the Privy App ID in your company credentials

### How It Works

1. User authenticates via Privy SDK in your application
2. Privy issues an access token and identity token
3. Your app calls `POST /api/v1/user/authorize` with Privy tokens
4. Wirex validates Privy tokens against Privy's JWKS endpoint
5. Wirex returns a Wirex-issued access token
6. Your app stores the Wirex token and uses it for all subsequent API calls

### Token Exchange Headers

When exchanging Privy tokens for a Wirex token:

| Header          | Description                     |
| --------------- | ------------------------------- |
| `Authorization` | `Bearer <privy_access_token>`   |
| `Identity`      | `Bearer <privy_identity_token>` |
| `X-Chain-Id`    | Target blockchain ID            |

### Token Exchange Endpoint

```
POST /api/v1/user/authorize
```

#### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": 1739612400
}
```

### Token Flow

```
User                    Your App                 Privy                   Wirex API
  |                        |                       |                         |
  | 1. Login               |                       |                         |
  |----------------------->|                       |                         |
  |                        | 2. Authenticate       |                         |
  |                        |---------------------->|                         |
  |                        |                       |                         |
  |                        |<----------------------|                         |
  |                        | privy_access_token +  |                         |
  |                        | privy_identity_token  |                         |
  |                        |                       |                         |
  |                        | 3. POST /user/authorize                         |
  |                        |------------------------------------------------>|
  |                        |   Authorization: Bearer <privy_access_token>    |
  |                        |   Identity: Bearer <privy_identity_token>       |
  |                        |                       |                         |
  |                        |                       | 4. Verify Privy tokens  |
  |                        |                       |<------------------------|
  |                        |                       |------------------------>|
  |                        |                       |                         |
  |                        |<------------------------------------------------|
  |                        |   { access_token, expires_at }                  |
  |                        |   (Wirex token)       |                         |
  |                        |                       |                         |
  |                        | 5. Store Wirex token  |                         |
  |                        |                       |                         |
  |                        | 6. Subsequent API calls with Wirex token        |
  |                        |------------------------------------------------>|
  |                        |   Authorization: Bearer <wirex_token>           |
  |                        |                       |                         |
  |<-----------------------|                       |                         |
```

### JavaScript (React with Privy SDK)

```javascript
import { usePrivy } from '@privy-io/react-auth';
import { useState, useEffect } from 'react';

// Token manager for storing Wirex token
const tokenManager = {
  token: null,
  expiresAt: 0,

  setToken(accessToken, expiresAt) {
    this.token = accessToken;
    this.expiresAt = expiresAt;
    localStorage.setItem('wirex_token', JSON.stringify({ accessToken, expiresAt }));
  },

  getToken() {
    if (this.token && Date.now() / 1000 < this.expiresAt - 300) {
      return this.token;
    }
    const stored = localStorage.getItem('wirex_token');
    if (stored) {
      const { accessToken, expiresAt } = JSON.parse(stored);
      if (Date.now() / 1000 < expiresAt - 300) {
        this.token = accessToken;
        this.expiresAt = expiresAt;
        return accessToken;
      }
    }
    return null;
  },

  clear() {
    this.token = null;
    this.expiresAt = 0;
    localStorage.removeItem('wirex_token');
  }
};

// Hook for token exchange and API calls
function useWirexAuth() {
  const { getAccessToken, getIdentityToken } = usePrivy();
  const baseUrl = 'https://api-baas.wirexapp.com';
  const chainId = 137;

  // Exchange Privy tokens for Wirex token
  async function authenticate() {
    const existingToken = tokenManager.getToken();
    if (existingToken) {
      return existingToken;
    }

    const privyAccessToken = await getAccessToken();
    const privyIdentityToken = await getIdentityToken();

    const response = await fetch(`${baseUrl}/api/v1/user/authorize`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${privyAccessToken}`,
        'Identity': `Bearer ${privyIdentityToken}`,
        'Content-Type': 'application/json',
        'X-Chain-Id': chainId.toString(),
      },
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.description);
    }

    const { access_token, expires_at } = await response.json();
    tokenManager.setToken(access_token, expires_at);
    return access_token;
  }

  // Make authenticated API request using Wirex token
  async function request(method, path, body = null) {
    const wirexToken = await authenticate();

    const headers = {
      'Authorization': `Bearer ${wirexToken}`,
      'Content-Type': 'application/json',
      'X-Chain-Id': chainId.toString(),
    };

    const options = { method, headers };
    if (body) {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(`${baseUrl}${path}`, options);

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.description);
    }

    return response.json();
  }

  return {
    authenticate,
    getUser: () => request('GET', '/api/v1/user'),
    getWallet: () => request('GET', '/api/v1/wallet'),
    getCards: () => request('GET', '/api/v1/cards'),
    createVirtualCard: () => request('POST', '/api/v1/cards/virtual'),
  };
}

// Usage in component
function Dashboard() {
  const { getUser } = useWirexAuth();
  const [user, setUser] = useState(null);

  useEffect(() => {
    getUser().then(setUser);
  }, []);

  return <div>Welcome, {user?.email}</div>;
}
```

### User Registration with Privy

For new users authenticating via Privy, use the retail registration endpoint first:

```
POST /api/v1/user/retail
```

This endpoint also exchanges Privy tokens and returns a Wirex token.

#### Request Headers

| Header          | Description                     |
| --------------- | ------------------------------- |
| `Authorization` | `Bearer <privy_access_token>`   |
| `Identity`      | `Bearer <privy_identity_token>` |
| `X-Chain-Id`    | Target blockchain ID            |

#### Request Body

```json
{
  "country": "US"
}
```

The user's email and wallet address are extracted from the Privy tokens.

#### Response

```json
{
  "id": "user-uuid"
}
```

After registration, call `POST /api/v1/user/authorize` to obtain the Wirex token for subsequent API calls.

### JavaScript

```javascript
async function registerAndAuthenticate(country) {
  const privyAccessToken = await getAccessToken();
  const privyIdentityToken = await getIdentityToken();
  const baseUrl = 'https://api-baas.wirexapp.com';
  const chainId = '137';

  // Step 1: Register user
  const registerResponse = await fetch(`${baseUrl}/api/v1/user/retail`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${privyAccessToken}`,
      'Identity': `Bearer ${privyIdentityToken}`,
      'Content-Type': 'application/json',
      'X-Chain-Id': chainId,
    },
    body: JSON.stringify({ country }),
  });

  if (!registerResponse.ok) {
    const error = await registerResponse.json();
    throw new Error(error.description);
  }

  const { id: userId } = await registerResponse.json();

  // Step 2: Exchange Privy tokens for Wirex token
  const authResponse = await fetch(`${baseUrl}/api/v1/user/authorize`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${privyAccessToken}`,
      'Identity': `Bearer ${privyIdentityToken}`,
      'Content-Type': 'application/json',
      'X-Chain-Id': chainId,
    },
  });

  if (!authResponse.ok) {
    const error = await authResponse.json();
    throw new Error(error.description);
  }

  const { access_token, expires_at } = await authResponse.json();

  // Step 3: Store Wirex token for subsequent API calls
  tokenManager.setToken(access_token, expires_at);

  return { userId, accessToken: access_token };
}
```

### Privy Token Claims

Wirex extracts the following from Privy tokens:

| Claim             | Source         | Description                          |
| ----------------- | -------------- | ------------------------------------ |
| `sub`             | Access token   | Privy user ID                        |
| `aud`             | Access token   | Privy App ID (must match configured) |
| `linked_accounts` | Identity token | Array of linked wallets              |

The `linked_accounts` claim contains wallet information:

```json
{
  "linked_accounts": [
    {
      "type": "smart_wallet",
      "address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
      "chain_type": "ethereum"
    }
  ]
}
```

***

## 4. Crossmint Authentication

Wirex also accepts Crossmint tokens, for integrations where users sign in through Crossmint in your
own front end.

The flow mirrors Privy: the acting user is taken from the provider's **identity token** — the wallet
address is read from its `linked_accounts` claim — so the `X-User-*` headers are not sent on this
path. The outcome is the same Wirex token, and everything after the exchange is identical.

Crossmint is also a supported wallet deployment path — see
[On-Chain Registration](https://docs.wirexapp.com/docs/retail-onchain-registration) for `Option C: Direct Deployment (Stellar with
Crossmint SDK)` and the [SDK](https://docs.wirexapp.com/docs/retail-sdk) guide for the client-side helpers.

Provider configuration is part of onboarding. Contact Wirex before building against this path.

***

## 5. Security Best Practices

### Credential Storage

* Store `client_secret` in a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager)
* Never commit credentials to source control
* Never expose credentials in client-side code or logs
* Rotate credentials immediately if compromised

### Token Handling

* Cache tokens until near expiration (recommended: refresh 5 minutes before expiry)
* Use secure storage for tokens (encrypted at rest)
* Never log full tokens; truncate for debugging
* Implement token refresh with retry logic

### Request Security

* Always use HTTPS (TLS 1.2+)
* Validate SSL certificates
* Implement request timeouts (recommended: 30 seconds)
* Use idempotency keys for mutation requests where supported

***

## 6. Error Responses

### Error Response Format

```json
{
  "error_reason": "ErrorPermissionDenied",
  "error_description": "Invalid client credentials",
  "error_category": {
    "category": "CategoryUnauthorized",
    "http_status_code": 401
  },
  "error_details": [
    {"key": "field", "details": "client_secret"}
  ]
}
```

### Error Categories

| Category                    | HTTP Status | Description                                         |
| --------------------------- | ----------- | --------------------------------------------------- |
| `CategoryValidationFailure` | 400         | Invalid request data, missing fields, format errors |
| `CategoryUnauthorized`      | 401         | Authentication or authorization failure             |
| `CategoryInternalFailure`   | 500         | Server configuration or internal errors             |
| `CategoryTransientFailure`  | 429         | Rate limiting                                       |

### S2S Token Exchange Errors (`POST /api/v1/token`)

| HTTP | Reason                  | Message                                                      | Cause                                    |
| ---- | ----------------------- | ------------------------------------------------------------ | ---------------------------------------- |
| 400  | `ErrorMissingField`     | "client id is required"                                      | Missing `client_id` in request           |
| 400  | `ErrorMissingField`     | "client secret is required"                                  | Missing `client_secret` in request       |
| 400  | `ErrorInvalidField`     | "Invalid value for grant type"                               | `grant_type` is not `client_credentials` |
| 401  | `ErrorPermissionDenied` | "Invalid client credentials"                                 | Unknown `client_id`                      |
| 401  | `ErrorPermissionDenied` | "Invalid client credentials"                                 | Incorrect `client_secret`                |
| 401  | `ErrorPermissionDenied` | "Client credentials access is not enabled for this account"  | No secret configured for client          |
| 401  | `ErrorNotSupported`     | "Client credentials are not supported for this company type" | Company type restriction                 |

### S2S Request Errors (Using S2S Token)

| HTTP | Reason                  | Message                                                              | Cause                                                   |
| ---- | ----------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- |
| 400  | `ErrorMissingField`     | "Missing Authorization Headers"                                      | No `Authorization` header                               |
| 400  | `ErrorInvalidField`     | "Bearer token is not in the correct format"                          | Token not in `Bearer <token>` format                    |
| 401  | `ErrorExpired`          | "Token is expired"                                                   | Token `exp` claim exceeded                              |
| 401  | `ErrorPermissionDenied` | "Token signature is not valid"                                       | Invalid HMAC signature                                  |
| 401  | `ErrorMissingField`     | "Client id claim is missing"                                         | Missing `azp` claim in token                            |
| 401  | `ErrorMissingField`     | "Company id claim is missing"                                        | Missing `company_id` claim                              |
| 400  | `ErrorMissingField`     | "X-Chain-Id header is missing"                                       | Required header not provided                            |
| 400  | `ErrorMissingField`     | "No valid user identifier provided"                                  | Missing `X-User-Wallet`, `X-User-Email`, or `X-User-Id` |
| 400  | `ErrorInvalidField`     | "Multiple user identifiers provided in headers. Only one is allowed" | More than one user identifier header                    |
| 400  | `ErrorNotFound`         | "Requested user was not found"                                       | User does not exist or not in your company              |

### Privy Token Exchange Errors

| HTTP | Reason                      | Message                                        | Cause                                |
| ---- | --------------------------- | ---------------------------------------------- | ------------------------------------ |
| 400  | `ErrorMissingField`         | "Missing Authorization Headers"                | No `Authorization` header            |
| 400  | `ErrorMissingField`         | "Missing Identity Headers"                     | No `Identity` header                 |
| 401  | `ErrorExpired`              | "Access token is expired"                      | Privy access token expired           |
| 401  | `ErrorExpired`              | "Id token is expired"                          | Privy identity token expired         |
| 401  | `ErrorPermissionDenied`     | "Access token signature is not valid"          | Invalid Privy signature              |
| 401  | `ErrorPermissionDenied`     | "Id token signature is not valid"              | Invalid Privy signature              |
| 401  | `ErrorMissingField`         | "Linked accounts claim is missing"             | No wallet linked in Privy            |
| 400  | `ErrorMissingField`         | "Not all identifiers found in linked accounts" | Email or wallet not in Privy account |
| 500  | `ErrorConfigurationInvalid` | "Client application not registered"            | Privy App ID not configured          |

### User Token Errors (Using Wirex User Token)

| HTTP | Reason                  | Message                         | Cause              |
| ---- | ----------------------- | ------------------------------- | ------------------ |
| 401  | `ErrorExpired`          | "Token is expired"              | User token expired |
| 401  | `ErrorPermissionDenied` | "Token signature is not valid"  | Invalid signature  |
| 401  | `ErrorMissingField`     | "User id claim is missing"      | Malformed token    |
| 401  | `ErrorMissingField`     | "User email claim is missing"   | Malformed token    |
| 401  | `ErrorMissingField`     | "User address claim is missing" | Malformed token    |

<br />