KYC Sharing via API
Provide complete verified user data to Wirex via API when you handle KYC verification.
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 perform full KYC verification on your platform
- You want to provide complete user data at registration
- Users should be immediately active without additional verification
Approval Required: KYC sharing via API is only available under the Enterprise model, subject to a detailed audit by Wirex, and may be rejected. Contact your Wirex account manager to discuss eligibility and requirements.
Request
POST /api/v2/user
{
"user_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"initial_data": {
"profile": {
"first_name": "Alex",
"last_name": "Grey",
"date_of_birth": "1990-01-15",
"nationality": "GB",
"email": "[email protected]",
"phone_number": "+447700900123"
},
"residence_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"post_code": "SW1A 2AA",
"country": "GB"
}
}
}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.
Profile Fields
| Field | Type | Required | Validation |
|---|---|---|---|
user_address | string | Yes | User's EOA address |
initial_data.profile.email | string | Yes | Valid email format, must be unique |
initial_data.profile.first_name | string | Yes | Alphabetic characters |
initial_data.profile.last_name | string | Yes | Alphabetic characters |
initial_data.profile.date_of_birth | string | Yes | YYYY-MM-DD format |
initial_data.profile.nationality | string | Yes | ISO 3166-1 alpha-2 code |
initial_data.profile.phone_number | string | Yes | E.164 format (e.g., +447700900123) |
Phone Number: The phone number provided during KYC sharing via API 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.
Address Fields
| Field | Type | Required | Validation |
|---|---|---|---|
initial_data.residence_address.line1 | string | Yes | Street address |
initial_data.residence_address.line2 | string | No | Apartment, suite, etc. |
initial_data.residence_address.city | string | Yes | City name |
initial_data.residence_address.state | string | No | State or province |
initial_data.residence_address.post_code | string | Yes | Postal code |
initial_data.residence_address.country | string | Yes | ISO 3166-1 alpha-2 code |
Code Examples
async function registerUserKycSharing(accessToken, userAddress, profile, address, 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,
initial_data: { profile, residence_address: address },
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Registration failed: ${error.error_description}`);
}
return response.json();
}
// Usage
const profile = {
first_name: 'Alex', last_name: 'Grey', date_of_birth: '1990-01-15',
nationality: 'GB', email: '[email protected]', phone_number: '+447700900123',
};
const address = {
line1: '10 Downing Street', line2: 'Flat 2', city: 'London',
post_code: 'SW1A 2AA', country: 'GB',
};
const { id } = await registerUserKycSharing(
access_token, '0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE',
profile, address, 8453, 'https://api-baas.wirexapp.com'
);import requests
def register_user_kyc_sharing(access_token, user_address, profile, residence_address, 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,
"initial_data": {"profile": profile, "residence_address": residence_address},
},
)
response.raise_for_status()
return response.json()
# Usage
profile = {
"first_name": "Alex", "last_name": "Grey", "date_of_birth": "1990-01-15",
"nationality": "GB", "email": "[email protected]", "phone_number": "+447700900123",
}
address = {
"line1": "10 Downing Street", "line2": "Flat 2", "city": "London",
"post_code": "SW1A 2AA", "country": "GB",
}
user = register_user_kyc_sharing(
access_token, "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
profile, address, 8453, "https://api-baas.wirexapp.com"
)func RegisterUserKycSharing(accessToken, userAddress string, profile, address map[string]string, chainID int, baseURL string) (string, error) {
reqBody, _ := json.Marshal(map[string]interface{}{
"user_address": userAddress,
"initial_data": map[string]interface{}{
"profile": profile, "residence_address": address,
},
})
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
}Document Upload
After user registration, you must upload verification documents.
POST /api/v1/user/documents
Required Documents
| Document Type | Description |
|---|---|
selfie | User's selfie photo |
poi | Proof of Identity (passport, national ID, driver's license) |
poa | Proof of Address (utility bill, bank statement) |
All documents must be uploaded before the user account can be fully activated.
Flow
sequenceDiagram
participant Backend as Your Backend
participant BaaS as Wirex API
Backend->>BaaS: 1. POST /api/v2/user { user_address, initial_data }
BaaS-->>Backend: { id }
Backend->>BaaS: 2. POST /api/v1/user/documents (selfie, poi, poa)
Note over BaaS: 3. Validate data & docs
Note over BaaS: 4. Activate account
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→ documents received, validation in progressApproved→ verification complete, user is activeRejected→ verification failed, contact Wirex support
See Webhooks for payload details.
What Happens Next
After registration and document upload:
- Wirex validates the provided data and documents
- User record is created with the provided profile and address
- Webhook notification sent with
verification_statusupdates - User account is activated upon approval
The user can perform all operations once verification is approved.
Your Responsibilities
When using KYC sharing via API, you are responsible for:
- Performing identity verification to regulatory standards
- Maintaining verification records and audit trails
- Ensuring data accuracy and currency
- Complying with applicable KYC/AML regulations
Correcting KYC Data
Updates the KYC profile data already on file — personal profile, residence address and PEP status.
Use it to fix data the user mistyped before verification completes, not to re-verify.
PUT /api/v2/user/kyc-data
Not available under the Wirex Hosted verification model. Companies on that model correct data
through the hosted flow — see Wirex Hosted KYC.
Request
{
"profile": {
"first_name": "John",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"nationality": "US",
"phone_number": "+14155551234"
},
"residence_address": {
"line1": "123 Main Street",
"line2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"post_code": "94105",
"country": "US"
},
"is_pep": false
}| Field | Type | Required | Description |
|---|---|---|---|
profile | object | Conditional | At least one of profile or residence_address is required |
profile.first_name | string | No | Given name |
profile.last_name | string | No | Family name |
profile.date_of_birth | string | No | YYYY-MM-DD. Must not be empty when present |
profile.nationality | string | No | ISO 3166-1 alpha-2 country code |
profile.phone_number | string | No | Must include the country code |
residence_address | object | Conditional | At least one of profile or residence_address is required |
residence_address.line1 | string | No | Street address |
residence_address.line2 | string | No | Additional address line |
residence_address.city | string | No | City |
residence_address.state | string | No | State or region |
residence_address.post_code | string | No | Postal code |
residence_address.country | string | No | ISO 3166-1 alpha-2 country code |
is_pep | boolean | No | Politically exposed person status |
Every field is individually optional, but a request carrying neither profile nor
residence_address is refused with Request body: At least one of 'profile' or 'residence_address' must be provided. Sending only is_pep is therefore not a valid update.
Field patterns are the shared validation regexes listed in
Validation Rules; the error message repeats the pattern that failed.
Response
{}Submitting the Questionnaire via API
Once the user is registered, submit the onboarding_survey_baas_v1 questionnaire through the Wirex API. This flow has no SumSub applicant for you to pre-fill — submit the answers directly to Wirex once the user has completed verification. The full section/item/option table is in Questionnaire (Survey); section IDs, item IDs and option values are identical across flows.
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 Questionnaire Errors.
| 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.
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
Profile Validation Errors
| Field | Error | Pattern/Requirement |
|---|---|---|
user_address | Invalid format | ^0x[a-fA-F0-9]{40}$ (EVM) |
first_name | Invalid format | ^[a-zA-Z][a-zA-Z\-' ]{1,50}$ |
last_name | Invalid format | ^[a-zA-Z][a-zA-Z\-' ]{1,50}$ |
date_of_birth | Invalid format | YYYY-MM-DD |
nationality | Invalid format | ISO 3166-1 alpha-2 (^[A-Z]{2}$) |
email | Invalid format | Valid email address |
phone_number | Invalid format | E.164 format (^\+?\d{1,15}$) |
Address Validation Errors
| Field | Error | Pattern/Requirement |
|---|---|---|
line1 | Invalid format | ^[A-Za-z0-9&.,''\-/() :+#]{2,100}$ |
line2 | Invalid format | ^[A-Za-z0-9&.,''\-/() :+#]{2,100}$ |
city | Invalid format | ^[A-Za-z\s\-'.]{2,100}$ |
state | Invalid format | ^[A-Za-z][A-Za-z\s\-']{1,99}$ |
post_code | Invalid format | ^[A-Za-z0-9\s\-]{3,12}$ |
country | Invalid format | ISO 3166-1 alpha-2 (^[A-Z]{2}$) |
Document Upload Errors
| Field | Error | Valid Values |
|---|---|---|
document | Missing | File is required |
document_type | Invalid | selfie, identity_card, passport, driver_license, proof_of_address, other |
document_country | Invalid | ISO 3166-1 alpha-2 |
side | Invalid | front, back, none |
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 |
Questionnaire Errors
| 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" }
]
}Error Response Format
{
"error_reason": "ErrorInvalidField",
"error_description": "Invalid format for phone_number",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "initial_data.profile.phone_number" },
{ "key": "issue", "details": "invalid_format" }
]
}Updated 8 days ago

