SYRID SYRID
KYC Orchestrator API · v1

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.

🖥
Your App
POST /api/verifications
SYRID
generates the verify link
👤
Your User
uploads ID + selfie
🤖
AI Model
face match + OCR
Result to You
signed webhook: approved/rejected
✍ What you actually write
Your code (~20 lines)
  • One API call to create a verification
  • Send the link to your user (email, SMS, redirect)
  • A webhook endpoint that receives the result
We handle
Everything else
  • 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.

1

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.

2

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.

3

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

StepWhereWhat to keep
Create your account/register or POST /api/auth/registerBearer token (shown once)
Generate an API keyDashboard → API KeysAPI key kyc_live_… (shown once)
Set your webhook URLDashboard → WebhooksSigning secret for X-KYC-Signature
Create a verificationPOST /api/verificationsThe returned verification_url + id
Receive the resultYour callback endpointFinal status + extracted identity
What you never handle: raw ID images and selfies. Your user uploads them directly to the hosted, isolated verification page — they never pass through your servers, which keeps your PII surface small.

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.

1

You create it

Your server calls POST /api/verifications and gets back a verification_url and an id.

2

User uploads

Your user opens the verification_url and uploads a selfie + ID front. No SDK required.

3

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.

cURL
# 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
curl https://api.yourdomain.com/api/verifications \
  -H "X-API-Key: kyc_live_8f3a91c2e7b04d56a1f0..."
Public routes under /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

Request
{
  "name": "Acme Integrations",
  "email": "dev@yourapp.com",
  "password": "secret123",
  "password_confirmation": "secret123"
}
201 Created
{
  "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

Request
{
  "reference": "order_10482",
  "callback_url": "https://yourapp.com/webhooks/kyc",
  "redirect_url": "https://yourapp.com/kyc/done"
}
201 Created
{
  "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.

Webhook 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"
}

5 · Or poll for the result

GET /api/verifications/{id} · requires auth

200 OK
{
  "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

MethodPathDescriptionAuthRate limit
POST/api/auth/registerCreate an account, returns a token.none10/min
POST/api/auth/loginExchange credentials for a Bearer token.none10/min
POST/api/auth/logoutRevoke the current token.required60/min
POST/api/auth/forgot-passwordSend a password reset email.none5/min
POST/api/auth/reset-passwordSet a new password with a reset token.none5/min

Verifications

MethodPathDescriptionAuthRate limit
POST/api/verificationsCreate a verification, returns verification_url.required60/min
GET/api/verificationsList your verifications (paginated).required120/min
GET/api/verifications/{id}Retrieve one verification with its result.required120/min
GET/api/verifications/{id}/statusLightweight status-only check.required240/min
GET/api/verifications/{id}/resultExtracted identity + match result only.required120/min

Public (hosted page)

MethodPathDescriptionAuthRate limit
GET/api/public/verify/{token}Fetch session info for the upload page.public60/min/IP
POST/api/public/verify/{token}/uploadUpload selfie + ID front (multipart).public20/min/IP
GET/api/public/verify/{token}/statusPoll progress from the hosted page.public120/min/IP

System

MethodPathDescriptionAuthRate limit
GET/api/healthLiveness probe, returns service status.noneunlimited

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).

CodeMeaningWhen it happens
200OKRequest succeeded.
201CreatedAccount or verification created.
202AcceptedDocuments uploaded; processing queued.
401UnauthenticatedMissing/invalid token or API key, or a bad webhook signature.
403ForbiddenAccount not active, or email not verified.
404Not foundVerification/token doesn't exist or isn't yours.
409ConflictThe verification link was already used.
410GoneThe verification link expired (see link lifetime below).
422Validation errorBad input, wrong file type, or already processed.
429Too many requestsRate 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=.

Header
X-KYC-Signature: sha256=3f8a1c...e90b
X-KYC-Event: verification.completed
Content-Type: application/json

Example payload

JSON
{
  "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
<?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);
Node.js (Express)
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);
  });
Respond with 2xx within 10s. Non-2xx responses are retried with exponential backoff for up to 24 hours. Always verify the signature before trusting any field.

Reference

Verification statuses

A verification moves through a small, predictable lifecycle. approved, rejected and expired are terminal — no further changes occur.

created pending processing approved / rejected
created

Verification exists and a verification_url has been issued, but the user has not uploaded anything yet.

pending

The user has submitted their selfie and ID front. Documents are queued for the pipeline.

processing

Document checks, OCR extraction and face matching are running.

approved

Terminal. Document valid and face matched. result.identity is populated. Webhook fired.

rejected

Terminal. The document was invalid or the face did not match. result.reason explains why. Webhook fired.

expired

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
<?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'
}
JavaScript (Node.js)
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));
}
Python
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)
That's the whole integration. Register once, create a verification per user, hand over the URL, and trust the signed webhook. Need a status check instead? Poll GET /api/verifications/{id}.

Integrate

Going-live checklist

Before you put real users through the flow, confirm each of these.

secrets

Token, API key and webhook secret are stored server-side only (env vars / secret manager) — never in client code or git.

HTTPS

Your callback_url is served over TLS and reachable from the public internet.

signature

Your webhook handler verifies X-KYC-Signature with a constant-time compare and rejects anything that fails.

idempotency

You handle the same webhook arriving more than once (retries) by keying on the verification id.

fast 2xx

The handler returns 200 within 10s and does heavy work asynchronously.

expiry

You re-issue a verification when a user hits an expired (410) or used (409) link.

rejections

Your UX has a path for rejected results (let the user retry with a clearer photo).

Need help? Reach the team at support@techsyria.com.