Verify identities with one API call.
KYC Orchestrator runs the full document + face-match pipeline so your app doesn't have to. Create a verification, hand your user a hosted link, and receive a signed webhook with approved / rejected plus extracted identity data.
- One API call to create a verification
- Send the link to your user (email, SMS, redirect)
- A webhook endpoint that receives the result
- Hosted upload page (no SDK)
- AI face-matching & liveness
- ID OCR & data extraction
- Storage, compliance & security
Base URL https://api.yourdomain.com · All requests and responses are JSON · TLS required in production.
Getting started
What you need
KYC Orchestrator is a hosted service — there is nothing to install and no SDK to ship. Everything below can be set up in a few minutes from the dashboard.
An account & credential
Register once to get a Bearer token and an API key. Store them securely on your server — never ship them to a browser or mobile app.
A public callback endpoint
A URL on your backend that can receive a POST over HTTPS. This is where the signed result webhook lands. Optional if you prefer to poll.
Your webhook secret
Copy it from Dashboard → Webhooks. Use it to verify the X-KYC-Signature on every inbound webhook so results can't be forged.
Integration checklist
| Step | Where | What to keep |
|---|---|---|
| Create your account | /register or POST /api/auth/register | Bearer token (shown once) |
| Generate an API key | Dashboard → API Keys | API key kyc_live_… (shown once) |
| Set your webhook URL | Dashboard → Webhooks | Signing secret for X-KYC-Signature |
| Create a verification | POST /api/verifications | The returned verification_url + id |
| Receive the result | Your callback endpoint | Final status + extracted identity |
Overview
How it works
Three moving parts: your server, your user, and the Orchestrator. You never touch raw documents — they go straight to a hosted, isolated upload page.
You create it
Your server calls POST /api/verifications and gets back a verification_url and an id.
User uploads
Your user opens the verification_url and uploads a selfie + ID front. No SDK required.
You get the result
A signed webhook (or a poll) delivers approved / rejected with extracted identity data.
Reference
Authentication
All authenticated endpoints accept one of two credentials. Pick whichever fits your integration — they are interchangeable on every protected route.
Method A — Bearer token
Exchange your account email + password at POST /api/auth/login for a token, then send it on every request. Best for interactive / dashboard-style integrations.
# 1. Get a token curl -X POST https://api.yourdomain.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"dev@yourapp.com","password":"secret123"}' # 2. Use it curl https://api.yourdomain.com/api/verifications \ -H "Authorization: Bearer 14|aBcD3f...your-token"
Method B — API key
Generate a key in the dashboard under API Keys and send it in the X-API-Key header. Best for server-to-server / machine integrations — no expiry handling needed.
curl https://api.yourdomain.com/api/verifications \ -H "X-API-Key: kyc_live_8f3a91c2e7b04d56a1f0..."
/api/public/* require no credential — they are scoped to a single one-time verification token embedded in the verification_url.Getting started
Quick start
Five steps from zero to a verified identity. The only requests your server makes are 1, 2 and (optionally) 5.
1 · Register an account
POST /api/auth/register
{
"name": "Acme Integrations",
"email": "dev@yourapp.com",
"password": "secret123",
"password_confirmation": "secret123"
}
{
"user": {
"id": "usr_9fK2...",
"name": "Acme Integrations",
"email": "dev@yourapp.com"
},
"token": "14|aBcD3f...your-token"
}
2 · Create a verification
POST /api/verifications · requires auth
{
"reference": "order_10482",
"callback_url": "https://yourapp.com/webhooks/kyc",
"redirect_url": "https://yourapp.com/kyc/done"
}
{
"id": "ver_7Ax91Qm2",
"reference": "order_10482",
"status": "created",
"verification_url": "https://api.yourdomain.com/verify/tok_5Bn3...e9",
"expires_at": "2026-06-25T12:00:00Z",
"created_at": "2026-06-24T12:00:00Z"
}
3 · Send the URL to your user
No API call. Just embed verification_url in your flow — redirect to it, render it in an iframe, or email/SMS it. The user uploads their selfie and ID front on the hosted page. The page handles capture, validation and submission for you.
4 · Listen for the webhook
Once processing finishes, the Orchestrator POSTs a signed payload to your callback_url.
{
"event": "verification.completed",
"id": "ver_7Ax91Qm2",
"reference": "order_10482",
"status": "approved",
"result": {
"face_match": true,
"face_match_score": 0.971,
"document_valid": true,
"identity": {
"full_name": "JANE A. DOE",
"document_number": "X1234567",
"date_of_birth": "1994-03-11",
"expiry_date": "2030-03-10",
"nationality": "USA"
}
},
"completed_at": "2026-06-24T12:04:18Z"
}
5 · Or poll for the result
GET /api/verifications/{id} · requires auth
{
"id": "ver_7Ax91Qm2",
"reference": "order_10482",
"status": "approved",
"result": {
"face_match": true,
"document_valid": true,
"identity": {
"full_name": "JANE A. DOE",
"document_number": "X1234567",
"date_of_birth": "1994-03-11"
}
},
"completed_at": "2026-06-24T12:04:18Z"
}
Reference
API reference
All endpoints are prefixed with the base URL http://kyc_orch.souryasocial.shop. Rate limits are per credential (token or API key); public routes are per IP.
Authentication
| Method | Path | Description | Auth | Rate limit |
|---|---|---|---|---|
| POST | /api/auth/register | Create an account, returns a token. | none | 10/min |
| POST | /api/auth/login | Exchange credentials for a Bearer token. | none | 10/min |
| POST | /api/auth/logout | Revoke the current token. | required | 60/min |
| POST | /api/auth/forgot-password | Send a password reset email. | none | 5/min |
| POST | /api/auth/reset-password | Set a new password with a reset token. | none | 5/min |
Verifications
| Method | Path | Description | Auth | Rate limit |
|---|---|---|---|---|
| POST | /api/verifications | Create a verification, returns verification_url. | required | 60/min |
| GET | /api/verifications | List your verifications (paginated). | required | 120/min |
| GET | /api/verifications/{id} | Retrieve one verification with its result. | required | 120/min |
| GET | /api/verifications/{id}/status | Lightweight status-only check. | required | 240/min |
| GET | /api/verifications/{id}/result | Extracted identity + match result only. | required | 120/min |
Public (hosted page)
| Method | Path | Description | Auth | Rate limit |
|---|---|---|---|---|
| GET | /api/public/verify/{token} | Fetch session info for the upload page. | public | 60/min/IP |
| POST | /api/public/verify/{token}/upload | Upload selfie + ID front (multipart). | public | 20/min/IP |
| GET | /api/public/verify/{token}/status | Poll progress from the hosted page. | public | 120/min/IP |
System
| Method | Path | Description | Auth | Rate limit |
|---|---|---|---|---|
| GET | /api/health | Liveness probe, returns service status. | none | unlimited |
Reference
Errors & status codes
The API uses conventional HTTP status codes and always returns a JSON body with a message (and, for validation, an errors map).
| Code | Meaning | When it happens |
|---|---|---|
| 200 | OK | Request succeeded. |
| 201 | Created | Account or verification created. |
| 202 | Accepted | Documents uploaded; processing queued. |
| 401 | Unauthenticated | Missing/invalid token or API key, or a bad webhook signature. |
| 403 | Forbidden | Account not active, or email not verified. |
| 404 | Not found | Verification/token doesn't exist or isn't yours. |
| 409 | Conflict | The verification link was already used. |
| 410 | Gone | The verification link expired (see link lifetime below). |
| 422 | Validation error | Bad input, wrong file type, or already processed. |
| 429 | Too many requests | Rate limit hit — back off and retry. |
Link lifetime & one-time use
72 hours: each verification_url is backed by a single-use session valid for that long. After a successful upload the link is consumed and can't be reused. An expired link returns 410; an already-used link returns 409. If either happens, create a fresh verification and send the new URL.
Reference
Webhooks
When a verification reaches a terminal state, the Orchestrator sends a POST to the callback_url you supplied at creation time. Every webhook is signed so you can prove it came from us and wasn't tampered with.
Signature
Each request carries an X-KYC-Signature header. It is the lowercase hex HMAC-SHA256 of the raw request body, keyed with your webhook signing secret (dashboard → API Keys → Webhook secret), prefixed with sha256=.
X-KYC-Signature: sha256=3f8a1c...e90b X-KYC-Event: verification.completed Content-Type: application/json
Example payload
{
"event": "verification.completed",
"id": "ver_7Ax91Qm2",
"reference": "order_10482",
"status": "approved",
"result": {
"face_match": true,
"face_match_score": 0.971,
"document_valid": true,
"identity": {
"full_name": "JANE A. DOE",
"document_number": "X1234567",
"date_of_birth": "1994-03-11",
"expiry_date": "2030-03-10",
"nationality": "USA"
}
},
"completed_at": "2026-06-24T12:04:18Z"
}
Verify the signature
<?php $secret = getenv('KYC_WEBHOOK_SECRET'); $payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_KYC_SIGNATURE'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret); // constant-time comparison if (! hash_equals($expected, $header)) { http_response_code(401); exit('invalid signature'); } $event = json_decode($payload, true); // ... handle $event['status'] ... http_response_code(200);
const crypto = require('crypto'); // mount with express.raw() so body is the exact bytes app.post('/webhooks/kyc', express.raw({ type: 'application/json' }), (req, res) => { const secret = process.env.KYC_WEBHOOK_SECRET; const header = req.get('X-KYC-Signature') || ''; const expected = 'sha256=' + crypto.createHmac('sha256', secret) .update(req.body) .digest('hex'); const ok = header.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected)); if (!ok) return res.status(401).send('invalid signature'); const event = JSON.parse(req.body.toString()); // ... handle event.status ... res.sendStatus(200); });
Reference
Verification statuses
A verification moves through a small, predictable lifecycle. approved, rejected and expired are terminal — no further changes occur.
Verification exists and a verification_url has been issued, but the user has not uploaded anything yet.
The user has submitted their selfie and ID front. Documents are queued for the pipeline.
Document checks, OCR extraction and face matching are running.
Terminal. Document valid and face matched. result.identity is populated. Webhook fired.
Terminal. The document was invalid or the face did not match. result.reason explains why. Webhook fired.
Terminal. The expires_at deadline passed before the user finished. Issue a new verification to retry.
Integrate
Code examples
Full server-side flow in three languages: register (once), create a verification, then verify the inbound webhook. Send your user the verification_url in between.
<?php $base = 'https://api.yourdomain.com'; // --- 1. Register once, store the token --- $ch = curl_init("$base/api/auth/register"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'name' => 'Acme Integrations', 'email' => 'dev@yourapp.com', 'password' => 'secret123', 'password_confirmation' => 'secret123', ]), ]); $token = json_decode(curl_exec($ch), true)['token']; // --- 2. Create a verification --- $ch = curl_init("$base/api/verifications"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', "Authorization: Bearer $token", ], CURLOPT_POSTFIELDS => json_encode([ 'reference' => 'order_10482', 'callback_url' => 'https://yourapp.com/webhooks/kyc', ]), ]); $verification = json_decode(curl_exec($ch), true); // 3. Send $verification['verification_url'] to your user. echo $verification['verification_url']; // --- 4. Webhook handler (separate endpoint) --- $secret = getenv('KYC_WEBHOOK_SECRET'); $body = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_KYC_SIGNATURE'] ?? ''; if (hash_equals('sha256=' . hash_hmac('sha256', $body, $secret), $sig)) { $event = json_decode($body, true); // $event['status'] === 'approved' | 'rejected' }
const crypto = require('crypto'); const BASE = 'https://api.yourdomain.com'; // --- 1. Register once, store the token --- async function register() { const r = await fetch(`${BASE}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme Integrations', email: 'dev@yourapp.com', password: 'secret123', password_confirmation: 'secret123', }), }); return (await r.json()).token; } // --- 2. Create a verification --- async function createVerification(token) { const r = await fetch(`${BASE}/api/verifications`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ reference: 'order_10482', callback_url: 'https://yourapp.com/webhooks/kyc', }), }); const v = await r.json(); // 3. Send v.verification_url to your user. return v.verification_url; } // --- 4. Verify the webhook --- function verifyWebhook(rawBody, header, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return header.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected)); }
import hmac, hashlib, requests BASE = "https://api.yourdomain.com" # --- 1. Register once, store the token --- def register(): r = requests.post(f"{BASE}/api/auth/register", json={ "name": "Acme Integrations", "email": "dev@yourapp.com", "password": "secret123", "password_confirmation": "secret123", }) return r.json()["token"] # --- 2. Create a verification --- def create_verification(token): r = requests.post(f"{BASE}/api/verifications", headers={"Authorization": f"Bearer {token}"}, json={ "reference": "order_10482", "callback_url": "https://yourapp.com/webhooks/kyc", }) v = r.json() # 3. Send v["verification_url"] to your user. return v["verification_url"] # --- 4. Verify the webhook (Flask example) --- def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header)
GET /api/verifications/{id}.Integrate
Going-live checklist
Before you put real users through the flow, confirm each of these.
Token, API key and webhook secret are stored server-side only (env vars / secret manager) — never in client code or git.
Your callback_url is served over TLS and reachable from the public internet.
Your webhook handler verifies X-KYC-Signature with a constant-time compare and rejects anything that fails.
You handle the same webhook arriving more than once (retries) by keying on the verification id.
The handler returns 200 within 10s and does heavy work asynchronously.
You re-issue a verification when a user hits an expired (410) or used (409) link.
Your UX has a path for rejected results (let the user retry with a clearer photo).