Wirex Hosted KYC
Collect basic user data and let users complete KYC verification through Wirex's hosted flow.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain access tokens |
| Webhooks | Receive status notifications |
| Onboarding | User and wallet registration |
| User Profile | User capabilities and status |
When to Use
- You want Wirex to handle KYC verification
- You collect minimal user data at registration
- Users will complete identity verification in the Wirex interface
Registration Methods
Two registration endpoints are available depending on your authentication flow:
| Method | Endpoint | Auth Flow |
|---|---|---|
| S2S Registration | POST /api/v2/user | S2S token (backend-to-backend) |
| Privy Registration | POST /api/v1/user/retail | Privy-based auth (frontend) |
Option A: S2S Registration
For backend-to-backend integration using S2S tokens.
POST /api/v2/user
{
"wallet_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"initial_data": {
"profile": {
"email": "[email protected]"
},
"residence_address": {
"country": "GB"
}
}
}Important: The wallet_address must be the user's EOA address (the signer), not the Smart Wallet address.
| Field | Type | Required | Validation |
|---|---|---|---|
wallet_address | string | Yes | User's EOA address |
initial_data.profile.email | string | Yes | Valid email format, must be unique |
initial_data.residence_address.country | string | Yes | ISO 3166-1 alpha-2 code |
Option B: Privy Registration
For partners using Privy authorization flow. The user context is derived from the Privy token.
POST /api/v1/user/retail
{
"email": "[email protected]",
"country": "GB"
}| Field | Type | Required | Validation |
|---|---|---|---|
email | string | Yes | Valid email format, must be unique |
country | string | Yes | ISO 3166-1 alpha-2 code |
Code Examples (S2S Registration)
async function registerUserHostedKYC(accessToken, walletAddress, email, country, chainId, baseUrl) {
const response = await fetch(`${baseUrl}/api/v2/user`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Chain-Id': chainId.toString(),
},
body: JSON.stringify({
wallet_address: walletAddress,
initial_data: {
profile: { email },
residence_address: { country },
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Registration failed: ${error.error_description}`);
}
return response.json();
}
// Usage
const { id } = await registerUserHostedKYC(
access_token,
'0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE',
'[email protected]',
'GB',
8453,
'https://api-baas.wirexapp.com'
);import requests
def register_user_hosted_kyc(access_token, wallet_address, email, country, chain_id, base_url):
response = requests.post(
f"{base_url}/api/v2/user",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"X-Chain-Id": str(chain_id),
},
json={
"wallet_address": wallet_address,
"initial_data": {
"profile": {"email": email},
"residence_address": {"country": country},
},
},
)
response.raise_for_status()
return response.json()
# Usage
user = register_user_hosted_kyc(
access_token,
"0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"[email protected]",
"GB",
8453,
"https://api-baas.wirexapp.com"
)func RegisterUserHostedKYC(accessToken, walletAddress, email, country string, chainID int, baseURL string) (string, error) {
reqBody, _ := json.Marshal(map[string]interface{}{
"wallet_address": walletAddress,
"initial_data": map[string]interface{}{
"profile": map[string]string{"email": email},
"residence_address": map[string]string{"country": country},
},
})
req, _ := http.NewRequest("POST", baseURL+"/api/v2/user", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Chain-Id", fmt.Sprintf("%d", chainID))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct{ ID string `json:"id"` }
json.NewDecoder(resp.Body).Decode(&result)
return result.ID, nil
}Flow
sequenceDiagram
participant App as Your App
participant BaaS as Wirex API
participant SumSub
App->>BaaS: 1. POST /api/v2/user or /api/v1/user/retail
BaaS-->>App: { id }
App->>BaaS: 2. GET /verification-link or /verification-token
BaaS-->>App: { url } or { token }
App->>SumSub: 3. User completes KYC (redirect or SDK)
SumSub->>BaaS: 4. Verification result
Note over BaaS: 5. Account activated
BaaS->>App: POST /webhook/users (verification_status)
KYC Completion Options
After registration, the user must complete KYC verification. Two options are available:
| Option | Endpoint | Use Case |
|---|---|---|
| Verification Link | GET /api/v1/user/verification-link | Redirect user to hosted SumSub widget |
| SDK Token | GET /api/v1/user/verification-token | Embed SumSub SDK in your app |
Option A: Verification Link
Redirect the user to a hosted SumSub widget where they complete all KYC steps.
GET /api/v1/user/verification-link
Response:
{
"url": "https://verify.sumsub.com/..."
}Open this URL in a browser or webview. The user completes verification in the hosted interface.
Option B: SDK Token
Obtain a token to initialize the SumSub SDK within your application.
GET /api/v1/user/verification-token
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Use this token to initialize the SumSub WebSDK or Mobile SDK in your frontend. For integration details, see the SumSub SDK documentation.
What Happens Next
- User completes identity verification (document upload, selfie, etc.)
- SumSub sends verification result to Wirex
- Wirex sends a webhook to your endpoint with updated
verification_status - User account is activated upon successful verification
The user cannot perform restricted operations (card issuance, fiat withdrawals) until KYC is approved.
For webhook payload details, see User Onboarding - Webhooks.
Phone Number Confirmation
A confirmed phone number is required for:
- 3DS Authentication — OTP codes for card transaction verification
- Read Card Details — OTP-based retrieval of PAN, CVV, and PIN
Since Wirex hosted onboarding does not collect a phone number, users must set and confirm their phone number separately.
Step 1: Set Phone Number
PUT /api/v1/user/phone-number
await fetch(`${baseUrl}/api/v1/user/phone-number`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
phone_number: '447700900123' // Without leading +
})
});requests.put(
f"{base_url}/api/v1/user/phone-number",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
},
json={"phone_number": "447700900123"} # Without leading +
)body, _ := json.Marshal(map[string]string{"phone_number": "447700900123"}) // Without leading +
req, _ := http.NewRequest("PUT", baseURL+"/api/v1/user/phone-number", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)Step 2: Request SMS Code
POST /api/v1/confirmation/sms
const response = await fetch(`${baseUrl}/api/v1/confirmation/sms`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
action_type: 'VerifyPhone'
})
});
const { session_id, code_length, expires_at } = await response.json();response = requests.post(
f"{base_url}/api/v1/confirmation/sms",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
},
json={"action_type": "VerifyPhone"}
)
session = response.json()
session_id = session["session_id"]body, _ := json.Marshal(map[string]string{"action_type": "VerifyPhone"})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/confirmation/sms", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var session struct{ SessionID string `json:"session_id"` }
json.NewDecoder(resp.Body).Decode(&session)Step 3: Verify SMS Code
POST /api/v1/confirmation/sms/verify
const response = await fetch(`${baseUrl}/api/v1/confirmation/sms/verify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: sessionId,
code: userEnteredCode
})
});
const { token: actionToken } = await response.json();response = requests.post(
f"{base_url}/api/v1/confirmation/sms/verify",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
},
json={"session_id": session_id, "code": user_entered_code}
)
action_token = response.json()["token"]body, _ := json.Marshal(map[string]string{"session_id": sessionID, "code": userEnteredCode})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/confirmation/sms/verify", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result struct{ Token string `json:"token"` }
json.NewDecoder(resp.Body).Decode(&result)
actionToken := result.TokenStep 4: Confirm Phone Number
PUT /api/v1/user/phone-number/confirm
await fetch(`${baseUrl}/api/v1/user/phone-number/confirm`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Address': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
action_token: actionToken
})
});requests.put(
f"{base_url}/api/v1/user/phone-number/confirm",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Address": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
},
json={"action_token": action_token}
)body, _ := json.Marshal(map[string]string{"action_token": actionToken})
req, _ := http.NewRequest("PUT", baseURL+"/api/v1/user/phone-number/confirm", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)The phone number is now confirmed and can be used for 3DS challenges and card details retrieval via OTP.
Error Handling
Validation Errors
| Field | Error | Pattern/Requirement |
|---|---|---|
wallet_address | Invalid format | ^0x[a-fA-F0-9]{40}$ (EVM) |
email | Invalid format | Valid email address |
email | Missing | Required field |
country | Invalid format | ISO 3166-1 alpha-2 (^[A-Z]{2}$) |
country | Missing | Required field |
Registration Errors
| Error Reason | Description | Resolution |
|---|---|---|
ErrorNotFound | Wallet not registered in Accounts contract | Complete on-chain registration first |
ErrorAlreadyExists | Email already exists | Use a different email address |
ErrorAlreadyExists | Wallet address already exists | User already registered |
Error Response Format
{
"error_reason": "ErrorInvalidField",
"error_description": "Invalid format for email",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "email" },
{ "key": "issue", "details": "invalid_format" }
]
}Updated 8 days ago
