Sharing Data Through SumSub
Share existing SumSub verification with Wirex for pre-verified users.
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
- 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):
| Field | Required |
|---|---|
| First name | Yes |
| Last name | Yes |
| Date of birth | Yes |
| Nationality | Yes |
| Phone | Yes |
| Country | Yes |
| State | No |
| City | Yes |
| Street | Yes |
| Street (line 2) | No |
| Building number | Yes |
| Postcode | Yes |
Documents
| Document | Required | Notes |
|---|---|---|
| Selfie | Yes | Liveness check |
| Proof of Identity (POI) | Yes | Request 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) | Yes | Must be issued within the last 6 months |
Additional Requirements
| Requirement | Value |
|---|---|
| User age | 18–90 years |
| POI document validity | More than 1 month left before the expiry date |
| POA document validity | 6 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.
| Section | Item | Type | Required | Options |
|---|---|---|---|---|
productsServicesRequired | productsServicesRequired | multiSelect | Yes | Card 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 |
employmentStatus | employmentStatus | select | Yes | Employed / Self Employed, Retired, Student / Not employed |
occupation | occupation | selectDropdown | Conditional — only when employmentStatus.employmentStatus = Employed / Self Employed | Agriculture / 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 |
actualIncomeValue | actualIncomeValue | select | Yes | Up to 25,000, 25,000 – 75,000, 75,000 – 150,000, Over 150,000 |
sourceOfFunds | sourceOfFunds | select | Yes | Employment / Salary, Business Income / Self-Employment, Savings / Existing Investments, Inheritance, Gift, Other |
annualTransactionVolume | annualTransactionVolume | select | Yes | Up to 25,000, 25,000 – 75,000, 75,000 – 150,000, Over 150,000 |
pepDeclaration | personalPepConfirmation | select | Yes | I am a PEP, I am not a PEP |
pepDeclaration | personalPepExplanation | textArea | Conditional — only when pepDeclaration.personalPepConfirmation = I am a PEP | — |
PEP declaration:
personalPepConfirmationalways requires an answer. SelectingI am a PEPmakespersonalPepExplanationrequired too; selectingI am not a PEPdoes 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:
- Endpoint: Generate Share Token
- Target client ID:
wirexapp.com_51921
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_addressreplaces the formerwallet_address. Both are accepted; if both are supplied they must match. New integrations should senduser_address.
| Field | Type | Required | Validation |
|---|---|---|---|
user_address | string | Yes | User's EOA address |
sharing_data.sharing_token | string | Yes | Valid SumSub share token for wirexapp.com_51921 |
sharing_data.email | string | Yes | Valid 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 progressApproved→ verification complete, user is activeRejected→ verification failed, contact Wirex support
See Webhooks for payload details.
What Happens Next
After registration with a valid share token:
- Wirex retrieves the user's verification data from SumSub using the share token
- Wirex validates the verification meets requirements
- Webhook notification sent with
verification_statusupdates - 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 becauseemploymentStatusisEmployed / Self Employed.personalPepExplanationis omitted becausepersonalPepConfirmationisI am not a PEP, so its condition is not met. A submission that skips any other item above is rejected — see Validation.
| Field | Type | Required | Description |
|---|---|---|---|
questionary_id | string | Yes | Questionnaire identifier — onboarding_survey_baas_v1 |
sections | array | Yes | At least one section |
sections[].section_id | string | Yes | Section identifier, from the table in Questionnaire (Survey) |
sections[].answers | array | Yes | At least one answer |
sections[].answers[].item_id | string | Yes | Item identifier within the section |
sections[].answers[].answers | array of string | Yes | One 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 whenpersonalPepConfirmationisI am a PEP. A submission missing a required answer is rejected before anything is forwarded; nothing is partially saved.
Error Handling
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | questionary_id missing | Supply the questionnaire identifier |
ErrorGeneral | Missing required questionary answer — error_details names section_id and item_id | The item is required unconditionally, or its triggering condition was met; supply it |
ErrorGeneral | `SubmitQuestionary is only supported for Trust | Full verification model` |
ErrorNotFound | Account not found | Register the user first |
ErrorGeneral | Account has no SumSub ID | User has not started verification yet |
ErrorGeneral | No level configured for questionary | Contact 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
| Field | Error | Pattern/Requirement |
|---|---|---|
user_address | Invalid format | ^0x[a-fA-F0-9]{40}$ (EVM) |
sharing_data.email | Invalid format | Valid email address |
sharing_data.email | Missing | Required field |
sharing_data.sharing_token | Missing | Required field |
| Mutual exclusivity | sharing_data and initial_data cannot both be provided |
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 |
Share Token Errors
| Error | Cause | Resolution |
|---|---|---|
| Invalid sharing token | Token expired or malformed | Generate a new token |
| Share token rejected | Target client ID mismatch | Ensure token is generated for wirexapp.com_51921 |
| Verification not found | User not verified in SumSub | Complete user verification first |
| Agreement not active | Data sharing agreement missing | Contact 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" }
]
}Updated 8 days ago

