Authentication
Authentication methods for Wirex BaaS API integration.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Platform Overview | 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.
Naming across trees. The Corporate API calls these the partner token and the corporation
token. Same two-step shape, different names — see
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
{
"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
| 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:
{
"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:
| 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
- Your backend authenticates with S2S credentials
- Your backend calls
POST /api/v1/user/authorizewith user identity headers - Wirex returns a user-specific access token
- You transfer this token to the client application
- 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
{
"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
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// 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-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
)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-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
}// 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
- Set up a Privy application at privy.io
- Provide your Privy App ID to Wirex during onboarding
- Wirex configures the Privy App ID in your company credentials
How It Works
- User authenticates via Privy SDK in your application
- Privy issues an access token and identity token
- Your app calls
POST /api/v1/user/authorizewith Privy tokens - Wirex validates Privy tokens against Privy's JWKS endpoint
- Wirex returns a Wirex-issued access token
- 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
{
"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
| Header | Description |
|---|---|
Authorization | Bearer <privy_access_token> |
Identity | Bearer <privy_identity_token> |
X-Chain-Id | Target 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:
| 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:
{
"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 for Option C: Direct Deployment (Stellar with Crossmint SDK) and the 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_secretin 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
{
"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)
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 |
Updated 20 days ago

