# YeboVerify — Full Documentation (llms-full.txt) > This file is the entire developer documentation for YeboVerify concatenated > into a single text blob, intended for ingestion by LLMs and AI agents that > want full context in one read. > > GENERATED — do not edit by hand. Edit the markdown in docs/ and run > `npm run build:llms` (docs:build does it automatically). > > Canonical sources: > - Online docs: https://docs.yeboverify.com > - Source repo: https://github.com/omegathesecond/yeboverify-docs > - llms.txt index: https://docs.yeboverify.com/llms.txt > > If anything here conflicts with the live docs, the live docs at > docs.yeboverify.com are authoritative. --- ================================================================ # /index # Source: https://docs.yeboverify.com/index ================================================================ ## Quick Start ```bash curl -X POST https://api.yeboverify.com/v1/verify/selfie-match \ -H "X-API-Key: your_api_key" \ -F "selfie=@selfie.jpg" \ -F "profilePictureUrl=https://example.com/profile.jpg" ``` ```json { "success": true, "data": { "verificationId": "vrf_sm_abc123xyz", "samePerson": true, "matchScore": 94.5, "confidence": "high" } } ``` ## Get your API key Sign up at [yeboverify.com/signup](https://yeboverify.com/signup), or from the terminal — no account needed to make this call: ```bash curl -X POST https://api.yeboverify.com/v1/signup \ -H "Content-Type: application/json" \ -d '{"name":"Acme Financial Services","email":"you@company.com"}' ``` You get two keys: `yv_test_…` for the sandbox (never bills, never calls a real provider — start here) and `yv_live_…` for production. Both are shown only once. Then follow [Getting Started](/getting-started) for a working verification in five steps. ## Integrating with an AI assistant? ```bash claude mcp add yeboverify -- npx -y github:omegathesecond/yeboverify-mcp ``` No API key needed to start — see [AI Agents & MCP](/mcp). ================================================================ # /getting-started # Source: https://docs.yeboverify.com/getting-started ================================================================ # Getting Started YeboVerify verifies that a person is who they claim to be, from an ID document and a selfie. This page takes you from nothing to a working verification. Every command below is real and runnable, and every response shown is actual output — not an illustration. ::: tip Using an AI coding assistant? Install the MCP server and let your agent do the integration: ```bash claude mcp add yeboverify -- npx -y github:omegathesecond/yeboverify-mcp ``` No API key needed to start. Then ask it to *"set up YeboVerify in this project"* — it calls `start_here`, gets the full briefing, and can create your account for you. See [AI Agents & MCP](/mcp). There is also a machine-readable index of these docs at [llms.txt](https://docs.yeboverify.com/llms.txt). ::: ## Step 1 — Get your API keys Sign up in the browser at [yeboverify.com/signup](https://yeboverify.com/signup), or do it from the terminal — no account or key required to make this call: ```bash curl -X POST https://api.yeboverify.com/v1/signup \ -H "Content-Type: application/json" \ -d '{"name":"Acme Financial Services","email":"you@company.com"}' ``` You get back **two** keys: ```json { "success": true, "data": { "businessId": "cmu5wlqba0f28s6010nudz0n1", "name": "Acme Financial Services", "email": "you@company.com", "plan": "free", "apiKey": "yv_live_6tb4xK…", "apiKeyPrefix": "yv_live_6tb4", "testApiKey": "yv_test_E9TfCL…", "testApiKeyPrefix": "yv_test_E9Tf", "passwordSet": false } } ``` | Key | What it does | |---|---| | `yv_test_…` | **Sandbox.** Never bills, never calls a real AI provider, returns deterministic results. **Start here.** | | `yv_live_…` | **Production.** Billable, runs the real checks on real documents. | ::: warning Both keys are shown exactly once They are not retrievable later — save them now (in `.env`, never in source control). If you lose the live key, `POST /v1/account/recover-api-key` emails you a reset link. ::: Store the sandbox key and use it for everything until you go live: ```bash export YEBOVERIFY_API_KEY=yv_test_E9TfCL… ``` ## Step 2 — Your first verification Submit an ID photo and a selfie. With a `yv_test_` key this costs nothing and never touches a real provider: ```bash curl -X POST https://api.yeboverify.com/v1/verify \ -H "X-API-Key: $YEBOVERIFY_API_KEY" \ -F "idFront=@id-front.jpg" \ -F "selfie=@selfie.jpg" \ -F "externalRef=user_123" ``` ```json { "success": true, "data": { "verificationId": "vrf_tpmd6kfkuc1of0ci", "status": "pending", "message": "Verification submitted successfully. Results will be sent via webhook." } } ``` `externalRef` is your own user or order ID. It comes back on every result, so you never have to store YeboVerify's IDs to know who a result belongs to. Fetch the result a few seconds later: ```bash curl https://api.yeboverify.com/v1/verifications/vrf_tpmd6kfkuc1of0ci \ -H "X-API-Key: $YEBOVERIFY_API_KEY" ``` ```json { "verificationId": "vrf_tpmd6kfkuc1of0ci", "externalRef": "user_123", "status": "COMPLETED", "decision": "APPROVED", "decisionReason": "[TEST MODE] Simulated approval (simulate=approved)", "confidence": "high", "faceScore": 98, "ocrConfidence": 95, "livenessDecision": "live", "extractedData": { "surname": "Testperson", "names": "Sandbox Jane", "dateOfBirth": "1990-01-01", "sex": "F", "idNumber": "TESTMODE0000", "nationality": "ZZ", "documentType": "NATIONAL_ID", "issueDate": "2020-01-01", "expiryDate": "2030-01-01", "issuingCountry": "ZZ" } } ``` That's a full verification. `decision` is the answer you act on: `APPROVED`, `REJECTED`, or `NEEDS_REVIEW`. ### Test the paths that aren't the happy one Approval is the easy case. Add `simulate` to force any other outcome — sandbox keys only: ```bash curl -X POST https://api.yeboverify.com/v1/verify \ -H "X-API-Key: $YEBOVERIFY_API_KEY" \ -F "idFront=@id-front.jpg" -F "selfie=@selfie.jpg" \ -F "simulate=rejected" ``` ```json { "decision": "REJECTED", "decisionReason": "[TEST MODE] Simulated rejection — face match below threshold (simulate=rejected)" } ``` | `simulate` | Result | |---|---| | `approved` | Approved (the default when you omit it) | | `rejected` | Rejected — face match below threshold | | `needs_review` | Routed to manual review | | `liveness_spoofed` | Rejected — anti-spoofing caught a presentation attack | | `expired_document` | Rejected — the ID has expired | **Build the `rejected` and `needs_review` branches now.** They are the ones that break in production. Passing `simulate` with a live key is rejected with `400`, so this can never silently affect real traffic. ## Step 3 — Choose your integration path | Path | Use it when | You build | |---|---|---| | **Hosted flow** | You want the shortest route to production | Nothing — we host the capture UI | | **Direct upload** | You already have the images | Your own capture UI | | **KYB** | You're verifying a company, not a person | See [KYB](/kyb) | ### Hosted flow (recommended) Create a session, and send the user to the URL it returns: ```bash curl -X POST https://api.yeboverify.com/v1/sessions/create \ -H "X-API-Key: $YEBOVERIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"userId":"user_123"}' ``` ```json { "success": true, "session": "eyJ1c2VySWQiOiJ1c2VyXzEyMyIs…", "verifyUrl": "https://verify.yeboverify.com#session=eyJ1c2VySWQiOiJ1c2VyXzEyMyIs…", "expiresIn": 1800 } ``` The user opens `verifyUrl`, captures their ID and selfie on `verify.yeboverify.com`, and the result arrives on your webhook. You build no camera UI, no upload handling, no liveness challenge. Note the shape of that response — see [Response format](#response-format) below, because it is the one endpoint family that differs. ::: warning You deliver the link. YeboVerify does not. `verifyUrl` is **returned to you**, not sent anywhere. YeboVerify never contacts your user — no SMS, no email, no verification code, ever. Redirect their browser to it, render it as a button, or send it over whatever channel you already use. Keep the `#`. The token is in the URL *fragment* so it never reaches server logs, analytics, or the `Referer` header of the next page. Rewriting it to `?session=` leaks a live credential. The link expires in `expiresIn` seconds (30 minutes) — create it when the user is ready to verify, not hours ahead. ::: ### Direct upload `POST /v1/verify` is what you already used in Step 2. Two narrower variants exist when you don't need the full check: [`/v1/verify/selfie-match`](/api-reference#selfie-match-verification) (face comparison only) and [`/v1/verify/id-document`](/api-reference#id-document-verification). ## Step 4 — Receive the result Register a webhook once: ```bash curl -X PUT https://api.yeboverify.com/v1/account/webhook \ -H "X-API-Key: $YEBOVERIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://yourapp.com/webhooks/yeboverify", "webhookSecret": "whsec_a_long_random_string" }' ``` The hosted flow delivers **two** events: `verification.submitted` the moment the user finishes uploading, then `verification.completed` with the decision (~5–10s later). Flip your own KYC state to "in progress" on `submitted` — never on session creation, since the user might abandon — and decide on `completed`. Two rules for the handler: 1. **Verify the signature** on every delivery (`X-YeboVerify-Signature-V2`, HMAC-SHA256). An unverified webhook endpoint is an open door. 2. **De-duplicate on `eventId`.** Retries are expected, so your handler must be idempotent. Both are covered with code in [Webhooks](/webhooks). ## Step 5 — Go live 1. Swap `yv_test_…` for `yv_live_…`. 2. Set a spend limit — `PUT /v1/account/spend-limit` — so a bug can't run up a bill. 3. Point the webhook at production and confirm it's reachable from the internet. 4. Re-test `rejected` and `needs_review` against your real UI. ## Reference ### Base URL ``` https://api.yeboverify.com ``` `https://dev-api.yeboverify.com` is a separate pre-production deployment. It is not related to which key you use — both key types work against either host. ### Authentication Every request carries your key in a header: ``` X-API-Key: yv_test_… ``` ### Response format Almost every endpoint wraps its payload in `data`: ```json { "success": true, "data": { } } ``` Errors are uniform: ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable error message" } } ``` ::: warning One exception: `/v1/sessions/*` responds flat The session endpoints put their fields **next to** `success`, with no `data` wrapper: ```json { "success": true, "session": "…", "verifyUrl": "…", "expiresIn": 1800 } ``` So `response.data.verifyUrl` is `undefined` — read `response.verifyUrl`. This one difference is the most common cause of a broken hosted-flow integration; it broke our own Node SDK. Every other endpoint in these docs is wrapped. ::: See [Error Codes](/error-codes) for the full list. ## Next steps - [SDKs](/sdks) — Node, React, React Native, Flutter, Kotlin, Swift - [AI Agents & MCP](/mcp) — integrate with Claude, Cursor, or any MCP client - [Webhooks](/webhooks) — signature verification, retries, event catalogue - [API Reference](/api-reference) — every endpoint - [KYB](/kyb) — company verification - [White Label](/white-label) — put your own brand on the hosted flow ================================================================ # /mcp # Source: https://docs.yeboverify.com/mcp ================================================================ # AI Agents & MCP Most integrations now get written with an AI assistant in the loop. YeboVerify ships an MCP server so your agent can do the integration properly — it gets the walkthrough, the API quirks, and the ability to create your account and run real verifications, without you hand-copying anything from these docs. ## Install ```bash claude mcp add yeboverify -- npx -y github:omegathesecond/yeboverify-mcp ``` **No API key needed to start.** Then ask your assistant: > *"Set up YeboVerify identity verification in this project."* It calls `start_here`, receives the full integration briefing, and walks you through creating an account and wiring the code. For Cursor, Windsurf, or any other MCP client, the stdio config is: ```json { "mcpServers": { "yeboverify": { "command": "npx", "args": ["-y", "github:omegathesecond/yeboverify-mcp"], "env": { "YEBOVERIFY_API_KEY": "yv_test_…" } } } } ``` Leave out the `env` block entirely until you have a key. The server starts without one, and the two onboarding tools work regardless — so your agent can get you a key from inside the editor rather than sending you off to a signup page. ::: warning The repository is private `npx -y github:…` needs your GitHub account to have access to `omegathesecond/yeboverify-mcp`, and the package is not on npm yet, so the command fails to authenticate until we grant you access. Ask us and we'll add your GitHub username. Everything on this page works the moment you have it. ::: Point the server at dev with `YEBOVERIFY_BASE_URL=https://dev-api.yeboverify.com`. ## Tools **Onboarding — no API key required:** | Tool | What it does | |---|---| | `start_here` | The integration briefing: getting keys, sandbox-first testing, which integration path to pick, webhook handling, and the three quirks that most often produce broken integrations. | | `create_sandbox_account` | Creates a real account from a business name + work email and returns both keys, so you can start without leaving the editor. | **Runtime — need a key:** | Tool | What it does | |---|---| | `create_verification_session` | Starts a hosted KYC flow; returns a `verifyUrl` to send the user to. | | `get_verification` | One verification by id — status, decision, scores, extracted ID fields. | | `list_verifications` | Recent verifications, optionally filtered by status. | | `create_company_verification` | Starts a KYB check (documents + registry + directors). | | `get_account` | Account name, plan, webhook URL — the cheapest proof a key works. | Failures come back as MCP error results carrying the real API message. Nothing is swallowed or replaced with placeholder data, so your agent sees what actually went wrong instead of inventing a reason. `create_sandbox_account` creates a **real** account and asks for a real name and email — the email is the only way to recover a lost key. A good agent will confirm both with you first rather than inventing them. ## Without MCP If you'd rather your assistant just read the docs, point it at **[https://docs.yeboverify.com/llms.txt](https://docs.yeboverify.com/llms.txt)** — a curated, machine-readable index of this documentation. [llms-full.txt](https://docs.yeboverify.com/llms-full.txt) is the entire documentation set as a single file, for pasting into a context window. ## What agents get wrong An LLM writing a YeboVerify integration will pattern-match a conventional REST API and get these three things wrong. `start_here` and `llms.txt` both state them; if you're prompting by hand, paste them in. **1. `/v1/sessions/*` responds flat.** Every other endpoint wraps its payload in `data`. The session routes put fields next to `success`: ```js // ❌ undefined const { verifyUrl } = (await res.json()).data; // ✅ const { verifyUrl } = await res.json(); ``` This is not hypothetical — it is exactly what broke our own Node SDK, which returned `undefined` from `createSession()` and so never produced a link for anyone. **2. `verifyUrl` uses a fragment, not a query parameter.** ``` ✅ https://verify.yeboverify.com#session=TOKEN ❌ https://verify.yeboverify.com?session=TOKEN ``` The `#` keeps the token out of server logs, analytics, and the `Referer` header. Don't rewrite it, and don't log the whole URL — it is a live credential. **3. YeboVerify sends your end user nothing.** No SMS, no email, no OTP, no verification code — there is no endpoint that does it. You deliver `verifyUrl`. Its only outbound message is the webhook to your server. If a user says *"I never got a code"*, what failed is your own delivery of `verifyUrl`. ## Also available: the CLI For a non-agent setup, the CLI wires a project up in one command: ```bash npx -y github:omegathesecond/yeboverify-cli init ``` It validates your key, writes it to `.env`, optionally registers your webhook, and scaffolds a working hosted-flow example. Same private-repo caveat as above. ================================================================ # /api-reference # Source: https://docs.yeboverify.com/api-reference ================================================================ # YeboVerify API Reference ## Base URL ``` https://api.yeboverify.com ``` ## Authentication All API endpoints (except health check) require authentication via the `X-API-Key` header. ``` X-API-Key: your_api_key_here ``` --- ## Endpoints ### Health Check Check API status and availability. ``` GET /health ``` **Response:** ```json { "status": "ok", "timestamp": "2024-01-15T12:00:00.000Z", "service": "yeboverify-api", "version": "1.0.0" } ``` --- ## Verification Endpoints ### Selfie Match Verification Compare a selfie against a profile picture URL using facial recognition. ``` POST /v1/verify/selfie-match ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `multipart/form-data` | **Request Body (multipart/form-data):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `selfie` | file | Yes | Selfie image (JPEG, PNG, max 10MB) | | `profilePictureUrl` | string | Yes | URL of the profile picture to compare against | | `externalRef` | string | No | Your internal reference ID | **Response:** ```json { "success": true, "data": { "verificationId": "vrf_sm_abc123xyz", "samePerson": true, "matchScore": 94.5, "confidence": "high", "processingMs": 2340 } } ``` **Response Fields:** | Field | Type | Description | |-------|------|-------------| | `verificationId` | string | Unique verification ID (prefixed with `vrf_sm_`) | | `samePerson` | boolean | `true` if faces match (score ≥ 80) | | `matchScore` | number | Similarity score (0-100) | | `confidence` | string | `high` (≥90), `medium` (75-89), `low` (<75) | | `processingMs` | number | Processing time in milliseconds | --- ### ID Document Verification Full identity verification with face matching, liveness detection, and document OCR. ``` POST /v1/verify/id-document ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `multipart/form-data` | **Request Body (multipart/form-data):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `selfie` | file | Yes | Live selfie image (JPEG, PNG, max 10MB) | | `idFront` | file | Yes | Front of ID document (JPEG, PNG, max 10MB) | | `idBack` | file | No | Back of ID document (JPEG, PNG, max 10MB) | | `externalRef` | string | No | Your internal reference ID | | `documentType` | string | No | Document type hint (e.g., `passport`, `national_id`) | **Response:** ```json { "success": true, "data": { "verificationId": "vrf_id_xyz789abc", "status": "approved", "decision": "approved", "confidence": "high", "faceMatch": { "score": 92.3, "samePerson": true }, "documentData": { "surname": "SMITH", "names": "JOHN MICHAEL", "dateOfBirth": "1990-05-15", "idNumber": "9005151234567", "sex": "M", "nationality": "ZA", "documentType": "NATIONAL_ID", "expiryDate": "2030-05-15" }, "ocrConfidence": 85, "processingMs": 5420 } } ``` **Response Fields:** | Field | Type | Description | |-------|------|-------------| | `verificationId` | string | Unique verification ID (prefixed with `vrf_id_`) | | `status` | string | `approved`, `rejected`, or `needs_review` | | `decision` | string | Same as status | | `confidence` | string | `high`, `medium`, or `low` | | `faceMatch.score` | number | Face similarity score (0-100) | | `faceMatch.samePerson` | boolean | `true` if faces match (score ≥ 70) | | `documentData` | object | Extracted document information | | `ocrConfidence` | number | OCR confidence (0-100) | | `processingMs` | number | Processing time in milliseconds | **Document Data Fields:** | Field | Type | Description | |-------|------|-------------| | `surname` | string | Last name from document | | `names` | string | First and middle names | | `dateOfBirth` | string | Date of birth (YYYY-MM-DD) | | `idNumber` | string | ID/Document number | | `sex` | string | `M` or `F` | | `nationality` | string | Nationality code | | `documentType` | string | Type of document | | `expiryDate` | string | Document expiry date (YYYY-MM-DD) | --- ## Session Endpoints For redirect-based verification flows. ### Create Session Create a verification session for redirect-based flows. ``` POST /v1/sessions/create ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `application/json` | **Request Body:** ```json { "userId": "user_123", "callback": "https://yourapp.com/verification-complete" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `userId` | string | Yes | Your user identifier | | `callback` | string | No | Callback URL after verification | | `theme` | object | No | Per-session branding override — see [White-Label Branding](/white-label) | Any other top-level field, or any `theme` sub-field outside `logoUrl`/`companyName`/`primaryColor`/`darkMode`, is rejected with `400`. **Response:** ```json { "success": true, "session": "eyJhbGciOiJIUzI1NiIs...", "verifyUrl": "https://verify.yeboverify.com#session=eyJhbGciOiJIUzI1NiIs...", "expiresIn": 1800 } ``` ::: warning You deliver the link — YeboVerify does not This endpoint **returns** `verifyUrl`. It does **not** send it anywhere. YeboVerify never contacts your user: there is no SMS, no email, and no one-time code or OTP issued by this API. Getting the link to your user is your integration's job — redirect their browser to it, render it as a button, or send it over whatever channel you already use. If your users report "I never received a code", they are waiting on a message YeboVerify was never going to send. Check that your own code actually delivered `verifyUrl`. Two constraints when you pass it on: - **Keep the `#`.** The token sits in the URL *fragment*, not the query string, so it never reaches server logs, analytics, or the `Referer` header of the next page. Rewriting it to `?session=` leaks a live credential. - **It expires in `expiresIn` seconds** (1800 = 30 min). Mint it when the user is ready to verify, not hours ahead. If a link expires before the user opens it, call this endpoint again for a fresh one. The verification *result* comes back to you on your webhook, not on this response — see [Webhooks](/webhooks). ::: --- ### Validate Session Validate a session token. ``` POST /v1/sessions/validate ``` **Request Body:** ```json { "session": "eyJhbGciOiJIUzI1NiIs..." } ``` **Response:** ```json { "success": true, "userId": "user_123", "businessId": "clxxxxx", "callback": "https://yourapp.com/verification-complete", "expiresAt": 1705323600000, "branding": { "logoUrl": null, "companyName": null, "primaryColor": null, "darkMode": true } } ``` --- ### Upload Photo (progressive) Upload a single photo against a session token. Useful on slow networks — your client uploads each captured photo as soon as it's ready, then calls **Submit Session** with just the keys instead of bytes. ``` POST /v1/sessions/upload ``` **Request Body:** ```json { "session": "eyJhbGciOiJIUzI1NiIs...", "photo": "data:image/jpeg;base64,/9j/4AAQ...", "type": "selfie" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | Yes | Session token from Create Session | | `photo` | string | Yes | Base64-encoded image (with or without `data:` URL prefix) | | `type` | string | Yes | One of `selfie`, `idFront`, `idBack`, `liveness1`, `liveness2` | **Response:** ```json { "success": true, "key": "sessions/user_123/selfie-abcd1234efgh", "type": "selfie", "size": 184320 } ``` The returned `key` can be passed to **Submit Session** instead of re-uploading the bytes. --- ### Submit Session Submit a verification with photos that were either uploaded progressively (using `*Key` fields) or sent inline as base64 (using non-`Key` fields). Triggers AI processing — the result lands in your webhook as `verification.completed` after ~5–10 seconds. A `verification.submitted` webhook fires immediately on success. ``` POST /v1/sessions/submit ``` **Request Body (using progressive upload keys):** ```json { "session": "eyJhbGciOiJIUzI1NiIs...", "selfieKey": "sessions/user_123/selfie-abcd", "idFrontKey": "sessions/user_123/idFront-efgh", "idBackKey": "sessions/user_123/idBack-ijkl" } ``` **Request Body (using inline base64):** ```json { "session": "eyJhbGciOiJIUzI1NiIs...", "selfie": "data:image/jpeg;base64,...", "idFront": "data:image/jpeg;base64,...", "idBack": "data:image/jpeg;base64,..." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | Yes | Session token from Create Session | | `selfie` / `selfieKey` | string | Yes (one of) | Selfie photo | | `idFront` / `idFrontKey` | string | Yes (one of) | Front of ID document | | `idBack` / `idBackKey` | string | No | Back of ID document (some types only) | **Response:** ```json { "success": true, "verificationId": "vrf_id_abc123xyz", "status": "PENDING", "message": "Verification submitted. Results will be sent via webhook." } ``` Listen for the `verification.completed` webhook to receive the final decision. See [Webhooks](/webhooks) for details. --- ## Verification History ### List Verifications Get a paginated list of verifications for your business. ``` GET /v1/verifications ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | number | 1 | Page number | | `limit` | number | 20 | Results per page (max 100) | | `status` | string | - | Filter by status: `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `NEEDS_REVIEW` | **Response:** ```json { "success": true, "data": { "verifications": [ { "verificationId": "vrf_sm_abc123", "externalRef": "order_456", "status": "COMPLETED", "decision": "APPROVED", "confidence": "high", "createdAt": "2024-01-15T12:00:00.000Z", "completedAt": "2024-01-15T12:00:03.000Z" } ], "pagination": { "page": 1, "limit": 20, "total": 156, "totalPages": 8 } } } ``` --- ### Get Verification Details Get detailed information about a specific verification. ``` GET /v1/verifications/:verificationId ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | **Response:** ```json { "success": true, "data": { "verificationId": "vrf_id_xyz789abc", "externalRef": "user_123", "status": "COMPLETED", "decision": "APPROVED", "decisionReason": "High face match and document verification confidence", "confidence": "high", "faceScore": 92.3, "ocrConfidence": 85, "extractedData": { "surname": "SMITH", "names": "JOHN MICHAEL", "dateOfBirth": "1990-05-15", "idNumber": "9005151234567", "documentType": "NATIONAL_ID" }, "processingMs": 5420, "createdAt": "2024-01-15T12:00:00.000Z", "completedAt": "2024-01-15T12:00:05.000Z" } } ``` --- ### Delete Verification Remove a verification record. Triggers a `verification.deleted` webhook so downstream systems (e.g., your KYC store) can reset state for the user. Useful for letting a user re-verify after a rejected decision, or for honouring a "delete my data" request. ``` DELETE /v1/verifications/:verificationId ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | **Response:** ```json { "success": true, "message": "Verification deleted" } ``` Errors with `404` if the verification doesn't exist or doesn't belong to your business. The associated images are removed from R2 storage immediately as part of the delete. --- ## Account Endpoints ### Get Account Info Get your business account information. ``` GET /v1/account ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | **Response:** ```json { "success": true, "data": { "id": "clxxxxx", "name": "Acme Corp", "email": "api@acme.com", "webhookUrl": "https://acme.com/webhooks/yeboverify", "plan": "business", "active": true, "createdAt": "2024-01-01T00:00:00.000Z" } } ``` --- ### Update Webhook URL Update your webhook configuration. ``` PUT /v1/account/webhook ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `application/json` | **Request Body:** ```json { "webhookUrl": "https://yourapp.com/webhooks/yeboverify", "webhookSecret": "your_webhook_secret" } ``` **Response:** ```json { "success": true, "data": { "webhookUrl": "https://yourapp.com/webhooks/yeboverify", "message": "Webhook configuration updated" } } ``` --- ### Get Branding Get your hosted-flow branding. See [White-Label Branding](/white-label) for the full picture. ``` GET /v1/settings/branding ``` **Response:** ```json { "success": true, "data": { "logoUrl": "https://acme.com/logo.svg", "companyName": "Acme Corp", "primaryColor": "#FF6B00", "darkMode": true } } ``` --- ### Update Branding Set (or clear, with `null`) your hosted-flow branding. A field outside `logoUrl`/`companyName`/`primaryColor`/`darkMode` is rejected with `400`. ``` PUT /v1/settings/branding ``` **Request Body:** ```json { "logoUrl": "https://acme.com/logo.svg", "companyName": "Acme Corp", "primaryColor": "#FF6B00", "darkMode": true } ``` --- ## File Requirements ### Supported Formats - **JPEG** (`.jpg`, `.jpeg`) - **PNG** (`.png`) - **WebP** (`.webp`) ### Size Limits - Maximum file size: **10MB** per file - Recommended resolution: **640x480** to **1920x1080** ### Image Quality Tips - Ensure good lighting - Face should be clearly visible - ID documents should be flat and fully visible - Avoid blur, glare, or shadows ================================================================ # /webhooks # Source: https://docs.yeboverify.com/webhooks ================================================================ # Webhooks YeboVerify can send webhook notifications to your server when verification events occur. This is useful for async verification flows or for keeping your system in sync with verification results. Everything on this page applies identically to individual KYC (`/v1/verify`, `/v1/sessions/*`) and [Company Verification (KYB)](/kyb) — KYB payloads carry `type: "company"` and a `company` block instead of person-specific fields, but sign, retry, and dedupe exactly the same way. ::: tip The webhook is the only thing YeboVerify sends Webhooks go **to your server**, and that is the *only* outbound message YeboVerify produces. It never contacts your end user — no SMS, no email, no one-time code. If a user is waiting on a "code", they are waiting on something your own integration has to send them (see [Create Session](/api-reference#create-session) for the `verifyUrl` you hand them). Treat the webhook as the result channel, not a user notification. ::: ## Setting Up Webhooks ### Configure Your Webhook URL ```bash curl -X PUT https://api.yeboverify.com/v1/account/webhook \ -H "X-API-Key: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://yourapp.com/webhooks/yeboverify", "webhookSecret": "whsec_your_secret_here" }' ``` ### Webhook Secret The webhook secret is used to sign every outbound webhook using HMAC-SHA256. Always verify the signature to ensure the webhook is genuinely from YeboVerify — see [Webhook Signature Verification](#webhook-signature-verification). --- ## Webhook Events YeboVerify currently emits three events. Listen for all of them — your handler should switch on `event` and ignore unknown values gracefully so future additions don't break your integration. | Event | Fires when | Includes decision/scores | |-------|------------|--------------------------| | `verification.submitted` | Documents arrive at YeboVerify (before AI processing) | No — submission only | | `verification.completed` | AI processing finishes (individual KYC or [KYB](/kyb)) | Yes — full result | | `verification.deleted` | An admin removes the verification on the YeboVerify side | No — verificationId only | Every payload carries an `eventId` — a stable id for that specific event, safe to use as a dedupe key. See [Deduplicating Events](#deduplicating-events). ### verification.submitted Fires the moment a verification is created — the user has uploaded their documents and processing is about to begin. Use this to flip a "pending" / "in progress" state in your UI without lying about progress before the user has actually submitted. **Payload:** ```json { "event": "verification.submitted", "eventId": "evt_9f8e7d6c5b4a3928170f6e5d", "verificationId": "vrf_id_abc123xyz", "externalRef": "user_123", "timestamp": "2026-05-08T12:00:00.000Z" } ``` **Payload Fields:** | Field | Type | Description | |-------|------|-------------| | `event` | string | Always `verification.submitted` | | `eventId` | string | Stable id for this event — see [Deduplicating Events](#deduplicating-events) | | `verificationId` | string | Unique verification ID | | `externalRef` | string \| null | Your reference ID (if provided when calling `/v1/verify` or `/v1/sessions/create`) | | `timestamp` | string | ISO 8601 timestamp | ::: tip Out-of-order safety On rare network conditions, `verification.submitted` can arrive *after* `verification.completed`. Handle this by refusing to downgrade your local state from a final decision (verified/rejected) back to "in progress." Keep a `kycStatus` guard like `if currentStatus is one of {NONE, PENDING}` before flipping to PENDING. ::: --- ### verification.completed Sent when processing finishes — the verification has a final decision (approved, rejected, or needs human review). The same event covers both individual KYC and [company (KYB)](/kyb) verifications; check `type` to know which shape you're looking at. **Individual KYC payload:** ```json { "event": "verification.completed", "eventId": "evt_1a2b3c4d5e6f7a8b9c0d1e2f", "type": "individual", "verificationId": "vrf_id_abc123xyz", "externalRef": "user_123", "status": "completed", "decision": "approved", "confidence": "high", "faceScore": 92.3, "ocrConfidence": 85, "livenessChecked": true, "livenessScore": 88, "livenessDecision": "live", "aml": { "decision": "clear", "score": 0, "hits": [] }, "extractedData": { "surname": "SMITH", "names": "JOHN MICHAEL", "dateOfBirth": "1990-05-15", "idNumber": "9005151234567", "documentType": "NATIONAL_ID" }, "timestamp": "2026-05-08T12:00:05.000Z" } ``` **Payload Fields:** | Field | Type | Description | |-------|------|-------------| | `event` | string | Always `verification.completed` | | `eventId` | string | Stable id for this event — see [Deduplicating Events](#deduplicating-events) | | `type` | string | `individual` or `company`. [KYB](/kyb#webhook-payload) sends `company` and a `company` block instead of `faceScore`/`ocrConfidence`/`extractedData`. | | `verificationId` | string | Unique verification ID | | `externalRef` | string \| null | Your reference ID (if provided) | | `status` | string | `completed`, `failed`, or `needs_review` | | `decision` | string | `approved`, `rejected`, or `needs_review` | | `confidence` | string | `high`, `medium`, or `low` | | `faceScore` | number | Face similarity score (0-100) | | `ocrConfidence` | number | OCR confidence (0-100) | | `livenessChecked` | boolean | Whether an anti-spoofing/liveness signal was actually evaluated. `false` means the decision carries **no** anti-spoofing assurance (e.g. a single-shot upload for a business that doesn't require liveness). | | `livenessScore` | number | Composite liveness/anti-spoofing score (0-100, `0` when not checked) | | `livenessDecision` | string | `live`, `spoofed`, `uncertain`, or `not_checked` | | `aml` | object | AML/sanctions/PEP/watchlist screening summary — see [AML screening result](#aml-screening-result) | | `extractedData` | object | Extracted document data — see [Extracted Data Fields](#extracted-data-fields) | | `timestamp` | string | ISO 8601 timestamp | #### AML screening result | Field | Type | Description | |-------|------|-------------| | `aml.decision` | string | `clear` (no match), `hit` (potential match — routes to `needs_review`), `error` (screening couldn't complete — routes to `needs_review`), or `not_screened` (provider not configured) | | `aml.score` | number | Highest match score across all hits (0-1) | | `aml.hits` | array | Up to 10 matches: `{ name, score, topics }` | #### Extracted Data Fields When `decision` is `approved`, `extractedData` includes whatever YeboVerify could read from the ID. All fields are optional — partial OCR is normal: | Field | Type | Description | |-------|------|-------------| | `surname` | string | Family name | | `names` | string | Given names | | `dateOfBirth` | string | Date of birth (YYYY-MM-DD) | | `sex` | string | M / F / X | | `idNumber` | string | National ID / passport number | | `nationality` | string | ISO country code or country name | | `issuingCountry` | string | Country that issued the document | | `issueDate` | string | Date issued (YYYY-MM-DD) | | `expiryDate` | string | Date of expiry (YYYY-MM-DD) | | `documentType` | string | `NATIONAL_ID`, `PASSPORT`, `DRIVERS_LICENSE`, etc. | For the `company`-shaped payload (registered name, signatory decision, document checklist), see [KYB → Webhook Payload](/kyb#webhook-payload). --- ### verification.deleted Fires when an admin removes a verification on the YeboVerify side — typically used to reset a user's KYC state so they can re-verify. **Payload:** ```json { "event": "verification.deleted", "eventId": "evt_5c4b3a2918f7e6d5c4b3a291", "verificationId": "vrf_id_abc123xyz", "externalRef": "user_123", "timestamp": "2026-05-08T14:30:00.000Z" } ``` **Payload Fields:** | Field | Type | Description | |-------|------|-------------| | `event` | string | Always `verification.deleted` | | `eventId` | string | Stable id for this event — see [Deduplicating Events](#deduplicating-events) | | `verificationId` | string | The verification that was removed | | `externalRef` | string \| null | Your reference ID (if provided) | | `timestamp` | string | ISO 8601 timestamp | Use this to reset the user's KYC state (e.g., flip `kycStatus` back to `NONE`) so they can re-submit. --- ## Webhook Signature Verification Every webhook is signed and sent with three headers. **Use `X-YeboVerify-Signature-V2`** — it's replay-resistant and is the only scheme that will keep receiving updates going forward. ``` X-YeboVerify-Signature: sha256= — legacy, body-only, no replay protection X-YeboVerify-Timestamp: — when this request was signed X-YeboVerify-Signature-V2: sha256= — HMAC over "." ``` ::: danger `X-YeboVerify-Signature` (no `-V2`) has no replay protection It's `HMAC-SHA256(body, secret)` — the body alone. Anyone who ever observes one legitimate delivery (a compromised log, a proxy, a misconfigured analytics tool) can replay that exact request to your endpoint indefinitely and it will verify successfully forever. It exists **only** so existing integrations don't break outright; do not build new verification against it, and migrate off it if you're currently using it. ::: `X-YeboVerify-Signature-V2` is `HMAC-SHA256("${timestamp}.${rawBody}", secret)` — the timestamp is *inside* what's signed, so a captured request can't be replayed once you reject stale timestamps (see the skew check below). This mirrors how YeboPay signs its own callbacks. ::: warning Verify on raw bytes, not a re-stringified parsed object JSON re-serialization can change whitespace or key ordering, which silently invalidates the signature. Always run HMAC against the exact bytes you received over the wire — capture the raw body before any JSON parser runs (Express: mount `express.raw({ type: 'application/json' })` on your webhook route *before* `express.json()`). ::: ### Node.js Verification ```javascript const crypto = require('crypto'); const express = require('express'); const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60; // reject anything signed >5 minutes ago function verifyWebhookSignatureV2(rawBody, timestampHeader, signatureV2Header, secret) { const timestamp = parseInt(timestampHeader || '', 10); if (!Number.isFinite(timestamp)) return false; // Replay protection: a captured, still-validly-signed request older than // the skew window is rejected outright, timestamp or not. const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestamp); if (ageSeconds > MAX_TIMESTAMP_SKEW_SECONDS) return false; const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); const sigBuf = Buffer.from(signatureV2Header || ''); const expBuf = Buffer.from(expected); if (sigBuf.length !== expBuf.length) return false; return crypto.timingSafeEqual(sigBuf, expBuf); } // Mount BEFORE express.json() so the body is captured as a Buffer app.post( '/webhooks/yeboverify', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.headers['x-yeboverify-timestamp']; const signatureV2 = req.headers['x-yeboverify-signature-v2']; const webhookSecret = process.env.YEBOVERIFY_WEBHOOK_SECRET; if (!verifyWebhookSignatureV2(req.body, timestamp, signatureV2, webhookSecret)) { console.error('Invalid or stale webhook signature'); return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body.toString('utf8')); switch (event.event) { case 'verification.submitted': handleSubmitted(event); break; case 'verification.completed': handleCompleted(event); break; case 'verification.deleted': handleDeleted(event); break; default: console.log('Unknown event type:', event.event); } res.status(200).send('OK'); } ); function handleSubmitted(event) { console.log(`Verification ${event.verificationId} submitted by ${event.externalRef}`); // Flip the user's local kycStatus to PENDING / "in progress" } function handleCompleted(event) { console.log(`Verification ${event.verificationId} (${event.type ?? 'individual'}) → ${event.decision}`); // Update your database with the final decision + extracted data } function handleDeleted(event) { console.log(`Verification ${event.verificationId} deleted`); // Reset the user's local kycStatus so they can re-verify } ``` ### Python Verification ```python import hmac import hashlib import json import time from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = 'your_webhook_secret' MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60 def verify_signature_v2(raw_body: bytes, timestamp: str, signature_v2: str, secret: str) -> bool: try: ts = int(timestamp) except (TypeError, ValueError): return False if abs(int(time.time()) - ts) > MAX_TIMESTAMP_SKEW_SECONDS: return False signed_payload = f'{ts}.'.encode() + raw_body expected = 'sha256=' + hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_v2 or '') @app.route('/webhooks/yeboverify', methods=['POST']) def handle_webhook(): timestamp = request.headers.get('X-YeboVerify-Timestamp') signature_v2 = request.headers.get('X-YeboVerify-Signature-V2') raw_body = request.get_data() # raw bytes, BEFORE Flask parses JSON if not verify_signature_v2(raw_body, timestamp, signature_v2, WEBHOOK_SECRET): abort(401) event = json.loads(raw_body) event_type = event.get('event') if event_type == 'verification.submitted': handle_submitted(event) elif event_type == 'verification.completed': handle_completed(event) elif event_type == 'verification.deleted': handle_deleted(event) return 'OK', 200 def handle_submitted(event): print(f"Verification {event['verificationId']} submitted by {event.get('externalRef')}") # Flip the user's local kycStatus to PENDING def handle_completed(event): print(f"Verification {event['verificationId']} ({event.get('type', 'individual')}) → {event['decision']}") # Update your database with the final decision def handle_deleted(event): print(f"Verification {event['verificationId']} deleted") # Reset the user's local kycStatus ``` --- ## Deduplicating Events Webhooks may be delivered more than once — a retry, a redelivery-sweep re-send, or a manual [resend](/api-reference#delete-verification). Every payload carries an `eventId`: the **same** logical event always produces the **same** `eventId`, across every retry and redelivery, so use it as your dedupe key instead of `verificationId` (a single verification legitimately fires multiple *different* events over its lifetime — `submitted`, `completed`, and potentially a later re-fire if a review or an AML rescreen changes the decision — each with its own `eventId`). ```javascript const processedEventIds = new Set(); // use a persistent store in production (Redis/DB), not an in-memory Set function handleWebhook(event) { if (processedEventIds.has(event.eventId)) { console.log('Duplicate delivery, skipping', event.eventId); return; } processedEventIds.add(event.eventId); // Process the event... } ``` ::: tip Falling back to `verificationId` If you're on an older integration that doesn't yet key off `eventId`, deduping on `verificationId` alone for `verification.completed` is still reasonably safe *as long as you also key on `decision`* — a genuine re-fire (e.g. an AML rescreen flipping a cleared verification to `needs_review`) is a materially different event you do want to process, not a duplicate to skip. ::: --- ## Webhook Retry Policy If your webhook endpoint returns 5xx or times out, YeboVerify retries the webhook with a short exponential backoff. **`verification.completed` is retried** because final decisions must reach you. **`verification.submitted` and `verification.deleted` are fire-and-forget** — they're best-effort UI signals, and the eventual `verification.completed` will always bring your state to truth. ### `verification.completed` (3 attempts, ~6s total) | Attempt | Delay before this attempt | |---------|---------------------------| | 1 | Immediate | | 2 | 1 second | | 3 | 2 seconds | If your endpoint returns 4xx (client error), retries stop immediately — fix your handler and we'll back-fill via the next webhook on a new verification. After 3 failed attempts the webhook is marked failed and no further retries are attempted from the in-process path — terminal `verification.completed` deliveries that are still unmarked `webhookSent` are also picked up by a background redelivery sweep over a longer horizon. ### `verification.submitted` and `verification.deleted` (1 attempt) Single best-effort send. If delivery fails, no retry. Your handler should be idempotent and stateless on these — `verification.completed` is the source of truth for the final state. --- ## Best Practices ### 1. Return 200 Quickly Always return a `200 OK` response as quickly as possible. Process the webhook data asynchronously. ```javascript app.post('/webhooks/yeboverify', (req, res) => { // Respond immediately res.status(200).send('OK'); // Process asynchronously processWebhookAsync(req.body).catch(console.error); }); ``` ### 2. Deduplicate on `eventId` See [Deduplicating Events](#deduplicating-events) above. ### 3. Verify Signatures with `X-YeboVerify-Signature-V2` Always verify the V2 signature (and its timestamp skew) before processing. This prevents both spoofed and replayed webhooks — see [Webhook Signature Verification](#webhook-signature-verification). ### 4. Use HTTPS Always use HTTPS for your webhook endpoint. YeboVerify will not send webhooks to HTTP URLs. ### 5. Handle Timeouts Your webhook endpoint should respond within 30 seconds. If processing takes longer, use a queue: ```javascript const { Queue } = require('bullmq'); const webhookQueue = new Queue('webhooks'); app.post('/webhooks/yeboverify', async (req, res) => { // Add to queue for async processing await webhookQueue.add('verification.completed', req.body); res.status(200).send('OK'); }); ``` --- ## Testing Webhooks ### Using ngrok for Local Development 1. Install ngrok: `npm install -g ngrok` 2. Start your local server: `npm run dev` 3. Expose it: `ngrok http 3000` 4. Update your webhook URL to the ngrok URL ### Manual Webhook Test You can simulate a webhook for testing (a hand-built test signature won't verify against a real secret — this is for exercising your handler's routing/parsing, not signature checks): ```bash curl -X POST http://localhost:3000/webhooks/yeboverify \ -H "Content-Type: application/json" \ -H "X-YeboVerify-Timestamp: $(date +%s)" \ -H "X-YeboVerify-Signature-V2: sha256=test_signature" \ -d '{ "event": "verification.completed", "eventId": "evt_test000000000000000000", "type": "individual", "verificationId": "vrf_test_123", "status": "completed", "decision": "approved", "confidence": "high", "faceScore": 92.3, "aml": { "decision": "clear", "score": 0, "hits": [] }, "timestamp": "2026-05-08T12:00:00.000Z" }' ``` ================================================================ # /examples # Source: https://docs.yeboverify.com/examples ================================================================ # YeboVerify Code Examples ## cURL ### Selfie Match Verification ```bash curl -X POST https://api.yeboverify.com/v1/verify/selfie-match \ -H "X-API-Key: your_api_key" \ -F "selfie=@/path/to/selfie.jpg" \ -F "profilePictureUrl=https://example.com/user/profile.jpg" \ -F "externalRef=user_123" ``` ### ID Document Verification ```bash curl -X POST https://api.yeboverify.com/v1/verify/id-document \ -H "X-API-Key: your_api_key" \ -F "selfie=@/path/to/selfie.jpg" \ -F "idFront=@/path/to/id-front.jpg" \ -F "idBack=@/path/to/id-back.jpg" \ -F "externalRef=user_123" ``` ### List Verifications ```bash curl -X GET "https://api.yeboverify.com/v1/verifications?page=1&limit=20" \ -H "X-API-Key: your_api_key" ``` ### Get Verification Details ```bash curl -X GET https://api.yeboverify.com/v1/verifications/vrf_id_abc123 \ -H "X-API-Key: your_api_key" ``` --- ## Node.js ### Installation ```bash npm install node-fetch form-data # or with axios npm install axios form-data ``` ### Selfie Match Verification ```javascript const fs = require('fs'); const FormData = require('form-data'); async function verifySelfieMatch(selfiePath, profilePictureUrl, externalRef) { const form = new FormData(); form.append('selfie', fs.createReadStream(selfiePath)); form.append('profilePictureUrl', profilePictureUrl); if (externalRef) { form.append('externalRef', externalRef); } const response = await fetch('https://api.yeboverify.com/v1/verify/selfie-match', { method: 'POST', headers: { 'X-API-Key': process.env.YEBOVERIFY_API_KEY, ...form.getHeaders(), }, body: form, }); const result = await response.json(); if (!result.success) { throw new Error(result.error.message); } return result.data; } // Usage const result = await verifySelfieMatch( './selfie.jpg', 'https://example.com/profile.jpg', 'user_123' ); console.log('Same person:', result.samePerson); console.log('Match score:', result.matchScore); console.log('Confidence:', result.confidence); ``` ### ID Document Verification ```javascript const fs = require('fs'); const FormData = require('form-data'); async function verifyIDDocument(selfiePath, idFrontPath, idBackPath, externalRef) { const form = new FormData(); form.append('selfie', fs.createReadStream(selfiePath)); form.append('idFront', fs.createReadStream(idFrontPath)); if (idBackPath) { form.append('idBack', fs.createReadStream(idBackPath)); } if (externalRef) { form.append('externalRef', externalRef); } const response = await fetch('https://api.yeboverify.com/v1/verify/id-document', { method: 'POST', headers: { 'X-API-Key': process.env.YEBOVERIFY_API_KEY, ...form.getHeaders(), }, body: form, }); const result = await response.json(); if (!result.success) { throw new Error(result.error.message); } return result.data; } // Usage const result = await verifyIDDocument( './selfie.jpg', './id-front.jpg', './id-back.jpg', 'user_123' ); console.log('Decision:', result.decision); console.log('Name:', result.documentData.names, result.documentData.surname); console.log('ID Number:', result.documentData.idNumber); ``` ### YeboVerify Client Class ```javascript const fs = require('fs'); const FormData = require('form-data'); class YeboVerifyClient { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.yeboverify.com'; } async selfieMatch(selfiePath, profilePictureUrl, options = {}) { const form = new FormData(); form.append('selfie', fs.createReadStream(selfiePath)); form.append('profilePictureUrl', profilePictureUrl); if (options.externalRef) { form.append('externalRef', options.externalRef); } return this._request('POST', '/v1/verify/selfie-match', form); } async idDocument(selfiePath, idFrontPath, options = {}) { const form = new FormData(); form.append('selfie', fs.createReadStream(selfiePath)); form.append('idFront', fs.createReadStream(idFrontPath)); if (options.idBackPath) { form.append('idBack', fs.createReadStream(options.idBackPath)); } if (options.externalRef) { form.append('externalRef', options.externalRef); } if (options.documentType) { form.append('documentType', options.documentType); } return this._request('POST', '/v1/verify/id-document', form); } async listVerifications(options = {}) { const params = new URLSearchParams(); if (options.page) params.append('page', options.page); if (options.limit) params.append('limit', options.limit); if (options.status) params.append('status', options.status); const query = params.toString() ? `?${params.toString()}` : ''; return this._request('GET', `/v1/verifications${query}`); } async getVerification(verificationId) { return this._request('GET', `/v1/verifications/${verificationId}`); } async _request(method, path, body = null) { const headers = { 'X-API-Key': this.apiKey, }; if (body instanceof FormData) { Object.assign(headers, body.getHeaders()); } const response = await fetch(`${this.baseUrl}${path}`, { method, headers, body, }); const result = await response.json(); if (!result.success) { const error = new Error(result.error.message); error.code = result.error.code; throw error; } return result.data; } } // Usage const client = new YeboVerifyClient(process.env.YEBOVERIFY_API_KEY); // Selfie match const selfieResult = await client.selfieMatch('./selfie.jpg', 'https://example.com/profile.jpg'); // ID document verification const idResult = await client.idDocument('./selfie.jpg', './id-front.jpg', { idBackPath: './id-back.jpg', externalRef: 'user_123', }); // List verifications const verifications = await client.listVerifications({ page: 1, limit: 10 }); ``` --- ## Python ### Installation ```bash pip install requests ``` ### Selfie Match Verification ```python import requests import os def verify_selfie_match(selfie_path, profile_picture_url, external_ref=None): url = 'https://api.yeboverify.com/v1/verify/selfie-match' headers = { 'X-API-Key': os.environ.get('YEBOVERIFY_API_KEY') } with open(selfie_path, 'rb') as selfie_file: files = { 'selfie': selfie_file } data = { 'profilePictureUrl': profile_picture_url } if external_ref: data['externalRef'] = external_ref response = requests.post(url, headers=headers, files=files, data=data) result = response.json() if not result.get('success'): raise Exception(result['error']['message']) return result['data'] # Usage result = verify_selfie_match( './selfie.jpg', 'https://example.com/profile.jpg', 'user_123' ) print(f"Same person: {result['samePerson']}") print(f"Match score: {result['matchScore']}") print(f"Confidence: {result['confidence']}") ``` ### ID Document Verification ```python import requests import os def verify_id_document(selfie_path, id_front_path, id_back_path=None, external_ref=None): url = 'https://api.yeboverify.com/v1/verify/id-document' headers = { 'X-API-Key': os.environ.get('YEBOVERIFY_API_KEY') } files = {} data = {} try: files['selfie'] = open(selfie_path, 'rb') files['idFront'] = open(id_front_path, 'rb') if id_back_path: files['idBack'] = open(id_back_path, 'rb') if external_ref: data['externalRef'] = external_ref response = requests.post(url, headers=headers, files=files, data=data) finally: for f in files.values(): f.close() result = response.json() if not result.get('success'): raise Exception(result['error']['message']) return result['data'] # Usage result = verify_id_document( './selfie.jpg', './id-front.jpg', './id-back.jpg', 'user_123' ) print(f"Decision: {result['decision']}") print(f"Name: {result['documentData']['names']} {result['documentData']['surname']}") print(f"ID Number: {result['documentData']['idNumber']}") ``` ### YeboVerify Client Class ```python import requests import os from typing import Optional, Dict, Any class YeboVerifyClient: def __init__(self, api_key: str): self.api_key = api_key self.base_url = 'https://api.yeboverify.com' def selfie_match( self, selfie_path: str, profile_picture_url: str, external_ref: Optional[str] = None ) -> Dict[str, Any]: """Compare a selfie against a profile picture URL.""" with open(selfie_path, 'rb') as selfie_file: files = {'selfie': selfie_file} data = {'profilePictureUrl': profile_picture_url} if external_ref: data['externalRef'] = external_ref return self._request('POST', '/v1/verify/selfie-match', files=files, data=data) def id_document( self, selfie_path: str, id_front_path: str, id_back_path: Optional[str] = None, external_ref: Optional[str] = None, document_type: Optional[str] = None ) -> Dict[str, Any]: """Full ID document verification with OCR and face matching.""" files = {} data = {} try: files['selfie'] = open(selfie_path, 'rb') files['idFront'] = open(id_front_path, 'rb') if id_back_path: files['idBack'] = open(id_back_path, 'rb') if external_ref: data['externalRef'] = external_ref if document_type: data['documentType'] = document_type return self._request('POST', '/v1/verify/id-document', files=files, data=data) finally: for f in files.values(): f.close() def list_verifications( self, page: int = 1, limit: int = 20, status: Optional[str] = None ) -> Dict[str, Any]: """List verifications for your business.""" params = {'page': page, 'limit': limit} if status: params['status'] = status return self._request('GET', '/v1/verifications', params=params) def get_verification(self, verification_id: str) -> Dict[str, Any]: """Get details of a specific verification.""" return self._request('GET', f'/v1/verifications/{verification_id}') def _request( self, method: str, path: str, files: Optional[Dict] = None, data: Optional[Dict] = None, params: Optional[Dict] = None ) -> Dict[str, Any]: headers = {'X-API-Key': self.api_key} response = requests.request( method, f'{self.base_url}{path}', headers=headers, files=files, data=data, params=params ) result = response.json() if not result.get('success'): raise YeboVerifyError( result['error']['message'], result['error']['code'] ) return result['data'] class YeboVerifyError(Exception): def __init__(self, message: str, code: str): self.message = message self.code = code super().__init__(f"[{code}] {message}") # Usage client = YeboVerifyClient(os.environ.get('YEBOVERIFY_API_KEY')) # Selfie match selfie_result = client.selfie_match('./selfie.jpg', 'https://example.com/profile.jpg') # ID document verification id_result = client.id_document( './selfie.jpg', './id-front.jpg', id_back_path='./id-back.jpg', external_ref='user_123' ) # List verifications verifications = client.list_verifications(page=1, limit=10) ``` --- ## Browser (JavaScript/TypeScript) ### Selfie Match with File Input ```javascript async function verifySelfie(selfieFile, profilePictureUrl) { const formData = new FormData(); formData.append('selfie', selfieFile); formData.append('profilePictureUrl', profilePictureUrl); const response = await fetch('https://api.yeboverify.com/v1/verify/selfie-match', { method: 'POST', headers: { 'X-API-Key': 'your_api_key', // Note: Don't expose API keys in client-side code! }, body: formData, }); return response.json(); } // Usage with file input document.getElementById('verifyBtn').addEventListener('click', async () => { const selfieInput = document.getElementById('selfieInput'); const selfieFile = selfieInput.files[0]; if (!selfieFile) { alert('Please select a selfie'); return; } try { const result = await verifySelfie(selfieFile, 'https://example.com/profile.jpg'); if (result.success) { console.log('Match score:', result.data.matchScore); console.log('Same person:', result.data.samePerson); } else { console.error('Error:', result.error.message); } } catch (error) { console.error('Request failed:', error); } }); ``` ### Using Base64 Images ```javascript // Convert file to base64 function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = reject; }); } // Convert base64 to Blob for FormData function base64ToBlob(base64, mimeType) { const byteString = atob(base64.split(',')[1]); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new Blob([ab], { type: mimeType }); } // Usage const base64Image = await fileToBase64(file); const blob = base64ToBlob(base64Image, 'image/jpeg'); const formData = new FormData(); formData.append('selfie', blob, 'selfie.jpg'); ``` > ⚠️ **Security Warning**: Never expose your API key in client-side code. Always make API calls from your backend server. ================================================================ # /error-codes # Source: https://docs.yeboverify.com/error-codes ================================================================ # Error Codes Reference All API errors follow a consistent format: ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable error description" } } ``` --- ## Authentication Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `MISSING_API_KEY` | 401 | `X-API-Key` header is missing | | `INVALID_API_KEY` | 401 | The provided API key is invalid or doesn't exist | | `ACCOUNT_DISABLED` | 403 | Your business account has been disabled | | `MISSING_AUTH` | 401 | Authorization header is missing (admin routes) | | `INVALID_TOKEN` | 401 | Invalid admin token | | `NOT_AUTHENTICATED` | 401 | Request is not authenticated | ### Examples **Missing API Key:** ```json { "success": false, "error": { "code": "MISSING_API_KEY", "message": "X-API-Key header is required" } } ``` **Invalid API Key:** ```json { "success": false, "error": { "code": "INVALID_API_KEY", "message": "Invalid API key" } } ``` --- ## Validation Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `VALIDATION_ERROR` | 400 | Request body/parameters validation failed | | `MISSING_FILES` | 400 | No files were uploaded | | `MISSING_SELFIE` | 400 | Selfie image is required but not provided | | `MISSING_ID_FRONT` | 400 | ID front image is required but not provided | | `MISSING_ID` | 400 | Verification ID parameter is required | | `INVALID_FILE_TYPE` | 400 | Uploaded file type is not supported | | `FILE_TOO_LARGE` | 400 | Uploaded file exceeds the size limit (10MB) | ### Examples **Missing Selfie:** ```json { "success": false, "error": { "code": "MISSING_SELFIE", "message": "Selfie image is required" } } ``` **Invalid File Type:** ```json { "success": false, "error": { "code": "INVALID_FILE_TYPE", "message": "Invalid file type. Allowed types: JPEG, PNG, WebP" } } ``` **Validation Error:** ```json { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Invalid profile picture URL" } } ``` --- ## Verification Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `VERIFICATION_ERROR` | 500 | General verification processing error | | `PROFILE_PICTURE_ERROR` | 400 | Failed to fetch the profile picture URL | | `FACE_COMPARISON_ERROR` | 400 | Face comparison failed (unclear photos) | | `SERVICE_UNAVAILABLE` | 503 | Required service (Rekognition/OCR) is unavailable | | `NOT_FOUND` | 404 | Verification not found | ### Examples **Profile Picture Fetch Error:** ```json { "success": false, "error": { "code": "PROFILE_PICTURE_ERROR", "message": "Failed to fetch profile picture from URL: HTTP 404" } } ``` **Face Comparison Error:** ```json { "success": false, "error": { "code": "FACE_COMPARISON_ERROR", "message": "Face comparison failed. Please ensure clear photos of face." } } ``` **Service Unavailable:** ```json { "success": false, "error": { "code": "SERVICE_UNAVAILABLE", "message": "Face comparison service is not available" } } ``` --- ## Session Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `SESSION_EXPIRED` | 401 | Session token has expired | | `INVALID_SESSION` | 401 | Session token is invalid or malformed | ### Examples **Session Expired:** ```json { "success": false, "error": "Session has expired" } ``` --- ## General Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `NOT_FOUND` | 404 | Endpoint or resource not found | | `INTERNAL_ERROR` | 500 | Unexpected server error | | `AUTH_ERROR` | 500 | Internal authentication error | ### Examples **Endpoint Not Found:** ```json { "success": false, "error": { "code": "NOT_FOUND", "message": "Endpoint not found" } } ``` --- ## Handling Errors ### Node.js Example ```javascript async function verifyWithErrorHandling(selfiePath, profileUrl) { try { const result = await client.selfieMatch(selfiePath, profileUrl); return result; } catch (error) { switch (error.code) { case 'MISSING_API_KEY': case 'INVALID_API_KEY': console.error('Authentication error - check your API key'); break; case 'PROFILE_PICTURE_ERROR': console.error('Could not fetch profile picture - check the URL'); break; case 'FACE_COMPARISON_ERROR': console.error('Face comparison failed - photos may be unclear'); break; case 'SERVICE_UNAVAILABLE': console.error('Service temporarily unavailable - retry later'); break; default: console.error('Unknown error:', error.message); } throw error; } } ``` ### Python Example ```python from yeboverify import YeboVerifyClient, YeboVerifyError client = YeboVerifyClient(api_key) try: result = client.selfie_match(selfie_path, profile_url) except YeboVerifyError as e: if e.code == 'MISSING_API_KEY' or e.code == 'INVALID_API_KEY': print('Authentication error - check your API key') elif e.code == 'PROFILE_PICTURE_ERROR': print('Could not fetch profile picture - check the URL') elif e.code == 'FACE_COMPARISON_ERROR': print('Face comparison failed - photos may be unclear') elif e.code == 'SERVICE_UNAVAILABLE': print('Service temporarily unavailable - retry later') else: print(f'Unknown error: {e.message}') raise ``` --- ## Rate Limiting When you exceed your rate limit, you'll receive: ```json { "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests. Please try again later." } } ``` **HTTP Status:** 429 Too Many Requests **Headers:** - `X-RateLimit-Limit`: Your rate limit - `X-RateLimit-Remaining`: Remaining requests - `X-RateLimit-Reset`: Unix timestamp when the limit resets ### Handling Rate Limits ```javascript async function verifyWithRetry(selfiePath, profileUrl, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await client.selfieMatch(selfiePath, profileUrl); } catch (error) { if (error.code === 'RATE_LIMIT_EXCEEDED' && attempt < maxRetries - 1) { const waitMs = Math.pow(2, attempt) * 1000; // Exponential backoff console.log(`Rate limited. Waiting ${waitMs}ms...`); await new Promise(resolve => setTimeout(resolve, waitMs)); continue; } throw error; } } } ``` ================================================================ # /kyb # Source: https://docs.yeboverify.com/kyb ================================================================ # Company Verification (KYB) KYB (Know Your Business) verifies a company as an entity: its registration documents, an authorized signatory's identity, and an AML/sanctions screen of the registered name. It runs on a separate set of endpoints from individual KYC (`/v1/verify`, `/v1/sessions/*`) but shares the same authentication, webhook delivery, and review model. ## How it works A company verification aggregates three independent checks into one decision: 1. **Documents** — every document your country's requirements matrix marks `required` must be uploaded and accepted (registration certificate, proof of address, etc.). 2. **Signatory identity** — the authorized signatory completes a normal individual KYC check (ID + selfie) through a hosted link you send them. 3. **AML screening** — the registered company name is screened for sanctions/PEP/watchlist hits the moment the verification is created. The terminal decision is **computed**, not asserted: `REJECT` dominates `NEEDS_REVIEW` dominates `APPROVED`. Any required document rejected, or the signatory rejected, rejects the whole verification. Anything still incomplete (a document not yet accepted, the signatory not yet done) parks the verification in `NEEDS_REVIEW` rather than guessing. ``` Create → AML screen (name) + resolve required documents │ ├── Upload documents (you, or the signatory via the hosted link) ├── Signatory completes ID + selfie via verifyUrl │ └── Every required doc accepted + signatory approved + AML clear → status: COMPLETED, decision: APPROVED Any required doc rejected, or signatory rejected → status: COMPLETED, decision: REJECTED Still incomplete, or an AML hit → status: NEEDS_REVIEW ``` `NEEDS_REVIEW` verifications resolve automatically as documents get accepted and the signatory finishes — or an operator can [manually review](#manually-review-a-verification) them. Either path fires the `verification.completed` webhook once the outcome is terminal. --- ## 1. Create a Company Verification ``` POST /v1/company-verifications ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `application/json` | | `Idempotency-Key` | string | No | Repeat the same key to safely retry without creating a duplicate — see [Idempotent replay](#idempotent-replay) | **Request Body:** ```json { "country": "SZ", "registeredName": "Acme Trading (Pty) Ltd", "registrationNumber": "REG-2019-004521", "registeredAddress": "12 Somhlolo Road, Mbabane", "taxId": "TIN-882910", "incorporationDate": "2019-03-14T00:00:00.000Z", "externalRef": "biz_acme_001" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `country` | string | Yes | ISO 3166-1 alpha-2 country code. Must have an active requirements matrix (see [Supported countries](#supported-countries)) — an unsupported country fails loudly rather than silently skipping requirements. | | `registeredName` | string | Yes | Legal/registered company name. This is what gets AML-screened. | | `registrationNumber` | string | No | Company registration number | | `registeredAddress` | string | No | Registered office address | | `taxId` | string | No | Tax identification number | | `incorporationDate` | string | No | ISO 8601 date. Affects which documents are required — see [Required Documents](#required-documents) | | `externalRef` | string | No | Your internal reference ID | **Response `202 Accepted`:** ```json { "success": true, "data": { "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2", "status": "pending", "requiredDocuments": [ { "docType": "company_registration", "label": "Certificate of Incorporation / Registration", "required": true, "sortOrder": 10 }, { "docType": "form_j", "label": "Form J — Return of Directors & Officers", "required": true, "sortOrder": 20 }, { "docType": "form_c", "label": "Form C — Notice of Registered Office & Postal Address", "required": true, "sortOrder": 30 }, { "docType": "trading_license", "label": "Municipal trading license", "required": true, "sortOrder": 40 }, { "docType": "proof_of_address", "label": "Utility bill / lease (≤3 months)", "required": true, "sortOrder": 50 }, { "docType": "director_id", "label": "Authorized signatory ID", "required": true, "sortOrder": 60 }, { "docType": "tax_clearance", "label": "SRA tax clearance / TIN", "required": false, "sortOrder": 70 } ], "signatorySessionToken": "eyJhbGciOiJIUzI1NiIs...", "verifyUrl": "https://verify.yeboverify.com#session=eyJhbGciOiJIUzI1NiIs..." } } ``` | Field | Type | Description | |-------|------|--------------| | `verificationId` | string | Unique company verification ID (prefixed `cv_`) | | `status` | string | `pending` immediately after creation | | `requiredDocuments` | array | The resolved document checklist for this company — see [Required Documents](#required-documents) | | `signatorySessionToken` | string | Session token for the signatory's individual identity check. Same shape as a normal `/v1/sessions/create` token. | | `verifyUrl` | string | Send the authorized signatory here to complete their ID + selfie check. | ::: warning `verifyUrl` carries the session token in the URL fragment `#session=...`, not `?session=...`. The token is deliberately kept out of the query string so it never reaches your server logs, analytics, or the `Referer` header of any page the signatory navigates to next. If you're constructing this URL yourself (rather than using the one returned in the response), preserve the `#`. ::: ### Idempotent replay Pass the same `Idempotency-Key` header on a retried request (e.g. after a network timeout) and you get back the **existing** verification instead of a duplicate — no second AML screen, no second billed usage event. The response is `200 OK` instead of `202 Accepted`, and every field (including a freshly-derived `signatorySessionToken` and `verifyUrl`, since session tokens expire) reflects the original record's current state. ### Errors | Code | HTTP Status | Description | |------|-------------|-------------| | `KYB_COUNTRY_UNSUPPORTED` | 400 | No active requirements matrix exists for `country` yet | | `VALIDATION_ERROR` | 400 | Request body failed validation (e.g. `country` isn't a 2-letter code) | | `NOT_AUTHENTICATED` | 401 | Missing/invalid `X-API-Key` | A `402` (quota exceeded) is possible too — a company verification is metered as one billable `company_verification` usage event, separate from the signatory's individual KYC event. --- ## Required Documents Which documents are required varies by country and, for some document types, by how old the company is. `createCompanyVerification`'s response already gives you the resolved checklist for this specific company — you don't need to look up the matrix separately. ### Supported countries Today Eswatini (`SZ`) is the only seeded country. Requesting any other country returns `KYB_COUNTRY_UNSUPPORTED`. Reach out if you need another country enabled — the matrix is per-country configurable, not hardcoded. ### Eswatini (SZ) document set | docType | Label | Required | |---------|-------|----------| | `company_registration` | Certificate of Incorporation / Registration | Always | | `form_j` | Form J — Return of Directors & Officers | Always | | `form_c` | Form C — Notice of Registered Office & Postal Address | Only if the company is ≥1 year old (unknown incorporation date defaults to required) | | `trading_license` | Municipal trading license | Always | | `proof_of_address` | Utility bill / lease (≤3 months) | Always | | `director_id` | Authorized signatory ID | Always | | `tax_clearance` | SRA tax clearance / TIN | Optional | The `form_c` condition is the one exception to "always required": pass `incorporationDate` on create so a genuinely young company isn't blocked waiting on a document it may not have yet. If you omit `incorporationDate`, it's treated as required (unknown age never waives a requirement). --- ## Uploading Documents Documents can come from either side of the relationship — whichever fits your flow: ### From your backend (API key auth) ``` POST /v1/company-verifications/:verificationId/documents ``` **Headers:** | Header | Type | Required | Description | |--------|------|----------|-------------| | `X-API-Key` | string | Yes | Your API key | | `Content-Type` | string | Yes | `multipart/form-data` | **Request Body (multipart/form-data):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `document` | file | Yes | The document file (JPEG, PNG, or PDF, max 10MB) | | `docType` | string | Yes | One of the `docType` values from `requiredDocuments` (e.g. `company_registration`) | **Response `201 Created`:** ```json { "success": true, "data": { "documentId": "clxxxxxxxxxxxx", "docType": "company_registration", "status": "uploaded" } } ``` ### From the hosted signatory flow (session auth) When the signatory is completing documents through the `verifyUrl` link (e.g. a hosted upload step in `verify-app` alongside their ID + selfie), use the session-authenticated counterpart instead of your API key: ``` POST /v1/sessions/company-documents ``` **Request Body (multipart/form-data):** | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | Yes | The `signatorySessionToken` (or the token from the `verifyUrl` fragment) | | `docType` | string | Yes | One of the required `docType` values | | `document` | file | Yes | The document file | **Response:** ```json { "success": true, "docType": "company_registration", "status": "uploaded" } ``` Both paths land the document in the same place — a document uploaded by the signatory is immediately visible to you via `GET /v1/company-verifications/:verificationId`. Uploaded documents start in `uploaded` status; they only count toward the `requiredDocuments` checklist once an operator marks them `accepted` via [review](#manually-review-a-verification). --- ## The Signatory Identity Check Every company verification has exactly one linked individual verification: the authorized signatory's ID + selfie check, run through the same hosted flow individual KYC uses (see [Mobile Integration](/mobile-integration) and [Getting Started](/getting-started)). - Send the signatory to the `verifyUrl` from the create response (or from `GET /v1/company-verifications/:verificationId` if they need it again). - They complete the normal ID + selfie capture in their browser — nothing KYB-specific from their side. - When their individual verification completes, it's automatically linked back to the company verification and the aggregate decision recomputes — you don't call anything to "attach" it. - The linked result surfaces on the company verification as `signatoryVerificationId` and, once decided, on the `verification.completed` webhook's `company.signatoryDecision`. If the signatory abandons the flow or their individual check ends in `NEEDS_REVIEW`, the company verification stays `NEEDS_REVIEW` too — it can't reach a terminal decision without a resolved signatory. --- ## Reading Verifications ### Get a Company Verification ``` GET /v1/company-verifications/:verificationId ``` **Response:** ```json { "success": true, "data": { "id": "clxxxxxxxxxxxx", "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2", "externalRef": "biz_acme_001", "country": "SZ", "registeredName": "Acme Trading (Pty) Ltd", "registrationNumber": "REG-2019-004521", "status": "NEEDS_REVIEW", "decision": null, "decisionReason": null, "amlDecision": "clear", "amlScore": 0, "signatoryVerificationId": "vrf_id_xyz789abc", "documents": [ { "id": "clyyyyyyyyyyyy", "docType": "company_registration", "status": "uploaded", "signedUrl": "https://cdn.yeboverify.com/company-verifications/...", "createdAt": "2026-05-08T12:00:00.000Z" } ], "reviewHistory": [], "webhookSent": false, "createdAt": "2026-05-08T12:00:00.000Z", "completedAt": null } } ``` `documents[].signedUrl` is a time-limited (1 hour) signed URL to view the uploaded file — re-fetch this endpoint if it expires. `reviewHistory` is the append-only audit trail of every review decision applied to this verification (mirrors the individual KYC audit trail). ### List Company Verifications ``` GET /v1/company-verifications ``` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | number | 1 | Page number | | `limit` | number | 20 | Results per page (max 100) | | `status` | string | – | Filter by `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `NEEDS_REVIEW` | **Response:** ```json { "success": true, "data": { "items": [ { "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2", "registeredName": "Acme Trading (Pty) Ltd", "status": "NEEDS_REVIEW", "decision": null, "createdAt": "2026-05-08T12:00:00.000Z" } ], "total": 1, "page": 1, "limit": 20 } } ``` --- ## Manually Review a Verification Operators (or your own back office, via the API key) resolve a `NEEDS_REVIEW` company verification by accepting/rejecting each uploaded document. The terminal decision is then **recomputed** from the aggregate — you never set `decision` directly, the same way individual KYC review works. ``` POST /v1/company-verifications/:verificationId/review ``` **Request Body:** ```json { "documentReviews": [ { "documentId": "clyyyyyyyyyyyy", "status": "accepted" }, { "documentId": "clzzzzzzzzzzzz", "status": "rejected", "note": "Certificate is expired" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `documentReviews` | array | Yes | At least one verdict | | `documentReviews[].documentId` | string | Yes | A document ID belonging to this verification | | `documentReviews[].status` | string | Yes | `accepted` or `rejected` | | `documentReviews[].note` | string | No | Reviewer note, max 1000 chars | Reviewer identity is derived entirely from the authenticated API key (name/prefix) — there's no `reviewedBy` free-text field to spoof. **Response:** ```json { "success": true, "data": { "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2", "status": "COMPLETED", "decision": "REJECTED" } } ``` **Errors:** | Code | HTTP Status | Description | |------|-------------|-------------| | `NOT_FOUND` | 404 | Verification doesn't exist for your business | | `INVALID_DOCUMENT` | 400 | One or more `documentId`s don't belong to this verification | | `NOT_REVIEWABLE` | 409 | Verification isn't `PENDING`/`NEEDS_REVIEW` (already terminal, or a concurrent review won the race) | A review doesn't have to immediately produce a terminal decision — if you accept some documents but the signatory hasn't finished yet, the verification stays `NEEDS_REVIEW` and will recompute again automatically once the signatory result lands. --- ## Resend Webhook If your endpoint missed the `verification.completed` delivery for a terminal company verification, re-trigger it manually: ``` POST /v1/company-verifications/:verificationId/resend-webhook ``` Returns `404` if not found for your business, `409` if the verification hasn't reached a terminal decision yet, `400` if you don't have a `webhookUrl` configured (see [Webhooks](/webhooks)). --- ## Webhook Payload KYB delivers through the exact same `verification.completed` webhook individual KYC uses — same signing, same retry policy, same `X-YeboVerify-Signature-V2` scheme — distinguished by a `type: "company"` field and a `company` block instead of the person-specific fields (`faceScore`, `ocrConfidence`, `extractedData`). ```json { "event": "verification.completed", "eventId": "evt_1a2b3c4d5e6f7a8b9c0d1e2f", "type": "company", "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2", "externalRef": "biz_acme_001", "status": "completed", "decision": "approved", "confidence": "high", "aml": { "decision": "clear", "score": 0, "hits": [] }, "company": { "registeredName": "Acme Trading (Pty) Ltd", "registrationNumber": "REG-2019-004521", "country": "SZ", "signatoryVerificationId": "vrf_id_xyz789abc", "signatoryDecision": "approved", "documents": [ { "docType": "company_registration", "status": "accepted" }, { "docType": "form_j", "status": "accepted" } ] }, "timestamp": "2026-05-08T12:05:00.000Z" } ``` | Field | Type | Description | |-------|------|--------------| | `type` | string | `"company"` — the KYB discriminator. Individual KYC payloads send `type: "individual"`; switch on it before assuming person-shaped fields exist. | | `company.registeredName` / `registrationNumber` / `country` | string | Echoes what you submitted at creation | | `company.signatoryVerificationId` | string \| null | The linked individual verification ID | | `company.signatoryDecision` | string \| null | `approved`, `rejected`, `needs_review`, or `null` if the signatory hasn't finished | | `company.documents` | array | Every uploaded document's final `docType`/`status` | | `aml` | object | Same AML block individual payloads carry — see [Webhooks → AML](/webhooks#aml-screening-result) | See [Webhooks](/webhooks) for signature verification (`X-YeboVerify-Signature-V2`), retry policy, and `eventId`-based dedupe — all of it applies identically to `type: "company"` deliveries. --- ## End-to-End Example ```bash # 1. Create the company verification curl -X POST https://api.yeboverify.com/v1/company-verifications \ -H "X-API-Key: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "country": "SZ", "registeredName": "Acme Trading (Pty) Ltd", "registrationNumber": "REG-2019-004521", "incorporationDate": "2019-03-14T00:00:00.000Z", "externalRef": "biz_acme_001" }' # → { verificationId: "cv_...", requiredDocuments: [...], verifyUrl: "https://verify.yeboverify.com#session=..." } # 2. Upload the documents you already have on file curl -X POST https://api.yeboverify.com/v1/company-verifications/cv_.../documents \ -H "X-API-Key: your_api_key" \ -F "docType=company_registration" \ -F "document=@registration_certificate.pdf" # 3. Send the signatory the verifyUrl to complete their ID + selfie check # (they do this in a browser — nothing further to call here) # 4. Once documents are uploaded, accept/reject them curl -X POST https://api.yeboverify.com/v1/company-verifications/cv_.../review \ -H "X-API-Key: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "documentReviews": [{ "documentId": "cl...", "status": "accepted" }] }' # 5. Listen for verification.completed on your webhook, or poll: curl https://api.yeboverify.com/v1/company-verifications/cv_... \ -H "X-API-Key: your_api_key" ``` ================================================================ # /sdks # Source: https://docs.yeboverify.com/sdks ================================================================ # SDKs & Libraries Official SDKs for integrating YeboVerify into your applications. All source lives under the [`omegathesecond` GitHub org](https://github.com/omegathesecond?q=yeboverify-). ## Available SDKs | Platform | Repository | Use case | |---|---|---| | **Flutter** | [`yeboverify-flutter`](https://github.com/omegathesecond/yeboverify-flutter) | iOS + Android via Flutter | | **Kotlin** | [`yeboverify-kotlin`](https://github.com/omegathesecond/yeboverify-kotlin) | Native Android | | **Swift** | [`yeboverify-swift`](https://github.com/omegathesecond/yeboverify-swift) | Native iOS | | **React Native** | [`yeboverify-react-native`](https://github.com/omegathesecond/yeboverify-react-native) | iOS + Android via React Native | | **React** | [`yeboverify-react`](https://github.com/omegathesecond/yeboverify-react) | React web apps + embeddable widgets | | **Node** | [`yeboverify-node`](https://github.com/omegathesecond/yeboverify-node) | Server-side integration | ::: warning These repositories are private The SDK repos are not public and none of the packages are published to npm, pub.dev, Maven Central or the Swift Package Index. The install commands below only work once your GitHub account has been granted access — until then a clone or `npm install` fails to authenticate, and the repo links above return 404 (GitHub hides private repos as 404). Ask us to add your GitHub username and we'll grant it. Installing from `github:` runs the package's build during install, so you need a working toolchain (Node + npm) on the installing machine — including in CI and in any Docker build stage that runs `npm install`. ::: > **Don't see your stack?** The REST API is documented in full at [API Reference](/api-reference) — every SDK is just a thin wrapper. If you're on Python, Go, PHP, Ruby, .NET, or anything else, the API works fine via direct HTTP. Open an issue on any SDK repo if you'd like a first-party SDK for your platform. --- ## Mobile (Flutter / Kotlin / Swift / React Native) These SDKs handle the camera capture + liveness flow on-device, then submit to YeboVerify's API. They're designed for the **hosted-flow pattern** — your backend creates a session, you hand the token to the SDK, and the SDK takes care of the user-facing capture UI. See [Mobile Integration](/mobile-integration) for the full per-platform integration walkthrough. ### Flutter ```yaml # pubspec.yaml dependencies: yeboverify_flutter: git: url: https://github.com/omegathesecond/yeboverify-flutter.git ``` ```dart import 'package:yeboverify_flutter/yeboverify_flutter.dart'; YeboVerifyButton( apiKey: 'yv_live_YOUR_API_KEY', externalRef: currentUser.id, onSuccess: (result) => print('Verified: ${result.names}'), ) ``` ### Kotlin (Android) Add via JitPack or Gradle source dependency. See the [Kotlin SDK README](https://github.com/omegathesecond/yeboverify-kotlin) for the current installation snippet. ### Swift (iOS) Add via Swift Package Manager: ``` https://github.com/omegathesecond/yeboverify-swift ``` See the [Swift SDK README](https://github.com/omegathesecond/yeboverify-swift) for usage. ### React Native ```bash npm install github:omegathesecond/yeboverify-react-native ``` See the [React Native SDK README](https://github.com/omegathesecond/yeboverify-react-native) for usage. --- ## Web (React) A single drop-in `` component for browser-based ID + selfie capture. See [Embeddable Widgets](/widgets) for the full prop reference, liveness behavior, and why it deliberately has no KYB support. ```bash npm install github:omegathesecond/yeboverify-react ``` Not published to npm yet — installs from GitHub, but its `package.json` name is `@yeboverify/react`, so it imports exactly as shown below. ```jsx import { YeboVerifyWidget } from '@yeboverify/react'; console.log('Submitted:', verificationId)} onResult={(result) => { if (result.decision === 'approved') unlockAccess(); }} onError={(err) => toast.error(err.message)} /> ``` `YeboVerifyWidget` is the only export — there is no `useYeboVerify` hook and no separate modal component. --- ## Server (Node) For your backend — creating sessions, fetching verification results, verifying webhook signatures. ```bash npm install github:omegathesecond/yeboverify-node ``` ```javascript import { YeboVerifyClient } from '@yeboverify/node'; const yv = new YeboVerifyClient({ apiKey: process.env.YEBOVERIFY_API_KEY, // yv_live_... or yv_test_... }); // Create a hosted-flow session const session = await yv.sessions.create({ externalRef: 'user_123', callback: 'https://yourapp.com/verify-complete', }); console.log(session.verifyUrl); // https://verify.yeboverify.com#session=... // Get a verification by ID const verification = await yv.verifications.get('vrf_abc123'); // List verifications const list = await yv.verifications.list({ externalRef: 'user_123', limit: 10 }); // Verify a webhook signature against the raw request body const isValid = yv.webhooks.verifySignature(rawBody, signatureHeader, webhookSecret); ``` --- ## Authentication ### API key formats | Key prefix | Where to use | Notes | |---|---|---| | `yv_live_…` | Production backends | Full access. Billable. Calls real verification providers. Never ship to a client bundle. | | `yv_test_…` | Integration testing / CI / local dev | Sandbox mode — see below. Never ship to a client bundle either. | Every business is issued **both** a live key and a test key at signup (`POST /v1/signup` returns `apiKey` + `testApiKey`); you can mint additional named keys of either kind from the console or `POST /v1/account/api-keys` (pass `"isTest": true` for a sandbox key). ::: danger Server-only secrets API keys grant full access to your YeboVerify account. **Never** expose them in client-side code, mobile binaries, or public repositories. For client-side widgets, use a short-lived **session token** issued by your backend via `POST /v1/sessions/create` instead. ::: ### Sandbox mode (`yv_test_` keys) A `yv_test_` key hits the **same** API host as your live key (`api.yeboverify.com`) — there is no separate "test infrastructure" to point at. What changes is behavior: - **Never bills.** A test-mode verification never writes a usage record and never counts against your plan's monthly quota. - **Never calls a paid provider.** OCR (Gemini), face/liveness matching (Rekognition), and AML/sanctions screening are all skipped — the decision is simulated instead. - **No real ID or selfie required.** Since nothing is actually inspected, any placeholder image bytes satisfy the upload — you never need to submit a real photo to integrate. - **Fully separate from live data.** Test verifications never appear in a live key's `GET /v1/verifications`, `GET /v1/verifications/:id`, review queue, or CSV export, and vice versa — a test key can't read or review a live verification either. The operator console shows a persistent "TEST MODE" banner whenever a sandbox key is signed in. - **Forced outcomes via `simulate`.** Pass a `simulate` field (multipart field on `POST /v1/verify`, `POST /v1/verify/id-document`, `POST /v1/verify/selfie-match`; JSON field on `POST /v1/sessions/create`, which carries it through to whatever the end user submits in that hosted session) set to one of: | `simulate` value | Result | |---|---| | `approved` (default if omitted) | `APPROVED`, high confidence | | `rejected` | `REJECTED` — simulated low face-match score | | `needs_review` | `NEEDS_REVIEW` — exercise your manual-review handling | | `liveness_spoofed` | `REJECTED` — simulated anti-spoofing/liveness failure | | `expired_document` | `REJECTED` — simulated expired ID | `simulate` is rejected with a 400 if sent alongside a live key — it only ever applies to sandbox verifications. ```bash curl -X POST https://api.yeboverify.com/v1/verify \ -H "X-API-Key: yv_test_YOUR_TEST_KEY" \ -F "idFront=@any-placeholder.jpg" \ -F "selfie=@any-placeholder.jpg" \ -F "externalRef=test-user-1" \ -F "simulate=needs_review" ``` The response and the `verification.completed` webhook that follows use the exact same shape as a live verification, so you can build and test your entire integration — including your rejection and manual-review handling — before ever switching to `apiKey` / going live. ### Recommended environment setup ```bash # .env (server) YEBOVERIFY_API_URL=https://api.yeboverify.com YEBOVERIFY_API_KEY=yv_live_xxx YEBOVERIFY_WEBHOOK_SECRET=whsec_xxx ``` For local dev / CI / integration tests, swap to your sandbox key on the SAME host — there is no separate dev API key format: ```bash YEBOVERIFY_API_URL=https://api.yeboverify.com YEBOVERIFY_API_KEY=yv_test_xxx ``` (`dev-api.yeboverify.com` is a separate pre-production deployment of the API itself, used to test upcoming API changes — unrelated to `yv_test_` sandbox keys, which work against production `api.yeboverify.com`.) --- ## Error handling The Node SDK throws typed errors. Catch the specific class to handle each path: ```javascript import { YeboVerifyError, AuthenticationError, ValidationError, RateLimitError, NotFoundError, } from '@yeboverify/node'; try { await yv.sessions.create({ externalRef: 'user_123' }); } catch (err) { if (err instanceof AuthenticationError) { // Bad API key — rotate via dashboard } else if (err instanceof ValidationError) { // Bad input — check err.errors for per-field details } else if (err instanceof RateLimitError) { // Slow down — retry after err.retryAfter seconds } else if (err instanceof NotFoundError) { // verificationId / session doesn't exist } else if (err instanceof YeboVerifyError) { // Generic API error — err.code matches /error-codes } else { // Network / unknown } } ``` See [Error Codes](/error-codes) for the full list of `error.code` values returned by the API. --- ## TypeScript The Node + React SDKs ship full type definitions. Webhook payloads can be discriminated on the `event` field: ```typescript import type { WebhookEvent } from '@yeboverify/node'; function handle(event: WebhookEvent) { switch (event.event) { case 'verification.submitted': // event has verificationId, externalRef, timestamp break; case 'verification.completed': // event also has decision, confidence, faceScore, extractedData break; case 'verification.deleted': // event has verificationId, externalRef, timestamp break; } } ``` --- ## Resources - **API Reference:** [/api-reference](/api-reference) — REST endpoints - **Webhooks:** [/webhooks](/webhooks) — event catalog + signature verification - **Examples:** [/examples](/examples) — full integration code samples - **Embeddable Widgets:** [/widgets](/widgets) — drop-in browser UI - **Mobile Integration:** [/mobile-integration](/mobile-integration) — Flutter / Kotlin / Swift / React Native - **White Label:** [/white-label](/white-label) — branding the hosted flow - **GitHub:** [github.com/omegathesecond](https://github.com/omegathesecond?q=yeboverify-) — all SDK repos --- ## Support - **Email:** [support@yeboverify.com](mailto:support@yeboverify.com) - **Issues:** open an issue on the relevant SDK's GitHub repo ================================================================ # /mobile-integration # Source: https://docs.yeboverify.com/mobile-integration ================================================================ # Mobile & Flutter Integration Integrate YeboVerify into your mobile apps for seamless KYC verification. ## Overview YeboVerify uses a web-based verification flow that works with any mobile platform: 1. Your app calls our API to create a verification session 2. Open the verification URL in a browser/WebView 3. User completes verification (ID + selfie) 4. Webhook notifies your backend of the result 5. User returns to your app ## Quick Start ### 1. Create Session (Backend) ```javascript // Node.js backend const response = await fetch('https://yeboverify-api.run.app/v1/sessions/create', { method: 'POST', headers: { 'X-API-Key': 'yv_live_YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ externalRef: 'user_123', // Your user ID redirectUrl: 'yourapp://verify', // Deep link back to app }), }); const { verifyUrl, sessionToken } = await response.json(); // verifyUrl: https://verify.yeboverify.com#session=xxx ``` ### 2. Open Verification (Mobile) ```dart // Flutter import 'package:url_launcher/url_launcher.dart'; await launchUrl( Uri.parse(verifyUrl), mode: LaunchMode.externalApplication, ); ``` ```swift // iOS UIApplication.shared.open(URL(string: verifyUrl)!) ``` ```kotlin // Android val intent = Intent(Intent.ACTION_VIEW, Uri.parse(verifyUrl)) startActivity(intent) ``` ### 3. Handle Return (Deep Link) ```dart // Flutter - Handle deep link // In your app's route handler: if (uri.path == '/verify') { final status = uri.queryParameters['status']; if (status == 'success') { // Verification completed - fetch updated user await refreshUser(); } } ``` ### 4. Receive Webhook (Backend) ```javascript // Your webhook endpoint app.post('/webhooks/yeboverify', (req, res) => { const { event, externalRef, decision, extractedData } = req.body; if (event === 'verification.completed') { if (decision === 'approved') { // Update user as verified await updateUser(externalRef, { kycVerified: true, kycName: `${extractedData.names} ${extractedData.surname}`, kycCountry: extractedData.issuingCountry, }); } } res.json({ received: true }); }); ``` --- ## Flutter Widget Library For Flutter apps, we provide pre-built widgets that handle the entire flow. ### Installation ```yaml # pubspec.yaml dependencies: yeboverify_flutter: git: url: https://github.com/omegathesecond/yeboverify-flutter.git ``` ### YeboVerifyButton One-tap verification button: ```dart YeboVerifyButton( apiKey: 'yv_live_YOUR_API_KEY', externalRef: currentUser.id, onSuccess: (VerificationResult result) { print('Verified: ${result.extractedData.names}'); refreshUser(); }, onError: (String error) { showSnackbar(error); }, // Customization text: 'Verify Identity', style: YeboVerifyButtonStyle.filled, ) ``` ### YeboVerifyBadge Display verification status: ```dart YeboVerifyBadge( isVerified: user.kycVerified, size: 24, ) ``` ### YeboVerifyCard Full verification card with status and action: ```dart YeboVerifyCard( isVerified: user.kycVerified, verifiedAt: user.kycVerifiedAt, country: user.kycCountry, onVerify: () async { final session = await createSession(user.id); launchUrl(Uri.parse(session.verifyUrl)); }, ) ``` --- ## React Native ### Create Session ```javascript const startVerification = async (userId) => { const response = await fetch(`${API_URL}/kyc/start`, { method: 'POST', headers: { 'Authorization': `Bearer ${userToken}`, }, }); const { verifyUrl } = await response.json(); // Open in browser await Linking.openURL(verifyUrl); }; ``` ### Handle Deep Link ```javascript // App.js useEffect(() => { const handleDeepLink = ({ url }) => { if (url.includes('/verify')) { const params = new URLSearchParams(url.split('?')[1]); if (params.get('status') === 'success') { refreshUser(); } } }; Linking.addEventListener('url', handleDeepLink); return () => Linking.removeEventListener('url', handleDeepLink); }, []); ``` --- ## iOS (Swift) ### Start Verification ```swift func startVerification() async throws { let url = URL(string: "\(apiUrl)/v1/sessions/create")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(apiKey, forHTTPHeaderField: "X-API-Key") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = ["externalRef": userId, "redirectUrl": "myapp://verify"] request.httpBody = try JSONEncoder().encode(body) let (data, _) = try await URLSession.shared.data(for: request) let result = try JSONDecoder().decode(SessionResponse.self, from: data) await UIApplication.shared.open(URL(string: result.verifyUrl)!) } ``` ### Handle Return ```swift // SceneDelegate.swift func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.first?.url else { return } if url.path == "/verify" { let components = URLComponents(url: url, resolvingAgainstBaseURL: false) let status = components?.queryItems?.first(where: { $0.name == "status" })?.value if status == "success" { NotificationCenter.default.post(name: .verificationComplete, object: nil) } } } ``` --- ## Android (Kotlin) ### Start Verification ```kotlin suspend fun startVerification(userId: String) { val response = apiService.createSession( CreateSessionRequest( externalRef = userId, redirectUrl = "myapp://verify" ) ) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(response.verifyUrl)) startActivity(intent) } ``` ### Handle Return ```kotlin // In your Activity override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) intent?.data?.let { uri -> if (uri.path == "/verify") { val status = uri.getQueryParameter("status") if (status == "success") { viewModel.refreshUser() } } } } ``` ### AndroidManifest.xml ```xml ``` --- ## Session Options ```javascript const session = await createSession({ externalRef: 'user_123', // Required: Your user ID // Optional redirectUrl: 'myapp://verify', // Deep link after verification webhookUrl: 'https://api.you.com/webhook', // Override default webhook metadata: { // Custom data (returned in webhook) plan: 'premium', source: 'mobile_app' }, // UX options language: 'en', // UI language theme: 'dark', // dark or light }); ``` --- ## Webhook Payload ```json { "event": "verification.completed", "verificationId": "ver_abc123", "externalRef": "user_123", "decision": "approved", "confidence": "high", "faceScore": 99.72, "ocrConfidence": 95.5, "extractedData": { "surname": "DLAMINI", "names": "SIPHO DAVID", "dateOfBirth": "1990-05-15", "sex": "Male", "idNumber": "9005155555083", "nationality": "Swazi", "issuingCountry": "Eswatini", "documentType": "National ID", "issueDate": "2020-01-15", "expiryDate": "2030-01-15" }, "timestamp": "2026-03-27T01:00:00Z" } ``` --- ## Best Practices ### 1. Always Use Backend Sessions Never expose your API key in mobile apps. Create sessions from your backend. ```dart // ❌ Bad - API key in app final session = await YeboVerify.createSession(apiKey: 'yv_live_xxx'); // ✅ Good - Call your backend final session = await yourApi.startKyc(userId); ``` ### 2. Handle All States ```dart switch (verificationStatus) { case 'success': showSuccess('Verification complete!'); break; case 'failed': showError('Verification failed. Please try again.'); break; case 'cancelled': // User closed without completing break; case 'expired': showError('Session expired. Please try again.'); break; } ``` ### 3. Implement Webhooks Don't rely only on redirect status. Always verify via webhook. ```javascript // Your backend app.post('/webhooks/yeboverify', async (req, res) => { // Verify the replay-resistant V2 signature (X-YeboVerify-Signature-V2 + // X-YeboVerify-Timestamp) — see /webhooks for the full verifyWebhookSignatureV2 // implementation. Do not verify against the legacy X-YeboVerify-Signature // alone; it has no replay protection. const timestamp = req.headers['x-yeboverify-timestamp']; const signatureV2 = req.headers['x-yeboverify-signature-v2']; if (!verifyWebhookSignatureV2(req.body, timestamp, signatureV2, WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook const { event, externalRef, decision } = req.body; // ... }); ``` ### 4. Show Progress ```dart YeboVerifyButton( onPressed: () async { setState(() => isLoading = true); try { final session = await api.startKyc(userId); await launchUrl(Uri.parse(session.verifyUrl)); } finally { setState(() => isLoading = false); } }, child: isLoading ? CircularProgressIndicator() : Text('Verify Identity'), ) ``` --- ## Business (KYB) Verification from Mobile If your app has a business-onboarding flow (e.g. a merchant/vendor signing up), the same `launchUrl` / hosted-browser pattern above applies — just create the verification with [`POST /v1/company-verifications`](/kyb#create-a-company-verification) on your backend instead of `/v1/sessions/create`, and send the authorized signatory to the returned `verifyUrl`. They complete the identical ID + selfie capture; your backend uploads the company's own documents (registration certificate, proof of address, etc.) separately via the API. See [Company Verification (KYB)](/kyb) for the full flow. --- ## Support - **API Reference:** [/api-reference](/api-reference) - **Webhooks:** [/webhooks](/webhooks) - **Company Verification (KYB):** [/kyb](/kyb) - **Error Codes:** [/error-codes](/error-codes) - **Email:** verify@yeboverify.com ================================================================ # /widgets # Source: https://docs.yeboverify.com/widgets ================================================================ # Embeddable Widgets A drop-in React component that captures a user's ID document plus a live anti-spoofing selfie and submits it to YeboVerify. ## Overview This is the full extent of widget support today: - **React** — [`@yeboverify/react`](https://github.com/omegathesecond/yeboverify-react) (not published to npm yet — install via `npm install github:omegathesecond/yeboverify-react`), a single `` component. It renders ID front/back capture and a live-camera liveness challenge (or a single-shot selfie), then submits directly to `POST /v1/verify`. ::: warning Not implemented There is no vanilla-JS SDK, no CDN bundle (`cdn.yeboverify.com` does not exist), no Vue component, and no framework-agnostic Web Component — despite what this page used to say. The React widget itself has no `theme`, `language`, `borderRadius`, `mode`, `showProgress`, `allowRetry`, or `metadata` config, no `YeboVerify.init()`/global object, no event system (`.on`/`.off`), and no `start()` / `destroy()` / `getStatus()` / `setConfig()` methods — it is one React component with the fixed prop surface below. If you need one of these, contact [support@yeboverify.com](mailto:support@yeboverify.com) — don't rely on any prop or API not documented on this page; an unknown prop is silently ignored, not honored. ::: --- ## Installation ```bash npm install github:omegathesecond/yeboverify-react ``` Not published to npm yet — the package installs from GitHub, but its `package.json` name is `@yeboverify/react`, so it resolves in `node_modules` and imports exactly as shown below. `react` and `react-dom` (≥ 17) are peer dependencies. ## Quick Start ```tsx import { YeboVerifyWidget } from '@yeboverify/react'; export function VerifyScreen() { return ( { console.log('Submitted:', verificationId); }} onResult={(result) => { if (result.decision === 'approved') unlockAccess(); }} onError={(err) => toast.error(err.message)} /> ); } ``` The widget renders the full ID + selfie capture flow itself and submits it to `POST /v1/verify` for you — there is no `containerId`/DOM-mount API, because it's a normal React component, not a script-tag widget with its own runtime. > **Browser security:** the widget calls the API directly with the `apiKey` you pass, so that key ships in your client-side JS bundle. Use a key scoped for the verify endpoint — never expose a full-privilege secret key in the browser. --- ## Active Liveness (anti-spoofing) By default (`liveness={true}`) the selfie step is a live-camera challenge, not a single photo upload. The widget: 1. Opens the front camera via `getUserMedia`. 2. Captures a neutral frame. 3. Prompts a randomized action — blink, smile, turn left, or turn right — and captures a second frame. 4. Submits both frames plus the challenge label with the verification. A business with `requireLiveness` enabled cannot auto-approve a submission that arrives without these frames. Pass `liveness={false}` to fall back to a single-shot selfie photo upload. Such submissions can never auto-approve for a `requireLiveness` business. Live capture needs a secure context (HTTPS or `localhost`) and camera permission. If the camera is unavailable or access is denied, the widget surfaces the failure rather than silently submitting without liveness. --- ## Props Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `apiKey` | string | required | Your YeboVerify API key | | `baseUrl` | string | `'https://api.yeboverify.com'` | API base URL — set to `https://dev-api.yeboverify.com` for dev | | `externalRef` | string | - | Your reference ID (e.g. user ID) | | `documentType` | string | `'National ID'` | e.g. "National ID", "Passport" | | `liveness` | boolean | `true` | Active anti-spoofing liveness capture — see above | | `requireIdBack` | boolean | `false` | Also capture the back of the ID document | | `showBack` | boolean | - | Show a back button | | `onSubmit` | `(verificationId: string) => void` | - | Called once the documents are accepted for processing | | `onResult` | `(result: VerificationResult) => void` | - | Only fires if a result is polled or pushed back into the widget — treat your server-side webhook as the source of truth, not this callback | | `onError` | `(error: Error) => void` | - | Called on error | | `primaryColor` | string | `'#16a34a'` | Widget accent color | | `className` | string | - | Custom container class | Any prop not in this table (`theme`, `language`, `borderRadius`, `mode`, `showProgress`, `allowRetry`, `metadata`, `onCancel`, `onReady`, `containerId`, ...) does not exist on this component. Passing one does nothing — same as any unrecognized prop on any React component — it is not validated or rejected. ### `VerificationResult` ```ts interface VerificationResult { verificationId: string; status: 'pending' | 'processing' | 'completed' | 'failed' | 'needs_review'; decision?: 'approved' | 'rejected' | 'needs_review' | null; confidence?: 'high' | 'medium' | 'low' | null; faceScore?: number | null; extractedData?: { surname?: string; names?: string; dateOfBirth?: string; idNumber?: string; documentType?: string; } | null; } ``` `onResult` only fires if a result is pushed or polled back into the widget — verify the outcome server-side via webhook, never trust a client-side callback alone. See [SDKs & Libraries](/sdks) for webhook signature verification with `@yeboverify/node`, and [Webhooks](/webhooks) for the event payloads. --- ## Company (KYB) verification is not available as a widget `@yeboverify/react` deliberately does not support company/KYB verification: - This widget's `apiKey` is passed straight into a browser-side `fetch` call and ships in your page's JS bundle. That's an acceptable trust model for one individual submitting their own ID + selfie, but KYB carries registration numbers, tax IDs, director PII, and AML-screening billing — never appropriate to drive from a public, unauthenticated browser context. - The KYB signatory's own individual identity check doesn't go through this widget either. Creating a company verification server-side returns a `verifyUrl` pointing at the separate hosted flow at `verify.yeboverify.com` — send the signatory there directly. See [White-Label Branding](/white-label) for how that hosted flow is themed. --- ## Styling The only visual customization is `primaryColor` — it sets the header background, upload-dropzone accent, and buttons. There is no CSS-variable theming API, no dark-mode prop, and no border-radius control; the widget ships as a fixed white card with rounded corners and a light theme only. --- ## Support - **SDKs Overview:** [/sdks](/sdks) - **White-Label:** [/white-label](/white-label) - **API Reference:** [/api-reference](/api-reference) - **Email:** support@yeboverify.com ================================================================ # /white-label # Source: https://docs.yeboverify.com/white-label ================================================================ # White-Label Branding Apply your own logo, company name, primary color, and light/dark preference to the hosted verification flow. ## Overview This is the full extent of white-label support today: - **Logo** — your logo URL, shown in place of the YeboVerify icon - **Company Name** — shown alongside your logo - **Primary Color** — a single accent hex color applied across the flow - **Dark Mode Preference** — light or dark ::: warning Not implemented Custom domains, custom email templates, CSS overrides, multi-brand profiles, a hosted branding-preview URL, and hiding YeboVerify attribution are **not** built. If you need one of these, contact [support@yeboverify.com](mailto:support@yeboverify.com) — don't rely on any endpoint or parameter not documented on this page; anything else is rejected with a `400`, not silently ignored. ::: --- ## Setting Your Branding `GET`/`PUT /v1/settings/branding` reads and writes the branding for your business (identified by your API key). This is account-wide — every session created with your API key uses it unless overridden per-session (see below). ```javascript // Set your branding const res = await fetch('https://api.yeboverify.com/v1/settings/branding', { method: 'PUT', headers: { 'X-API-Key': 'yv_live_YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ logoUrl: 'https://yourcompany.com/logo.svg', companyName: 'Acme Corp', primaryColor: '#FF6B00', darkMode: true, }), }); ``` ```javascript // Read it back const res = await fetch('https://api.yeboverify.com/v1/settings/branding', { headers: { 'X-API-Key': 'yv_live_YOUR_API_KEY' }, }); // { success: true, data: { logoUrl, companyName, primaryColor, darkMode } } ``` ### Fields | Field | Type | Description | |-------|------|-------------| | `logoUrl` | string \| null | A valid URL to your logo. `null` clears it back to the default YeboVerify icon. | | `companyName` | string \| null | 1–255 characters. `null` clears it. | | `primaryColor` | string \| null | A hex color, e.g. `#FF6B00` or `#F60`. `null` clears it. | | `darkMode` | boolean | `true` (default) keeps the standard dark flow; `false` switches to a light theme. | A field omitted from the request body is left unchanged. Any field not in this table (`backgroundColor`, `textColor`, `borderRadius`, `fontFamily`, `favicon`, `hideYeboVerify`, ...) is **rejected with `400`** — it is never silently accepted and ignored. --- ## Per-Session Override `POST /v1/sessions/create` accepts an optional `theme` object with the same four fields, which overrides your account default for that one session only: ```javascript const session = await fetch('https://api.yeboverify.com/v1/sessions/create', { method: 'POST', headers: { 'X-API-Key': 'yv_live_YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ userId: 'user_123', theme: { primaryColor: '#1E40AF', // overrides your account default for this session only }, }), }); ``` The resolved branding (your account default, with any `theme` fields overridden) is embedded in the signed session token and returned by `POST /v1/sessions/validate` as `branding`. The hosted flow at `verify.yeboverify.com` reads it from there and themes itself accordingly — there is no separate brand-management step and no `brandId`/multi-brand concept. As with `PUT /v1/settings/branding`, any field in `theme` outside the four above is rejected with `400`. --- ## What This Looks Like To The End User With a logo and company name set, the hosted flow's intro screen shows your logo and company name in place of the default YeboVerify branding, themed with your `primaryColor`. A small "Secured by YeboVerify" line stays at the bottom — attribution is not currently removable. --- ## Testing Your Branding in Sandbox Mode Sandbox mode is real and unrelated to the unbuilt branding-preview URL above: every business is issued a `yv_test_` key alongside its live key at signup. A sandbox session opens the SAME hosted verify-app UI a real user sees — fully branded from your account defaults and any per-session `theme` — so you can check how your branding looks end to end without submitting a real ID or selfie and without being billed. ```javascript const session = await fetch('https://api.yeboverify.com/v1/sessions/create', { method: 'POST', headers: { 'X-API-Key': 'yv_test_YOUR_TEST_KEY', // sandbox key — never bills, never calls a real provider 'Content-Type': 'application/json', }, body: JSON.stringify({ userId: 'test_user_1', theme: { primaryColor: '#1E40AF' }, // branding works exactly as it does live // Forces the outcome of whatever the end user submits in this session: // approved (default) | rejected | needs_review | liveness_spoofed | expired_document simulate: 'needs_review', }), }); const { verifyUrl } = await session.json(); ``` Any placeholder photo satisfies the capture step, and the identity check itself is simulated per `simulate` — so you can also confirm your branding renders on the success, rejection and review screens. `simulate` is rejected with a `400` on a live key. See [SDKs & Libraries → Sandbox mode](/sdks#sandbox-mode-yv-test-keys) for the full sandbox reference. --- ## Support - **API Reference:** [/api-reference](/api-reference) - **SDKs:** [/sdks](/sdks) - **Email:** support@yeboverify.com