Rox Verify API
Integrate KYC & KYB identity verification into your application. REST + JSON, authenticated with HMAC request signing.
Base URL — https://platform.roxverify.io/api/v1
API keys
Keys are created inside your Rox Verify account.
Sign in at platform.roxverify.io → Integration, and generate a credential. Each credential is a public Key + a Secret. The secret is shown once — store it securely; it never travels over the network and is used only to sign requests. Credentials are scoped to the abilities you select (kyc:create, kyc:read, kyc:pdf, and the kyb:* equivalents).
Authentication (HMAC request signing)
Send these headers on every request:
X-Api-Key: <your key>
X-Api-Timestamp: <unix seconds> # must be within 5 minutes
X-Api-Signature: <hex HMAC-SHA256>
The signature is HMAC-SHA256 of the following string, using your secret as the key:
{METHOD}\n{REQUEST_PATH}\n{TIMESTAMP}\n{sha256_hex(body)}
# e.g. POST /api/v1/sessions
POST\n/api/v1/sessions\n1750000000\n<sha256 of the JSON body>
Ready-to-use Node.js helper:
const crypto = require('crypto');
const KEY = 'rk_live_...'; // X-Api-Key
const SECRET = 'rsk_...'; // never sent — signs requests
const BASE = 'https://platform.roxverify.io/api/v1';
// Build the signed headers for a request
function sign(method, path, body = '') {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const signingString = [method.toUpperCase(), path, ts, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', SECRET).update(signingString).digest('hex');
return {
'X-Api-Key': KEY,
'X-Api-Timestamp': ts,
'X-Api-Signature': signature,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
// Create a verification session
const body = JSON.stringify({ client_name: 'Jane Cooper', vendor_data: 'user-123' });
const path = '/api/v1/sessions';
fetch(BASE + '/sessions', { method: 'POST', headers: sign('POST', path, body), body })
.then(r => r.json()).then(console.log);
Requests with a missing/invalid signature, or a timestamp older than 5 minutes, are rejected with 401 — so an intercepted request can’t be replayed or modified.
1 · Create a verification session
Creates a user (KYC) session and returns a hosted URL to send to your client.
{ "client_name": "Jane Cooper", "client_email": "jane@example.com", "vendor_data": "user-123" }
→ { "id": 42, "type": "user", "status": "Not Started", "session_url": "https://verify.roxverify.io/session/AbC123" }
To open a business verification (KYB) session instead, pass "type": "business" (requires the kyb:create ability):
{ "type": "business", "company_name": "Sellsides Ltd", "company_website": "sellsides.com", "vendor_data": "company-123" }
→ { "id": 51, "type": "business", "status": "Not Started", "session_url": "https://verify.roxverify.io/session/XyZ789" }
company_website (or a company email) is optional — it’s used to show the business logo on the verification.
2 · Get status & extracted data
Overall status, per-check results, and the extracted identity once your client finishes — so you get more than just Approved / Declined.
{
"id": 42, "type": "user", "status": "Approved",
"checks": { "id_verification": "Approved", "liveness": "Approved", "face_match": "Approved",
"aml": "Approved", "ip_analysis": "Approved",
"proof_of_address": "Approved", "email": "Approved", "phone": "Approved" },
"identity": {
"first_name": "James", "last_name": "Whitfield", "full_name": "James Whitfield",
"date_of_birth": "1988-04-17", "nationality": "GBR",
"country": "United Kingdom", "country_code": "GBR",
"country_code_alpha2": "GB", "country_flag": "🇬🇧",
"document_type": "Passport",
"document_number": "533401372",
"id_number_type": "passport_number", // national_id_number | residence_number | ...
"passport_number": "533401372", // same value, under its type-specific key
"gender": "M", // "M" | "F", as read from the document / MRZ
"date_of_issue": "2019-02-11", // null on passports (the MRZ has no issue date)
"expiration_date": "2029-02-10",
"place_of_birth": "London",
"personal_number": null,
"marital_status": null, // only when the document carries it
"portrait_available": true, // GET /sessions/{id}/portrait will return an image
"address": "221B Baker Street, London, NW1 6XE",
"parsed_address": { "street": "221B Baker Street", "city": "London",
"region": null, "postal_code": "NW1 6XE", "country": "GBR" }
},
// The three blocks below appear ONLY when your workflow runs that step:
"address_verification": { "verified": true, "status": "Approved",
"address": "221B Baker Street, London, NW1 6XE",
"document_type": "BANK_STATEMENT",
"issuer": "Barclays Bank", "issue_date": "2026-07-30" },
"email_verification": { "email": "james@example.com", "verified": true, "status": "Approved" },
"phone_verification": { "phone_number": "+44 7700 900123", "verified": true, "status": "Approved" },
// Present only when your workflow includes an in-flow questionnaire:
"questionnaire": [ { "questionnaire": "Address", "status": "Approved",
"question": "Please provide your address:", "type": "ADDRESS",
"answer": { "street": "Wilson Gardens 6", "city": "London",
"state": "England", "postalCode": "HA1 4DZ",
"country": "GBR" } } ],
// Present only when the subject filled the hosted declare page (see below):
"declared_details": { "street": "12 Omar Mukhtar St", "city": "Tripoli", "region": null,
"postal_code": null, "country": "LBY", "date_of_issue": null,
"declared_at": "2026-08-06T10:12:00Z" },
"charged_services": ["kyc","liveness","aml","ip"]
}
identityis returned as soon as an ID document has been read.id_number_typetells you whether the ID number is apassport_number,national_id_number,residence_numberordriving_license_number; the same value is also repeated under that key.country_flag(emoji) andcountry_code_alpha2are derived from the issuing country for easy display.- Document attributes (
gender,date_of_issue,expiration_date,place_of_birth,marital_status,address) arenullwhen the document type does not carry them: a passport MRZ has gender and expiry but no issue date or address, while national IDs typically carry issue date and the holder's address. parsed_addressalways has the keysstreet,city,region,postal_code,country. Components are filled, in order of precedence, by the provider's structured data, by Rox Verify's confidence-scored split of the verified free-text line (only fields meeting the confidence bar are used; the raw line inaddressis always retained unchanged), by in-flow questionnaire ADDRESS answers, and finally by client-declared details. The siblingparsed_address_metareports thesource(provider|model|questionnaire|declared|mixed) and, when our split contributed, the lowest confidence among its used fields. A component no source can supply confidently staysnulland the declare page collects it.- Address precedence: the document itself, then a verified proof-of-address (when your workflow includes that step), then Rox Verify's split of the verified line, then in-flow questionnaire answers, then declared details.
identity.addressandidentity.parsed_addressare filled in that order, and a verified value is never overwritten by a derived or declared one. declare_url(returned when you create a session and on this status call) is a public, tokenized page where your client declares only what is still missing: the structured address split and, when the document lacks one, the issue date. A verified free-text address (document OCR or proof-of-address) stays authoritative inidentity.addresswhile the page collects its street/city breakdown; when a verified source already supplies structured components, the address step disappears entirely. Each submission fires adeclared_details.submittedwebhook. Accounts can adopt a time-bounded link policy (for example 14 days):declare_url_expires_atthen reports the expiry, an expired link shows only an ask-for-a-new-link notice (no personal data, no form), reading the session mints a fresh link automatically, andPOST /sessions/{id}/declare-link(scopekyc:create) mints one on demand for re-sends, immediately invalidating the previous link.questionnairelists every answered item from an in-flow questionnaire in your verification workflow (questionnaire title and status, question, element type, and the raw answer value), so custom questions reach your system without a contract change.ADDRESS-type answers additionally feedparsed_addressat the questionnaire tier and composeidentity.addresswhen the document carried no address line.- Declared-details capture is optional per account and off by default: if your integration only needs identification, your clients are never asked for anything beyond the document flow and
declare_urlisnull. Your account admin can turn it on from the platform's Integration page when you need the full core-banking field set. The declare page works in Arabic and English, and can also read the address from an uploaded utility bill or bank statement for the client to confirm. address_verification,email_verificationandphone_verificationare present only when your workflow includes those steps — each carries a booleanverifiedflag.
3 · Get the full decision
Everything from the status call above (identity + the verification blocks) plus the complete raw decision — document data, biometric scores, AML hits, IP / device.
4 · Download the report PDF
Returns the official report as a PDF (application/pdf).
5 · Download the customer portrait
Returns the verified likeness of the customer as a raw image (Content-Type reflects the stored file, typically image/jpeg).
- The image is the face crop from the identity document when available, falling back to the liveness selfie frame. The full document image is never returned by this endpoint.
identity.portrait_available(onGET /sessions/{id}and in the webhookextractedobject) tells you in advance whether this call will succeed; while it isfalsethe endpoint answers 404. Re-fetch after anextracted.updatedevent to pick up a portrait that becomes available late.- User (KYC) sessions only — business sessions answer 422.
Webhooks (optional)
Instead of polling, Rox Verify will POST the event to your endpoint as a session progresses. Configure your receiving URL in your account under Integration → Webhooks.
The body carries the provider event plus an extracted object, identical in shape to the identity / verification blocks returned by GET /sessions/{id}, so you can parse one contract in both places.
An extracted.updated event (same envelope) fires when Rox Verify's address split adds structured components shortly after a decision, so a payload refused for a missing street can be rebuilt without polling.
You will also receive a declared_details.submitted event (with session_id, vendor_data, status and the full extracted object) whenever your client submits the declare page, so a payload that was missing mandatory fields can be completed without polling.
Treat webhooks as a notification: before acting on a result, confirm it by calling the signed GET /sessions/{id} endpoint. That call is authenticated with your API credentials, so its response is the authoritative record.
KYB (kyb:*) endpoints mirror the KYC ones and are available when business verification is enabled on your account.