Authentication

Authentication methods for Wirex BaaS API integration.

Before You Start

Read the following guides before proceeding:

GuideWhy
Platform OverviewCredentials, environments, and onboarding requirements

Overview

Wirex supports three authentication patterns:

PatternUse CaseToken Source
Server-to-Server (S2S)Backend operations without user contextOAuth2 client credentials
User Token IssuanceDirect client-to-server communication using user-scoped tokenS2S obtained, user-scoped
Privy-basedFrontend/mobile apps with Privy wallet integrationPrivy SDK token exchange

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

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

Response

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

Token Characteristics

PropertyValue
AlgorithmHMAC-SHA256
Validity48 hours (172800 seconds)
Scopepartner: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:

{
  "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

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

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

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:

HeaderDescription
X-User-AddressUser's EOA address
X-User-EmailUser's email address
X-User-IdUser's UUID

You must provide at least one of these headers. For endpoints that support multiple identifiers, X-User-Id takes precedence.

User-Agnostic Endpoints

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

EndpointDescription
POST /api/v1/tokenS2S token exchange
POST /api/v1/userUser registration (V1)
POST /api/v2/userUser registration (V2)
GET /api/v1/configApplication configuration
GET /api/v1/validation/rulesValidation 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

HeaderDescription
AuthorizationBearer <s2s_token> from S2S auth
X-User-AddressUser's EOA address (not the Smart Wallet address)
X-Chain-IdTarget blockchain ID (137=Polygon, 8453=Base)

Response

{
  "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-Address      |                            |
      |------------------------->|                            |
      |                          |                            |
      |<-------------------------|                            |
      |    { user_token }        |                            |
      |                          |                            |
      | 3. Transfer token        |                            |
      |----------------------------------------------->       |
      |                          |                            |
      |                          | 4. Direct API calls        |
      |                          |<---------------------------|
      |                          |    Authorization: Bearer   |
      |                          |    <user_token>            |
      |                          |--------------------------->|
      |                          |                            |

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-Address': 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
// 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

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-Address": 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
)
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

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-Address", 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
}
// 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
  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:

HeaderDescription
AuthorizationBearer <privy_access_token>
IdentityBearer <privy_identity_token>
X-Chain-IdTarget blockchain ID

Token Exchange Endpoint

POST /api/v1/user/authorize

Response

{
  "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)

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

HeaderDescription
AuthorizationBearer <privy_access_token>
IdentityBearer <privy_identity_token>
X-Chain-IdTarget blockchain ID

Request Body

{
  "country": "US"
}

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

Response

{
  "id": "user-uuid"
}

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

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:

ClaimSourceDescription
subAccess tokenPrivy user ID
audAccess tokenPrivy App ID (must match configured)
linked_accountsIdentity tokenArray of linked wallets

The linked_accounts claim contains wallet information:

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

4. 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

5. Error Responses

Error Response Format

{
  "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

CategoryHTTP StatusDescription
CategoryValidationFailure400Invalid request data, missing fields, format errors
CategoryUnauthorized401Authentication or authorization failure
CategoryInternalFailure500Server configuration or internal errors
CategoryTransientFailure429Rate limiting

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

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

S2S Request Errors (Using S2S Token)

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

Privy Token Exchange Errors

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

User Token Errors (Using Wirex User Token)

HTTPReasonMessageCause
401ErrorExpired"Token is expired"User token expired
401ErrorPermissionDenied"Token signature is not valid"Invalid signature
401ErrorMissingField"User id claim is missing"Malformed token
401ErrorMissingField"User email claim is missing"Malformed token
401ErrorMissingField"User address claim is missing"Malformed token