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:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain access tokens
WebhooksReceive status notifications
OnboardingUser and wallet registration
User ProfileUser 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:

MethodEndpointAuth Flow
S2S RegistrationPOST /api/v2/userS2S token (backend-to-backend)
Privy RegistrationPOST /api/v1/user/retailPrivy-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.

FieldTypeRequiredValidation
wallet_addressstringYesUser's EOA address
initial_data.profile.emailstringYesValid email format, must be unique
initial_data.residence_address.countrystringYesISO 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"
}
FieldTypeRequiredValidation
emailstringYesValid email format, must be unique
countrystringYesISO 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:

OptionEndpointUse Case
Verification LinkGET /api/v1/user/verification-linkRedirect user to hosted SumSub widget
SDK TokenGET /api/v1/user/verification-tokenEmbed 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

  1. User completes identity verification (document upload, selfie, etc.)
  2. SumSub sends verification result to Wirex
  3. Wirex sends a webhook to your endpoint with updated verification_status
  4. 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:

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

Step 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

FieldErrorPattern/Requirement
wallet_addressInvalid format^0x[a-fA-F0-9]{40}$ (EVM)
emailInvalid formatValid email address
emailMissingRequired field
countryInvalid formatISO 3166-1 alpha-2 (^[A-Z]{2}$)
countryMissingRequired field

Registration Errors

Error ReasonDescriptionResolution
ErrorNotFoundWallet not registered in Accounts contractComplete on-chain registration first
ErrorAlreadyExistsEmail already existsUse a different email address
ErrorAlreadyExistsWallet address already existsUser 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" }
  ]
}