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:

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 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_address replaces the former wallet_address. Both are accepted; if both are supplied they must match. New integrations should send user_address.

Profile Fields

FieldTypeRequiredValidation
user_addressstringYesUser's EOA address
initial_data.profile.emailstringYesValid email format, must be unique
initial_data.profile.first_namestringYesAlphabetic characters
initial_data.profile.last_namestringYesAlphabetic characters
initial_data.profile.date_of_birthstringYesYYYY-MM-DD format
initial_data.profile.nationalitystringYesISO 3166-1 alpha-2 code
initial_data.profile.phone_numberstringYesE.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

FieldTypeRequiredValidation
initial_data.residence_address.line1stringYesStreet address
initial_data.residence_address.line2stringNoApartment, suite, etc.
initial_data.residence_address.citystringYesCity name
initial_data.residence_address.statestringNoState or province
initial_data.residence_address.post_codestringYesPostal code
initial_data.residence_address.countrystringYesISO 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 TypeDescription
selfieUser's selfie photo
poiProof of Identity (passport, national ID, driver's license)
poaProof 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 progress
  • Approved → verification complete, user is active
  • Rejected → verification failed, contact Wirex support

See Webhooks for payload details.


What Happens Next

After registration and document upload:

  1. Wirex validates the provided data and documents
  2. User record is created with the provided profile and address
  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.


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
}
FieldTypeRequiredDescription
profileobjectConditionalAt least one of profile or residence_address is required
profile.first_namestringNoGiven name
profile.last_namestringNoFamily name
profile.date_of_birthstringNoYYYY-MM-DD. Must not be empty when present
profile.nationalitystringNoISO 3166-1 alpha-2 country code
profile.phone_numberstringNoMust include the country code
residence_addressobjectConditionalAt least one of profile or residence_address is required
residence_address.line1stringNoStreet address
residence_address.line2stringNoAdditional address line
residence_address.citystringNoCity
residence_address.statestringNoState or region
residence_address.post_codestringNoPostal code
residence_address.countrystringNoISO 3166-1 alpha-2 country code
is_pepbooleanNoPolitically 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 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 Questionnaire Errors.

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.

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

FieldErrorPattern/Requirement
user_addressInvalid format^0x[a-fA-F0-9]{40}$ (EVM)
first_nameInvalid format^[a-zA-Z][a-zA-Z\-' ]{1,50}$
last_nameInvalid format^[a-zA-Z][a-zA-Z\-' ]{1,50}$
date_of_birthInvalid formatYYYY-MM-DD
nationalityInvalid formatISO 3166-1 alpha-2 (^[A-Z]{2}$)
emailInvalid formatValid email address
phone_numberInvalid formatE.164 format (^\+?\d{1,15}$)

Address Validation Errors

FieldErrorPattern/Requirement
line1Invalid format^[A-Za-z0-9&.,''\-/() :+#]{2,100}$
line2Invalid format^[A-Za-z0-9&.,''\-/() :+#]{2,100}$
cityInvalid format^[A-Za-z\s\-'.]{2,100}$
stateInvalid format^[A-Za-z][A-Za-z\s\-']{1,99}$
post_codeInvalid format^[A-Za-z0-9\s\-]{3,12}$
countryInvalid formatISO 3166-1 alpha-2 (^[A-Z]{2}$)

Document Upload Errors

FieldErrorValid Values
documentMissingFile is required
document_typeInvalidselfie, identity_card, passport, driver_license, proof_of_address, other
document_countryInvalidISO 3166-1 alpha-2
sideInvalidfront, back, none

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

Questionnaire Errors

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" }
  ]
}

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" }
  ]
}

Did this page help you?