Sharing Data Through SumSub

Share existing SumSub verification with Wirex for pre-verified users.

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

  • Your users are already KYC-verified via SumSub
  • You want to avoid duplicate verification for your users
  • You have a data sharing agreement with Wirex and SumSub

Approval Required: This flow is only available after a detailed audit by Wirex and may be rejected. Contact your Wirex account manager to discuss eligibility and requirements.


Setup Requirements

Before using this flow, ensure your SumSub integration can generate share tokens for external clients.


Required Verification Data

Wirex maps the user's profile from the SumSub applicant behind the share token. Make sure the applicant carries every item below before you generate the token. If something is missing, the user verification stays incomplete until the user uploads the missing data or documents manually.

Applicant Data

Fill these fields on the SumSub applicant (applicant info and address):

FieldRequired
First nameYes
Last nameYes
Date of birthYes
NationalityYes
PhoneYes
CountryYes
StateNo
CityYes
StreetYes
Street (line 2)No
Building numberYes
PostcodeYes

Documents

DocumentRequiredNotes
SelfieYesLiveness check
Proof of Identity (POI)YesRequest the list of accepted POI document types for each country from your Wirex account manager. The submitted document must have at least one month of validity remaining at the time of submission.
Proof of Address (POA)YesMust be issued within the last 6 months

Additional Requirements

RequirementValue
User age18–90 years
POI document validityMore than 1 month left before the expiry date
POA document validity6 months from the issue date

Questionnaire (Survey)

The applicant must have a completed SumSub questionnaire, onboarding_survey_baas_v1. Request it from your Wirex account manager and add it to your SumSub verification level — keep the section IDs, item IDs and option values unchanged, because Wirex reads the answers by value.

SectionItemTypeRequiredOptions
productsServicesRequiredproductsServicesRequiredmultiSelectYesCard purchases including restaurants, shopping, leisure, Send money to friends and family, Flights, hotels, car hire etc., Mortgage/rent payments etc. and ATM withdrawals, Traditionally investing with banks etc., Trading or investing in Crypto, including staking, mining & NFTs
employmentStatusemploymentStatusselectYesEmployed / Self Employed, Retired, Student / Not employed
occupationoccupationselectDropdownConditional — only when employmentStatus.employmentStatus = Employed / Self EmployedAgriculture / Natural Resources / Mining, Construction / Engineering, Education / Academic, Financial Services / Banking, Government / Public Sector / Defence, Healthcare / Medical / Pharmaceuticals, Hospitality / Travel / Tourism, Legal / Professional Services (Law, Accountancy, Consulting), Manufacturing / Industrial, Media / Entertainment / Arts, Real Estate / Property Development, Retail / E-Commerce, Technology / IT Services, Transportation / Logistics / Shipping, Other
actualIncomeValueactualIncomeValueselectYesUp to 25,000, 25,000 – 75,000, 75,000 – 150,000, Over 150,000
sourceOfFundssourceOfFundsselectYesEmployment / Salary, Business Income / Self-Employment, Savings / Existing Investments, Inheritance, Gift, Other
annualTransactionVolumeannualTransactionVolumeselectYesUp to 25,000, 25,000 – 75,000, 75,000 – 150,000, Over 150,000
pepDeclarationpersonalPepConfirmationselectYesI am a PEP, I am not a PEP
pepDeclarationpersonalPepExplanationtextAreaConditional — only when pepDeclaration.personalPepConfirmation = I am a PEP—

PEP declaration: personalPepConfirmation always requires an answer. Selecting I am a PEP makes personalPepExplanation required too; selecting I am not a PEP does not require it. This is the one item on the questionnaire whose requirement depends on another answer.

Instead of pre-filling this questionnaire on the SumSub applicant, you can submit it through the Wirex API after the user is registered — see Submitting the Questionnaire via API.


Generating a Share Token

Use the SumSub API to generate a share token for each user:

The share token allows Wirex to access the user's verification data from SumSub.


Request

POST /api/v2/user
{
  "user_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
  "sharing_data": {
    "sharing_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "email": "[email protected]"
  }
}

Field Requirements

Important: The user_address must be the user's EOA address (the signer), not the Smart Wallet address.

user_address replaces the former wallet_address. Both are accepted; if both are supplied they must match. New integrations should send user_address.

FieldTypeRequiredValidation
user_addressstringYesUser's EOA address
sharing_data.sharing_tokenstringYesValid SumSub share token for wirexapp.com_51921
sharing_data.emailstringYesValid email format, must be unique

Phone Number: The phone number from the user's SumSub verification is automatically marked as confirmed. It will be used for 3DS Authentication OTP codes and Read Card Details OTP verification. No separate confirmation step is required.


Code Examples

async function registerUserSharedKYC(accessToken, userAddress, sharingToken, email, 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({
      user_address: userAddress,
      sharing_data: { sharing_token: sharingToken, email },
    }),
  });

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

// Usage
const sharingToken = await generateSumSubShareToken(userId, 'wirexapp.com_51921');
const { id } = await registerUserSharedKYC(
  access_token,
  '0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE',
  sharingToken,
  '[email protected]',
  8453,
  'https://api-baas.wirexapp.com'
);
import requests

def register_user_shared_kyc(access_token, user_address, sharing_token, email, 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={
            "user_address": user_address,
            "sharing_data": {"sharing_token": sharing_token, "email": email},
        },
    )
    response.raise_for_status()
    return response.json()

# Usage
sharing_token = generate_sumsub_share_token(user_id, "wirexapp.com_51921")
user = register_user_shared_kyc(
    access_token,
    "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
    sharing_token,
    "[email protected]",
    8453,
    "https://api-baas.wirexapp.com"
)
func RegisterUserSharedKYC(accessToken, userAddress, sharingToken, email string, chainID int, baseURL string) (string, error) {
    reqBody, _ := json.Marshal(map[string]interface{}{
        "user_address": userAddress,
        "sharing_data":   map[string]string{"sharing_token": sharingToken, "email": email},
    })

    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 Backend as Your Backend
    participant SumSub as SumSub API
    participant BaaS as Wirex API

    Backend->>SumSub: 1. Generate share token for wirexapp.com_51921
    SumSub-->>Backend: { sharing_token }

    Backend->>BaaS: 2. POST /api/v2/user { user_address, sharing_data }

    BaaS->>SumSub: 3. Fetch verification
    SumSub-->>BaaS: { user_data }

    Note over BaaS: 4. Validate & create account

    BaaS-->>Backend: { id }

    BaaS->>Backend: 5. POST /webhook/users (verification_status)

Webhooks

Wirex sends webhook notifications when verification status changes.

Note: This is a legacy webhook format that will be replaced with a v2 model in a future release.

Endpoint: POST {your_webhook_base_url}/webhook/users

You will receive status updates as verification progresses:

  • Applied → data received from SumSub, validation in progress
  • Approved → verification complete, user is active
  • Rejected → verification failed, contact Wirex support

See Webhooks for payload details.


What Happens Next

After registration with a valid share token:

  1. Wirex retrieves the user's verification data from SumSub using the share token
  2. Wirex validates the verification meets requirements
  3. Webhook notification sent with verification_status updates
  4. User account is activated upon approval

The user can perform all operations once verification is approved.


Submitting the Questionnaire via API

Once the user is registered, submit the onboarding_survey_baas_v1 answers through the Wirex API instead of pre-filling them on the SumSub applicant.

If the user has already completed SDD, submitting the questionnaire also moves them onto the verification level that hosts it, and a webhook with the updated verification_status follows. If SDD is not complete yet, the answers are recorded against the user's current level and verification_status does not change.

Existing users: users registered before this endpoint was available have no questionnaire answers on file. Call this endpoint for each of them to provide the answers Wirex did not collect at registration.

POST /api/v2/user/questionary
{
  "questionary_id": "onboarding_survey_baas_v1",
  "sections": [
    {
      "section_id": "productsServicesRequired",
      "answers": [
        { "item_id": "productsServicesRequired", "answers": ["Send money to friends and family"] }
      ]
    },
    {
      "section_id": "employmentStatus",
      "answers": [
        { "item_id": "employmentStatus", "answers": ["Employed / Self Employed"] }
      ]
    },
    {
      "section_id": "occupation",
      "answers": [
        { "item_id": "occupation", "answers": ["Technology / IT Services"] }
      ]
    },
    {
      "section_id": "actualIncomeValue",
      "answers": [
        { "item_id": "actualIncomeValue", "answers": ["25,000 – 75,000"] }
      ]
    },
    {
      "section_id": "sourceOfFunds",
      "answers": [
        { "item_id": "sourceOfFunds", "answers": ["Employment / Salary"] }
      ]
    },
    {
      "section_id": "annualTransactionVolume",
      "answers": [
        { "item_id": "annualTransactionVolume", "answers": ["25,000 – 75,000"] }
      ]
    },
    {
      "section_id": "pepDeclaration",
      "answers": [
        { "item_id": "personalPepConfirmation", "answers": ["I am not a PEP"] }
      ]
    }
  ]
}

This example answers every item that is unconditionally required, plus occupation — required here because employmentStatus is Employed / Self Employed. personalPepExplanation is omitted because personalPepConfirmation is I am not a PEP, so its condition is not met. A submission that skips any other item above is rejected — see Validation.

FieldTypeRequiredDescription
questionary_idstringYesQuestionnaire identifier — onboarding_survey_baas_v1
sectionsarrayYesAt least one section
sections[].section_idstringYesSection identifier, from the table in Questionnaire (Survey)
sections[].answersarrayYesAt least one answer
sections[].answers[].item_idstringYesItem identifier within the section
sections[].answers[].answersarray of stringYesOne value for a single-choice item (select, selectDropdown), several for multiSelect

When personalPepConfirmation is I am a PEP, personalPepExplanation becomes required. Its pepDeclaration section then carries both answers:

{
  "section_id": "pepDeclaration",
  "answers": [
    { "item_id": "personalPepConfirmation", "answers": ["I am a PEP"] },
    { "item_id": "personalPepExplanation", "answers": ["Member of the regional legislative assembly"] }
  ]
}

Response: {}

Validation: Wirex checks every submitted section against the questionnaire definition, including items that are only required conditionally — such as personalPepExplanation, required only when personalPepConfirmation is I am a PEP. A submission missing a required answer is rejected before anything is forwarded; nothing is partially saved.

Error Handling

Error ReasonDescriptionResolution
ErrorGeneralquestionary_id missingSupply the questionnaire identifier
ErrorGeneralMissing required questionary answer — error_details names section_id and item_idThe item is required unconditionally, or its triggering condition was met; supply it
ErrorGeneral`SubmitQuestionary is only supported for TrustFull verification model`
ErrorNotFoundAccount not foundRegister the user first
ErrorGeneralAccount has no SumSub IDUser has not started verification yet
ErrorGeneralNo level configured for questionaryContact your Wirex account manager
{
  "error_reason": "ErrorGeneral",
  "error_description": "Missing required questionary answer",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    { "key": "section_id", "details": "pepDeclaration" },
    { "key": "item_id", "details": "personalPepExplanation" }
  ]
}

Code Examples

async function submitQuestionary(accessToken, baseUrl) {
  const response = await fetch(`${baseUrl}/api/v2/user/questionary`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      questionary_id: 'onboarding_survey_baas_v1',
      sections: [
        {
          section_id: 'productsServicesRequired',
          answers: [{ item_id: 'productsServicesRequired', answers: ['Send money to friends and family'] }],
        },
        {
          section_id: 'employmentStatus',
          answers: [{ item_id: 'employmentStatus', answers: ['Employed / Self Employed'] }],
        },
        {
          section_id: 'occupation',
          answers: [{ item_id: 'occupation', answers: ['Technology / IT Services'] }],
        },
        {
          section_id: 'actualIncomeValue',
          answers: [{ item_id: 'actualIncomeValue', answers: ['25,000 – 75,000'] }],
        },
        {
          section_id: 'sourceOfFunds',
          answers: [{ item_id: 'sourceOfFunds', answers: ['Employment / Salary'] }],
        },
        {
          section_id: 'annualTransactionVolume',
          answers: [{ item_id: 'annualTransactionVolume', answers: ['25,000 – 75,000'] }],
        },
        {
          section_id: 'pepDeclaration',
          answers: [{ item_id: 'personalPepConfirmation', answers: ['I am not a PEP'] }],
        },
      ],
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Questionary submission failed: ${error.error_description}`);
  }
}
import requests

def submit_questionary(access_token, base_url):
    response = requests.post(
        f"{base_url}/api/v2/user/questionary",
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        },
        json={
            "questionary_id": "onboarding_survey_baas_v1",
            "sections": [
                {
                    "section_id": "productsServicesRequired",
                    "answers": [{"item_id": "productsServicesRequired", "answers": ["Send money to friends and family"]}],
                },
                {
                    "section_id": "employmentStatus",
                    "answers": [{"item_id": "employmentStatus", "answers": ["Employed / Self Employed"]}],
                },
                {
                    "section_id": "occupation",
                    "answers": [{"item_id": "occupation", "answers": ["Technology / IT Services"]}],
                },
                {
                    "section_id": "actualIncomeValue",
                    "answers": [{"item_id": "actualIncomeValue", "answers": ["25,000 – 75,000"]}],
                },
                {
                    "section_id": "sourceOfFunds",
                    "answers": [{"item_id": "sourceOfFunds", "answers": ["Employment / Salary"]}],
                },
                {
                    "section_id": "annualTransactionVolume",
                    "answers": [{"item_id": "annualTransactionVolume", "answers": ["25,000 – 75,000"]}],
                },
                {
                    "section_id": "pepDeclaration",
                    "answers": [{"item_id": "personalPepConfirmation", "answers": ["I am not a PEP"]}],
                },
            ],
        },
    )
    response.raise_for_status()
func SubmitQuestionary(accessToken, baseURL string) error {
    reqBody, _ := json.Marshal(map[string]interface{}{
        "questionary_id": "onboarding_survey_baas_v1",
        "sections": []map[string]interface{}{
            {
                "section_id": "productsServicesRequired",
                "answers": []map[string]interface{}{
                    {"item_id": "productsServicesRequired", "answers": []string{"Send money to friends and family"}},
                },
            },
            {
                "section_id": "employmentStatus",
                "answers": []map[string]interface{}{
                    {"item_id": "employmentStatus", "answers": []string{"Employed / Self Employed"}},
                },
            },
            {
                "section_id": "occupation",
                "answers": []map[string]interface{}{
                    {"item_id": "occupation", "answers": []string{"Technology / IT Services"}},
                },
            },
            {
                "section_id": "actualIncomeValue",
                "answers": []map[string]interface{}{
                    {"item_id": "actualIncomeValue", "answers": []string{"25,000 – 75,000"}},
                },
            },
            {
                "section_id": "sourceOfFunds",
                "answers": []map[string]interface{}{
                    {"item_id": "sourceOfFunds", "answers": []string{"Employment / Salary"}},
                },
            },
            {
                "section_id": "annualTransactionVolume",
                "answers": []map[string]interface{}{
                    {"item_id": "annualTransactionVolume", "answers": []string{"25,000 – 75,000"}},
                },
            },
            {
                "section_id": "pepDeclaration",
                "answers": []map[string]interface{}{
                    {"item_id": "personalPepConfirmation", "answers": []string{"I am not a PEP"}},
                },
            },
        },
    })

    req, _ := http.NewRequest("POST", baseURL+"/api/v2/user/questionary", bytes.NewBuffer(reqBody))
    req.Header.Set("Authorization", "Bearer "+accessToken)
    req.Header.Set("Content-Type", "application/json")

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

    return nil
}

Error Handling

Validation Errors

FieldErrorPattern/Requirement
user_addressInvalid format^0x[a-fA-F0-9]{40}$ (EVM)
sharing_data.emailInvalid formatValid email address
sharing_data.emailMissingRequired field
sharing_data.sharing_tokenMissingRequired field
Mutual exclusivitysharing_data and initial_data cannot both be provided

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

Share Token Errors

ErrorCauseResolution
Invalid sharing tokenToken expired or malformedGenerate a new token
Share token rejectedTarget client ID mismatchEnsure token is generated for wirexapp.com_51921
Verification not foundUser not verified in SumSubComplete user verification first
Agreement not activeData sharing agreement missingContact Wirex account manager

Error Response Format

{
  "error_reason": "ErrorInvalidField",
  "error_description": "Invalid sharing token",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    { "key": "field", "details": "sharing_token" },
    { "key": "issue", "details": "invalid_format" }
  ]
}

Did this page help you?