Open your account →

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.ioIntegration, 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

POST/sessions kyc:create

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

GET/sessions/{id} kyc:read

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"]
}
  • identity is returned as soon as an ID document has been read.
  • id_number_type tells you whether the ID number is a passport_number, national_id_number, residence_number or driving_license_number; the same value is also repeated under that key.
  • country_flag (emoji) and country_code_alpha2 are derived from the issuing country for easy display.
  • Document attributes (gender, date_of_issue, expiration_date, place_of_birth, marital_status, address) are null when 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_address always has the keys street, 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 in address is always retained unchanged), by in-flow questionnaire ADDRESS answers, and finally by client-declared details. The sibling parsed_address_meta reports the source (provider | model | questionnaire | declared | mixed) and, when our split contributed, the lowest confidence among its used fields. A component no source can supply confidently stays null and 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.address and identity.parsed_address are 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 in identity.address while the page collects its street/city breakdown; when a verified source already supplies structured components, the address step disappears entirely. Each submission fires a declared_details.submitted webhook. Accounts can adopt a time-bounded link policy (for example 14 days): declare_url_expires_at then 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, and POST /sessions/{id}/declare-link (scope kyc:create) mints one on demand for re-sends, immediately invalidating the previous link.
  • questionnaire lists 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 feed parsed_address at the questionnaire tier and compose identity.address when 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_url is null. 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_verification and phone_verification are present only when your workflow includes those steps — each carries a boolean verified flag.

3 · Get the full decision

GET/sessions/{id}/decision kyc:read

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

GET/sessions/{id}/pdf kyc:pdf

Returns the official report as a PDF (application/pdf).

5 · Download the customer portrait

GET/sessions/{id}/portrait kyc:read

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 (on GET /sessions/{id} and in the webhook extracted object) tells you in advance whether this call will succeed; while it is false the endpoint answers 404. Re-fetch after an extracted.updated event 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.