Guides

Identity verification (KYC)

Every customer completes identity verification before they can operate. Pre-submit the profile you already hold, launch the hosted verification flow with a single call, and track the outcome by polling or webhooks — the more data you send, the less your customer has to type.

Audience server-side integrators API External API v1 Two calls to integrate

You'll need workspace credentials and a bearer token — see the Authentication guide. Drive the whole flow against https://api-stage.virtuabroker.com first; stage behaves like production but verifies no real identities.

How it works

Verification has two parts, and only one of them can be pre-filled by you:

The hosted flow adapts to how much profile data is already on file. The more you push ahead of time with POST /v1/kyc/data, the shorter the form your customer sees:

No data sent

The customer fills the full profile form, then completes the document + selfie check.

Partial data sent

The form asks only the fields still missing, then moves to the document + selfie check.

All required data sent

No form at all — the customer goes straight to the document + selfie check.

Recommended integration. Collect the profile in your own onboarding UI (or reuse what you already hold), push it with POST /v1/kyc/data, then start verification — your customers go straight to the document + selfie step with no duplicate form.

Prerequisites & authentication

All endpoints require a bearer token. Exchange your workspace credentials for one with the OAuth 2.0 password grant, then send it on every request as Authorization: Bearer <access_token>. See the Authentication guide for the token lifecycle.

get a token
export VB_TOKEN=$(curl -s https://auth2.virtuabroker.com/realms/cryptobot/protocol/openid-connect/token \
  -d "grant_type=password" \
  -d "client_id=$VB_CLIENT_ID" \
  -d "username=$VB_USERNAME" \
  -d "password=$VB_PASSWORD" | jq -r .access_token)

Customers are identified by their email address. No prior registration call is needed — an account is created on first use. In the request body the customer's email is userEmail; on the status read the path parameter is accountEmail.

1 · Pre-submit customer data optional · recommended

POST /v1/kyc/data sends any subset of the customer's profile. Calls merge: you can send data incrementally across several calls, and each response reports exactly which required fields are still missing. Re-sending a field overwrites it; nested objects (addresses, document, taxId, depositEstimation) are replaced whole, so always send them complete.

POST /v1/kyc/data
curl https://api.virtuabroker.com/v1/kyc/data \
  -H "Authorization: Bearer $VB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userEmail": "maria@example.com",
    "firstName": "María",
    "lastName": "González",
    "phone": "+34600000000",
    "birthdate": "1990-01-31",
    "birthCountry": "ES",
    "nationality": "ES",
    "employmentStatus": "employed",
    "profession": "Nurse",
    "fundsOrigin": "salary",
    "accountPurpose": "currencyExchange",
    "depositEstimation": { "minimum": 0, "maximum": 10000 },
    "addresses": [{
      "street": "Calle Mayor 1",
      "city": "Madrid",
      "state": "Madrid",
      "zip": "28001",
      "country": "ES"
    }]
  }'
200 · response
{
  "success": true,
  "providedFields": ["firstName", "lastName", "phone", "nationality",
                     "employmentStatus", "fundsOrigin", "accountPurpose", "addresses"],
  "missingFields": []
}

When missingFields is empty the hosted flow will skip the profile form entirely. missingFields is the authoritative source of what is still needed — read it after every call rather than assuming a fixed list.

Profile fields

The fields below make up the profile. Whether a given field is required depends on your workspace's verification provider — the exact required set is provider-dependent. A lighter provider may not require birthdate, birthCountry, accountPurpose, profession, or depositEstimation; another may require none of them up front. Do not hard-code the list below as an API-wide requirement — trust the missingFields array in the responses instead.

FieldType / formatNotes
firstName, lastNamestringLegal name, as on the ID document
birthdateISO 8601 datee.g. 1990-01-31
birthCountryISO 3166-1 alpha-2Country of birth
phonestring, E.164e.g. +34600000000
nationalityISO 3166-1 alpha-2Two letters, not three — ES, VE
employmentStatusenumSee values below
professionstringFree text, e.g. Nurse
fundsOriginenumSee values below
accountPurposeenumSee values below
depositEstimationobject{ "minimum": <int>, "maximum": <int> } — expected deposit volume
addressesarray, ≥ 1Each with street, city, state, zip, country (ISO-2)

Conditional & optional fields

FieldNotes
fundsOriginOthersReasonCONDITIONALRequired when fundsOrigin is others — free-text description
genderOPTIONALmale · female · N/D (confirmed from the ID document)
documentCONDITIONAL{ "type", "id", "expiryDate", "issuingCountry" } — the block is optional (the hosted flow captures the document), but when you send it all four fields are required. Types: idcard · passport · drivinglicense · visa; issuingCountry ISO-2
taxIdOPTIONAL{ "number", "country" } — tax identifier
pepOPTIONALboolean — politically exposed person self-declaration

Enumerated values

FieldAccepted values
employmentStatusemployed · selfEmployed · businessOwner · unemployed · student · retired · others
fundsOriginsalary · incomes · investments · pensions · donations · propertiesSell · others
accountPurposesavings · currencyExchange · investment · trading · cryptocurrencyPayments

Document policy is set by compliance, not the API. Some jurisdictions restrict which documents are accepted — for example, residents of Spain may be required to verify with a national ID (DNI/NIF) or foreigner ID (NIE) rather than a passport. These rules come from your workspace's verification provider and compliance configuration; the API itself does not restrict document type by residency. Confirm the accepted documents for each market with your account contact.

2 · Start the verification

POST /v1/kyc returns the kycUrl your customer opens to complete verification. Pass your return URLs here — the customer is redirected to them when the hosted flow finishes.

POST /v1/kyc
curl https://api.virtuabroker.com/v1/kyc \
  -H "Authorization: Bearer $VB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userEmail": "maria@example.com",
    "countryCode": "ES",
    "type": "INDIVIDUAL",
    "sendOTPMail": false,
    "language": "es",
    "successUrl": "https://yourapp.example.com/kyc/success",
    "errorUrl": "https://yourapp.example.com/kyc/error"
  }'
FieldNotes
userEmailREQUIREDThe customer's email
countryCodeREQUIREDISO 3166-1 alpha-2 — pre-selects the document country
typeREQUIREDINDIVIDUAL
sendOTPMailREQUIREDboolean — whether the hosted flow sends the customer a one-time email code
languageOPTIONALUI language — e.g. en, es, fr, de, pt, it; any locale code your verification provider's WebSDK supports is accepted (not a fixed enum). Defaults to en when omitted or blank.
successUrlOPTIONALWhere the customer is redirected after completing verification
errorUrlOPTIONALWhere the customer is redirected if verification fails or is cancelled
blankOPTIONALtrue renders the page without framing — ideal for embedding in a WebView
200 · response
{
  "success": true,
  "status": "PENDING",
  "kycUrl": "https://api.virtuabroker.com/kyc/widget?data=…",
  "kycId": "maria@example.com",
  "userEmail": "maria@example.com"
}

3 · Your customer completes verification

What the customer sees at kycUrl depends on how much you pre-submitted in step 1:

Data on fileCustomer experience
All required fieldsStraight to the identity check — document photos + selfie. No form.
Some fieldsA short form asking only the missing fields, then the identity check.
NoneThe full profile form, then the identity check.

The hosted pages are responsive and mobile-friendly, with searchable selectors and keyboard navigation built in. When the flow ends, the customer is redirected to your successUrl or errorUrl.

Approval is not always instant at redirect. Document review can take from seconds to a few minutes. Treat successUrl as "submission finished" and rely on the webhook (step 5) or a poll (step 4) for the final decision.

4 · Track the verification status

GET /v1/kyc/{accountEmail} returns the current verification status wrapped in the standard { ok, value } envelope.

GET /v1/kyc/{accountEmail}
curl https://api.virtuabroker.com/v1/kyc/maria@example.com \
  -H "Authorization: Bearer $VB_TOKEN"
200 · response
{
  "ok": true,
  "value": {
    "status": "IN_PROGRESS",
    "missingFields": [],
    "accountId": "acct_5f3c9a2e"
  }
}

value.accountId is present once an account exists for the customer. Keep it — it's the identifier you pass to later deposit-method, deposit, and order calls, so the verified customer and their operations line up.

The status is one of six external values. Poll here, or subscribe to webhooks to react the moment it becomes APPROVED or REJECTED:

NOT_AVAILABLE PENDING IN_PROGRESS REVIEWING APPROVED REJECTED
StatusMeaningTerminal
NOT_AVAILABLENo verification exists for this customer yet.No
PENDINGVerification created, but the customer hasn't started.No
IN_PROGRESSThe customer is completing the flow, or the submission is being processed.No
REVIEWINGThe submitted information is under review.No
APPROVEDFully verified — the customer can operate.Yes
REJECTEDVerification did not pass. Follow up through your account contact to resolve.Yes

value.missingFields lists any profile fields still not on file — useful to prompt the customer, or your own systems, for exactly what's left.

5 · Receive status webhooks

Register an HTTPS endpoint once and VirtuaBroker notifies you on every verification status change — no polling needed. Verification changes arrive as the verification_status_updated event. For the full webhook contract — the event catalog, envelope shape, and delivery guarantees — see the Webhooks guide.

POST /v1/webhooks registers an endpoint. You supply the signing secret (minimum 8 characters) in the request — VirtuaBroker signs every delivery with it and never generates or returns it. Keep it in a secret store.

POST /v1/webhooks
curl https://api.virtuabroker.com/v1/webhooks \
  -H "Authorization: Bearer $VB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production",
    "url": "https://yourapp.example.com/webhooks/vb",
    "secret": "a-strong-random-secret"
  }'

Each delivery carries an X-VirtuaBroker-Signature-256 header: the string sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the secret you supplied. Verify it before trusting the payload — recompute the value over the exact bytes received and compare against the full header value (including the sha256= prefix) with a constant-time comparison.

verify X-VirtuaBroker-Signature-256
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Compute the HMAC over the raw request body, exactly as received, and compare against the whole header string — not a bare hex. Re-serializing parsed JSON reorders keys and whitespace, which changes the bytes and breaks verification.

The verification_status_updated payload for a KYC change looks like this:

verification_status_updated (KYC)
{
  "eventId": "b2c3d4e5-6f70-4a1b-8c2d-3e4f5a6b7c8d",
  "occurredAt": 1710000000000,
  "verificationType": "KYC",
  "customerEmail": "maria@example.com",
  "entityId": "entity-1",
  "status": "APPROVED",
  "previousStatus": "REVIEWING"
}

Deliveries may occasionally repeat — de-duplicate on eventId, and always confirm the latest state with GET /v1/kyc/{accountEmail} before granting access.

End-to-end sequence

The recommended integration, end to end. Steps 1 and 6 are optional but make the experience seamless and the state authoritative.

Pre-submit the profile

POST /v1/kyc/data with the profile you already hold. Read missingFields to see what's left; keep sending until it's empty.

Start verification

POST /v1/kyc with successUrl and errorUrl. The response returns the kycUrl.

Send the customer to the hosted flow

Open kycUrl via redirect or WebView. The customer fills any missing fields, then completes the document + selfie check and is redirected back to your successUrl.

Verification is reviewed

The submission is processed and, where needed, reviewed. This can take from seconds to a few minutes — don't block on the redirect.

React to the webhook

A verification_status_updated event arrives when the status changes — e.g. to APPROVED. Verify the signature and de-duplicate on eventId.

Confirm and grant access

GET /v1/kyc/{accountEmail} to confirm APPROVED authoritatively before enabling operations for the customer.

Error handling & best practices

Errors

HTTPCauseWhat to do
400Validation failed — unknown enum value, malformed field, bad country codeFix the field named in the response and retry — e.g. nationality must be 2-letter ISO (VE, not VEN)
401Missing or expired tokenRe-authenticate and retry
5xxTemporary failureRetry with backoff; POST /v1/kyc/data and POST /v1/kyc are safe to re-call

Best practices

Quick reference

EndpointPurpose
POST /v1/kyc/dataPre-submit / update customer profile data (merges; returns missingFields)
POST /v1/kycStart verification; returns kycUrl plus successUrl / errorUrl redirects
GET /v1/kyc/{accountEmail}Current status + missingFields + accountId, wrapped in { ok, value }
POST /v1/webhooksRegister a signed webhook endpoint for verification_status_updated
← Prev
Webhooks