مواد پر جائیں
Zoi ID

Zoi ID کے ساتھ بنائیں

ایک ہوسٹ پر دو API، چھ زبانوں میں ٹائپ والے کلائنٹس، ہوسٹڈ فلو اور سینڈ باکس۔ پوری گائیڈ پڑھنے کے لیے کھلی ہے، اور سینڈ باکس کلیدیں ارلی ایکسس کے ساتھ ملتی ہیں۔

بیس URLhttps://kyc.zoi.network

کوئک اسٹارٹ

  1. 1

    سیشن بنائیں

    آپ کا بیک اینڈ اپنی پارٹنر کلید کے ساتھ POST /v1/partner/sessions کو کال کرتا ہے۔

  2. 2

    فلو چلائیں

    کلائنٹ ٹوکن SDK کو دیں، یا verificationUrl براؤزر میں کھولیں۔

  3. 3

    نتیجہ وصول کریں

    ویب ہُک کا دستخط جانچیں، پھر شناخت کی ID اور اس کی حالت محفوظ کریں۔

shell
curl https://kyc.zoi.network/v1/partner/sessions \
  -H "Authorization: Bearer $ZOI_ID_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-user_42" \
  -d '{ "externalUserId": "user_42", "level": "standard", "appId": "com.example.app" }'

دو API

پارٹنر API

سرور سے سرور، /v1/partner کے تحت۔ اپنی پارٹنر کلید Bearer ٹوکن کے طور پر بھیجیں۔ اسے کبھی کسی ایپ میں نہ رکھیں۔

کلائنٹ API

SDK اسے /v1/client کے تحت استعمال کرتا ہے۔ یہ ایک کلائنٹ ٹوکن بھیجتا ہے جو ایک سیشن کے لیے 60 منٹ چلتا ہے۔

اصول

  • وقت epoch ملی سیکنڈ میں ہوتا ہے، ان فیلڈز میں جن کے نام At پر ختم ہوتے ہیں۔
  • تاریخیں YYYY-MM-DD میں ہوتی ہیں۔ نامعلوم حصے XX ہوتے ہیں، جیسے 1990-XX-XX۔
  • کچھ بنانے یا طے کرنے والی POST درخواستیں Idempotency-Key ہیڈر قبول کرتی ہیں۔ کلیدیں 24 گھنٹے رکھی جاتی ہیں۔
  • سیشن میں تبدیلی کے ساتھ متوقع ریویژن بھیجا جاتا ہے۔ اگر وہ بدل گیا ہو تو آپ کو revision_conflict ملتا ہے۔

سیشن کا بہاؤ

آپ کا بیک اینڈ سیشن بناتا ہے۔ SDK قدم چلاتا ہے۔ سرور ترتیب جانچتا ہے اور کوئی قدم بہت جلد آئے تو step_not_allowed لوٹاتا ہے۔

1. اپنے بیک اینڈ سے سیشن بنائیں

اسی صارف کے لیے دوبارہ کال کرنے پر کھلا سیشن جاری رہتا ہے اور نیا کلائنٹ ٹوکن ملتا ہے۔

POST /v1/partner/sessions
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-signup-42

{
  "externalUserId": "user_42",
  "level": "standard",
  "locale": "en",
  "appId": "com.example.app"
}

200 OK
{
  "sessionId": "ses_...",
  "clientToken": "...",
  "clientTokenExpiresAt": 1767225600000,
  "status": "created",
  "apiBaseUrl": "https://kyc.zoi.network",
  "resumed": false
}

2. SDK جو قدم چلاتا ہے

  1. 01

    سیشن اور باقی ضروری قدم پڑھیں۔

    GET /v1/client/session

  2. 02

    رضامندی درج کریں، ساتھ میں چہرے کے موازنے کے لیے الگ اور اختیاری انتخاب۔

    POST /v1/client/consent

  3. 03

    ایپ اور ڈیوائس جانچیں۔ نتیجہ فیصلے کا حصہ بنتا ہے۔

    POST /v1/client/attestation/challenge, POST /v1/client/attestation

  4. 04

    دستاویز بھیجیں: MRZ اسکین کے بعد چپ پڑھیں، یا تصویریں بھیجیں۔

    POST /v1/client/document/mrz-scan, POST /v1/client/document/chip/challenge, POST /v1/client/document/chip or POST /v1/client/document/photos

  5. 05

    جب ایپ سر کی حرکتیں دکھائے، تب سامنے والے کیمرے سے ایک مختصر ویڈیو ریکارڈ کریں۔

    POST /v1/client/liveness/challenge, POST /v1/client/liveness

  6. 06

    رہائش کا ملک بھیجیں، اور بہتر سطح پر پتہ بھی۔

    POST /v1/client/profile

  7. 07

    صرف پتے کی شرط کے ساتھ: دستاویز اپ لوڈ کریں، پھر پتے کی تصدیق کریں۔ اگر شناختی دستاویز سے پتہ پہلے ہی مل گیا ہو تو یہ قدم چھوڑ دیا جاتا ہے۔

    POST /v1/client/address/document, POST /v1/client/address/confirm

  8. 08

    صرف فنڈز کی شرط کے ساتھ: جوابات محفوظ کریں، پالیسی مانگے تو دستاویزات شامل کریں، پھر قدم مکمل کریں۔

    POST /v1/client/funds/answers, POST /v1/client/funds/document, POST /v1/client/funds/remove, POST /v1/client/funds/complete

  9. 09

    دو سوالوں کے جواب دیں: کیا آپ سیاسی طور پر نمایاں شخص ہیں، اور کیا آپ اپنی طرف سے کام کر رہے ہیں۔

    POST /v1/client/declarations

  10. 10

    موجودہ ریویژن کے ساتھ جمع کریں۔

    POST /v1/client/submit

صارف کسی خودکار نتیجے پر انسانی جائزے کی درخواست بھی کر سکتا ہے (POST /review-request) یا رضامندی واپس لے سکتا ہے، جس سے سیشن منسوخ ہوتا ہے اور اس کی فائلیں فوراً حذف ہوتی ہیں (POST /withdraw)۔

پتے کا ثبوت اور فنڈز کا ذریعہ

شناخت سے آگے تصدیق کے لیے POST /v1/partner/sessions میں requirements شامل کریں۔ address پتے کا ثبوت مانگتا ہے۔ source_of_funds پوچھتا ہے کہ پیسہ کہاں سے آتا ہے، اور source_of_wealth کل دولت اور اس کے بننے کا طریقہ بھی پوچھتا ہے۔ فنڈز کی یہ دونوں قدریں مل کر ایک قدم ہیں، اور دونوں address کے ساتھ مل سکتی ہیں۔

POST /v1/partner/sessions
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json

{
  "externalUserId": "user_42",
  "level": "standard",
  "appId": "com.example.app",
  "requirements": ["address", "source_of_funds"],
  // Optional. Without them your partner default applies, else ours.
  "addressPolicy": { "maxAgeDays": 90, "signedOnly": false },
  "fundsPolicy": { "evidence": "optional", "currency": "EUR" }
}

اختیاری پالیسیاں

پالیسی نہ ہو تو آپ کی پارٹنر ڈیفالٹ لاگو ہوتی ہے، ورنہ ہماری۔ سیشن بنتے وقت پالیسی اس میں کاپی ہو جاتی ہے۔

  • addressPolicy: maxAgeDays (ڈیفالٹ 90، 1 سے 366)، acceptedTypes (ڈیفالٹ: یوٹیلیٹی، ٹیلی کام اور بینک دستاویزات، ٹیکس دستاویزات، سرکاری خطوط اور رہائش کے سرٹیفکیٹ) اور signedOnly (ڈیفالٹ false)۔
  • fundsPolicy: questions، evidence (none، optional یا required، ڈیفالٹ none)، acceptedTypes، maxAgeDays (ڈیفالٹ 180)، currency (ISO 4217، ڈیفالٹ USD) اور رینجز کے لیے brackets۔
  • دوسری شرائط والا کوئی کھلا سیشن جو ابھی جمع نہیں ہوا، منسوخ ہو جاتا ہے اور نیا سیشن شروع ہوتا ہے، بالکل سطح بدلنے کی طرح۔
  • ہر نتیجہ سینڈ باکس منظرناموں سے ٹیسٹ کریں، جیسے address_signed_pdf، address_document_review، funds_signed_documents اور funds_review۔

فالو اپ سیشنز

ایسے صارف کے لیے صرف شرائط کی تصدیق کریں جس کی شناخت آپ کے پاس پہلے سے فعال ہے۔ requirements کے ساتھ reuseIdentity: true بھیجیں۔

  • صارف رضامندی دیتا ہے اور صرف نئے قدم مکمل کرتا ہے۔ دستاویز، سیلفی یا بیانات دوبارہ نہیں مانگے جاتے۔
  • منظوری پر شناخت اپنی ID، تصدیق کی تاریخ اور میعاد برقرار رکھتی ہے، اور اسے نیا نتیجہ ملتا ہے۔ آپ کو session.approved اور identity.updated ملتے ہیں۔
  • فعال شناخت کے بغیر کال کا جواب 409 step_not_allowed ہوتا ہے۔
  • اسے اپنے رسک اصولوں سے شروع کریں، جیسے بڑی رقم جمع ہونا، PEP فلیگ یا پرخطر ملک۔ آپ کے اصول آپ کے بیک اینڈ میں، ایک ہی جگہ رہتے ہیں۔
POST /v1/partner/sessions
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: edd-user_42-2026-11

{
  "externalUserId": "user_42",
  "appId": "com.example.app",
  "requirements": ["source_of_funds", "source_of_wealth"],
  "reuseIdentity": true,
  "fundsPolicy": { "evidence": "required" }
}

// No active identity for user_42:
409 { "error": { "code": "step_not_allowed", ... } }

نتائج اور اسکوپس

منظور شدہ سیشنز میں address اور funds ہوتے ہیں، اور شناخت ہر ایک کا تازہ ترین نتیجہ رکھتی ہے۔ خلاصے کی فیلڈز ہر اسکوپ کے ساتھ آتی ہیں۔ تفصیلات کے لیے اسکوپ چاہیے۔

  • اسکوپ address: پتے کی سطریں۔ اس کے بغیر آپ کو verified، method، evidenceLevel، documentType، documentDate، verifiedAt اور country ملتے ہیں۔
  • اسکوپ financial_data: answers، documents اور findings۔ اس کے بغیر آپ کو kinds، evidenceLevel، reviewed، documentCount اور verifiedAt ملتے ہیں۔ یہ کبھی eligibility کا حصہ نہیں ہوتا۔
  • evidenceLevel کی قدر high، medium، low یا none ہوتی ہے۔ پتے کے طریقے، سب سے مضبوط پہلے: wallet، id_document، signed_pdf، official_code، document_review اور declared۔
  • دستاویزات API کے ذریعے نہیں بھیجی جاتیں۔ جب انوائرمنٹ کے پاس financial_data اسکوپ ہو تو کمپلائنس رول والے ممبرز ڈیش بورڈ میں فنڈز کے ذریعے کی دستاویزات کے صفحات دیکھ سکتے ہیں۔ ہر بار دیکھنا ریکارڈ ہوتا ہے۔
GET /v1/partner/sessions/:id
"session": {
  "id": "ses_...",
  "status": "approved",
  "requirements": ["address", "source_of_funds"],
  "reusesIdentity": false,
  "address": {
    "verified": true,
    "method": "signed_pdf",
    "evidenceLevel": "high",
    "documentType": "bank_statement",
    "documentDate": "2026-10-14",
    "verifiedAt": 1767225600000,
    "country": "DE",
    // scope "address"
    "address": { "line1": "Musterstrasse 12", "city": "Berlin", "postalCode": "10115", "country": "DE" }
  },
  "funds": {
    "kinds": ["source_of_funds"],
    "evidenceLevel": "medium",
    "reviewed": true,
    "documentCount": 2,
    "verifiedAt": 1767225600000,
    // scope "financial_data"
    "answers": {
      "employmentStatus": "employed",
      "income": { "min": 50000, "max": 100000, "currency": "EUR" },
      "sources": ["salary", "savings"],
      ...
    },
    "documents": [
      { "documentType": "payslip", "documentDate": "2026-10-31", "amount": 5200, "currency": "EUR",
        "signature": "valid", "signedByKnownIssuer": true, "nameMatch": "match", "findings": [], ... },
      ...
    ],
    "findings": []
  }
}

ویب فلو

SDK نہیں؟ صارف کو ہمارے ہوسٹ کیے گئے صفحے پر بھیجیں۔ یہ وہی API، وہی جانچ اور وہی ریویو قطار استعمال کرتا ہے۔

1. لنک حاصل کریں

POST /v1/partner/sessions سے verificationUrl ملتا ہے۔ آخر میں صارف کو واپس بھیجنے کے لیے redirectUrl دیں۔ یہ آپ کے رجسٹرڈ ری ڈائریکٹ origins میں سے کسی ایک سے شروع ہونا چاہیے۔

POST /v1/partner/sessions
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json

{
  "externalUserId": "user_42",
  "level": "standard",
  "locale": "en",
  "appId": "com.example.app",
  "redirectUrl": "https://example.com/kyc/done"
}

200 OK
{
  "sessionId": "ses_...",
  "clientToken": "...",
  "verificationUrl": "https://kyc.zoi.network/verify/#token=...&lang=en",
  ...
}
  • کلائنٹ ٹوکن URL کے fragment میں ہوتا ہے، یعنی # کے بعد۔ براؤزر fragment کبھی سرور کو نہیں بھیجتے، اس لیے ٹوکن لاگز یا Referer ہیڈر میں نہیں آتا۔ صفحہ اسے پڑھتا ہے، ایڈریس بار سے ہٹا دیتا ہے اور صرف میموری میں رکھتا ہے۔
  • 19 زبانوں میں سے ایک چننے کے لیے fragment میں lang شامل کریں۔ اس کے بغیر صفحہ براؤزر کی زبان استعمال کرتا ہے۔
  • صفحہ دوبارہ لوڈ کرنے سے ٹوکن ختم ہو جاتا ہے۔ اپنے بیک اینڈ سے اسی سیشن کے لیے نیا لنک بنائیں۔
  • صفحہ رضامندی کے ساتھ channel: "web" بھیجتا ہے۔ براؤزر شناختی کارڈ کی چپ نہیں پڑھ سکتے، اس لیے ویب سیشن دستاویز کی تصاویر یا ڈیجیٹل ID والٹ استعمال کرتے ہیں۔ یہ ڈیوائس کی جانچ چھوڑ دیتے ہیں، اور ویب سے آنے والی دستاویز کی تصاویر کا فیصلہ ہمیشہ تربیت یافتہ جائزہ کار کرتا ہے۔
  • کمپیوٹر پر صفحہ ایک QR کوڈ دکھاتا ہے تاکہ صارف فون پر جاری رکھ سکے۔

2. جب صارف مکمل کر لے

جب فلو ختم ہوتا ہے (بھیج دیا گیا، منسوخ ہوا یا آخری حالت)، صفحہ یہ پیغام ان میں سے ہر موجود چیز کو بھیجتا ہے: window.ReactNativeWebView، window.webkit.messageHandlers.zoiId، window.ZoiIdAndroid اور window.opener۔ پھر یہ zoiIdSession اور zoiIdStatus شامل کر کے redirectUrl کھولتا ہے۔

پیغام یا ری ڈائریکٹ پیرامیٹرز پر کبھی بھروسہ نہ کریں۔ صرف اپنے webhook یا GET /v1/partner/sessions/:id پر بھروسہ کریں۔

postMessage
// Sent to each bridge that exists:
//   window.ReactNativeWebView.postMessage(JSON string)
//   window.webkit.messageHandlers.zoiId.postMessage(object)
//   window.ZoiIdAndroid.postMessage(JSON string)
//   window.opener.postMessage(object, "<origin of your redirectUrl>")
//     (popups only, and only when the session has a redirectUrl)
{
  "source": "zoi-id",
  "type": "finished",        // or "cancelled"
  "sessionId": "ses_...",
  "status": "processing"     // the session status at that moment
}

// Then, if redirectUrl was set (the page uses the one stored with the
// session, never one from the address bar):
https://example.com/kyc/done?zoiIdSession=ses_...&zoiIdStatus=processing

3. ویب ویو کی سیٹنگز

Android WebView

javaScriptEnabled اور domStorageEnabled آن کریں اور mediaPlaybackRequiresUserGesture کو false کریں۔ جب آپ کی ایپ کو CAMERA اجازت مل جائے تو WebChromeClient میں onPermissionRequest پر RESOURCE_VIDEO_CAPTURE کی اجازت دیں۔ تصویر والے متبادل کے لیے فائل چننے کا آپشن شامل کریں۔

Kotlin
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
  override fun onPermissionRequest(request: PermissionRequest) {
    // Only after your app holds Manifest.permission.CAMERA.
    if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
      request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
    } else {
      request.deny()
    }
  }
  override fun onShowFileChooser(
    view: WebView, callback: ValueCallback<Array<Uri>>, params: FileChooserParams,
  ): Boolean {
    // Open a picker and pass the result to callback (photo fallback).
    return true
  }
}

iOS WKWebView

iOS 14.3 یا نیا۔ allowsInlineMediaPlayback کو true کریں اور اپنی ایپ میں NSCameraUsageDescription شامل کریں۔ iOS 15 اور نئے ورژن میں آپ کیمرے کی اجازت کا فیصلہ UI delegate میں کر سکتے ہیں۔

Swift
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self // iOS 15+: decide camera access below

func webView(_ webView: WKWebView,
             requestMediaCapturePermissionFor origin: WKSecurityOrigin,
             initiatedByFrame frame: WKFrameInfo,
             type: WKMediaCaptureType,
             decisionHandler: @escaping (WKPermissionDecision) -> Void) {
  decisionHandler(origin.host == "kyc.zoi.network" ? .grant : .deny)
}
// Info.plist: NSCameraUsageDescription

React Native WebView

mediaCapturePermissionGrantType="grant"، allowsInlineMediaPlayback اور javaScriptEnabled سیٹ کریں۔ آپ کی ایپ کو کیمرے کی اجازت چاہیے۔

TSX
<WebView
  source={{ uri: verificationUrl }}
  javaScriptEnabled
  allowsInlineMediaPlayback
  mediaCapturePermissionGrantType="grant"
  onMessage={(event) => {
    const message = JSON.parse(event.nativeEvent.data);
    if (message.source === "zoi-id") {
      // Close the webview, then ask your backend for the real status.
    }
  }}
/>

کیمرے کی اجازتوں اور clickjacking کی وجہ سے iframe سپورٹ نہیں ہوتے۔ براؤزر ٹیب، پاپ اپ یا ویب ویو استعمال کریں۔

حالتیں

سیشن

created
سیشن بن گیا ہے، ابھی کچھ اپلوڈ نہیں ہوا۔
in_progress
رضامندی دی گئی، قدم اپلوڈ ہو رہے ہیں۔
processing
جمع ہو گیا، جانچ جاری ہے۔
review
تربیت یافتہ جائزہ کار کا انتظار۔
approved
حتمی۔ شناخت کی تصدیق ہو گئی۔
rejected
حتمی۔ وجہ کے کوڈ دیکھیں۔
resubmit
صارف کو requiredSteps کے قدم دوبارہ کرنے ہوں گے۔
expired
وقت پر حتمی فیصلہ نہیں ہوا۔
cancelled
پارٹنر، صارف یا ڈیٹا حذف کرنے سے منسوخ۔

شناخت

active
قابل استعمال۔
suspended
رکی ہوئی، مثلاً پابندیوں سے کسی نئے ممکنہ میل کے بعد۔
expired
دستاویز یا تصدیق کی مدت ختم۔ نیا سیشن دوبارہ تصدیق کرتا ہے۔
revoked
حتمی۔

SDK

React Native (Expo) SDK آپ کو ٹائپ والا API کلائنٹ، NFC چپ ریڈنگ اور ڈیوائس کی جانچ دیتا ہے۔ یہ ہیڈلیس ہے، اس لیے آپ کی ایپ اپنی اسکرینوں اور کیمرے پر پورا کنٹرول رکھتی ہے۔ اپنے سرورز کے لیے TypeScript، Python، Go، PHP، Java اور Ruby کے ٹائپ والے کلائنٹس استعمال کریں۔

@zoi/id-sdk
import { ZoiIdClient, nextStep } from "@zoi/id-sdk";

// clientToken and apiBaseUrl come from your backend (POST /v1/partner/sessions).
const client = new ZoiIdClient({ baseUrl: apiBaseUrl, clientToken });

const session = await client.getSession();
switch (nextStep(session)) {
  case "consent":   /* show the notice and the biometric choice */ break;
  case "document":  /* chip read or photos */ break;
  case "liveness":  /* record the video */ break;
  case "address":   /* upload a document, then confirm the address */ break;
  case "funds":     /* answers, then documents when the policy asks */ break;
  case "submit":    await client.submit(session.revision); break;
  case "done":      /* wait for your backend to get the webhook */ break;
}

ویب ہُک

جب کوئی سیشن یا شناخت بدلتی ہے، ہم ویب ہُک بھیجتے ہیں۔ مواد پر بھروسہ کرنے سے پہلے ہمیشہ دستخط جانچیں۔

  • ہر ویب ہُک میں Zoi-Id-Signature ہیڈر ہوتا ہے: t=unix seconds,v1=hex۔
  • v1 آپ کے ویب ہُک سیکرٹ سے متن t.raw body کا HMAC-SHA256 ہے۔ JSON پارس کرنے سے پہلے والی خام باڈی استعمال کریں۔
  • جب سیکرٹ بدلا جا رہا ہو، ہیڈر میں ہر فعال سیکرٹ کے لیے ایک v1 ہوتا ہے۔ اگر کوئی بھی میل کھائے تو ویب ہُک قبول کریں۔
  • اگر t موجودہ وقت سے 300 سیکنڈ سے زیادہ دور ہو تو اسے رد کریں۔
  • ناکام ترسیل 72 گھنٹے تک دوبارہ کی جاتی ہے۔ ہر بار نئے t کے ساتھ دوبارہ دستخط ہوتا ہے۔
  • شناختوں میں revision ہوتا ہے۔ کوئی ایونٹ تبھی لاگو کریں جب اس کا ریویژن آپ کے محفوظ کردہ سے نیا ہو۔
identity.suspended
POST <your webhook URL>
Zoi-Id-Signature: t=1767225600,v1=5f2b...e9

{
  "v": 1,
  "eventId": "evt_...",
  "type": "identity.suspended",
  "createdAt": 1767225600000,
  "object": "identity",
  "externalUserId": "user_42",
  "identity": {
    "identityId": "idn_...",
    "revision": 7,
    "status": "suspended",
    "level": "standard",
    "verifiedAt": 1764547200000,
    "validUntil": 1859155200000,
    "ageOver18": true,
    "captureMethod": "nfc"
  }
}

ایونٹ کی اقسام

  • session.processing
  • session.review
  • session.approved
  • session.rejected
  • session.resubmit
  • session.expired
  • session.cancelled
  • identity.updated
  • identity.suspended
  • identity.reinstated
  • identity.expired
  • identity.revoked

TypeScript میں دستخط جانچیں

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifyZoiIdSignature(
  rawBody: string,
  header: string | null,
  secret: string,
  nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
  if (!header) return false;

  let t: string | undefined;
  const signatures: Buffer[] = [];
  for (const part of header.split(",")) {
    const [key, value] = part.trim().split("=");
    if (!value) continue;
    if (key === "t") t = value;
    if (key === "v1" && /^[0-9a-f]+$/i.test(value)) signatures.push(Buffer.from(value, "hex"));
  }
  if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
  if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return signatures.some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected));
}

// Example handler: read the raw body first, parse JSON only after the check.
export async function POST(request: Request) {
  const rawBody = await request.text();
  const ok = verifyZoiIdSignature(rawBody, request.headers.get("Zoi-Id-Signature"), process.env.ZOI_ID_WEBHOOK_SECRET!);
  if (!ok) return new Response("bad signature", { status: 400 });

  const event = JSON.parse(rawBody);
  // Store event.eventId to skip duplicates, then apply it if the revision is newer.
  return new Response(null, { status: 204 });
}

Go میں دستخط جانچیں

verify.go
package zoiid

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
)

const toleranceSeconds = 300

// VerifySignature checks the Zoi-Id-Signature header against the raw body.
func VerifySignature(rawBody []byte, header, secret string, now time.Time) bool {
	var t string
	var sigs [][]byte
	for _, part := range strings.Split(header, ",") {
		key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
		if !ok || value == "" {
			continue
		}
		switch key {
		case "t":
			t = value
		case "v1":
			if b, err := hex.DecodeString(value); err == nil {
				sigs = append(sigs, b)
			}
		}
	}
	ts, err := strconv.ParseInt(t, 10, 64)
	if err != nil || len(sigs) == 0 {
		return false
	}
	if d := now.Unix() - ts; d > toleranceSeconds || d < -toleranceSeconds {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(t + "."))
	mac.Write(rawBody)
	expected := mac.Sum(nil)
	for _, sig := range sigs {
		if hmac.Equal(sig, expected) {
			return true
		}
	}
	return false
}

ایونٹ فیڈ

GET /v1/partner/events پچھلے 30 دنوں کے تمام ایونٹ لوٹاتا ہے، سب سے پرانا پہلے۔ اگلا صفحہ پڑھنے کے لیے آخری nextCursor کو after میں بھیجیں۔ لمبی رکاوٹ کے بعد، یا یہ یقینی بنانے کے لیے کہ کچھ نہیں چھوٹا، اسے استعمال کریں۔

GET /v1/partner/events
GET /v1/partner/events?after=<nextCursor>&limit=100
Authorization: Bearer <partner key>

200 OK
{
  "items": [ /* webhook payloads, oldest first */ ],
  "nextCursor": "..."   // null when there is nothing more
}

فراڈ رپورٹس

جب کوئی فیصلہ شدہ سیشن فراڈ نکلے تو ہمیں بتائیں، مثلاً کسی چارج بیک کے بعد۔ رپورٹس سے ہم غلط منظوریوں کی پیمائش کرتے ہیں۔ یہ سیشن، شناخت یا صارف کا نتیجہ نہیں بدلتیں۔

  • اسکوپ sessions:write۔ صرف فیصلہ شدہ سیشنز کے لیے (منظور یا مسترد)۔ فیصلے سے پہلے کال کا جواب 409 ہوتا ہے۔
  • خالی باڈی confirmed_fraud کو other قسم کے ساتھ رپورٹ کرتی ہے۔ status کی قدر suspected_fraud بھی ہو سکتی ہے، یا اپنی رپورٹ واپس لینے کے لیے cleared۔
  • اقسام: forged_document، impostor، deepfake_or_replay، synthetic_identity، account_takeover اور other۔ cleared کے ساتھ کوئی قسم نہیں۔
  • note ہمارے تجزیہ کاروں کے لیے آزاد متن ہے، زیادہ سے زیادہ 1000 حروف۔ اس میں کبھی نام، دستاویز نمبر یا کوئی اور ذاتی ڈیٹا نہ لکھیں۔
  • نئی رپورٹ کا جواب 201 ہوتا ہے۔ وہی رپورٹ دوبارہ بھیجنے پر محفوظ شدہ رپورٹ 200 کے ساتھ لوٹتی ہے۔ محفوظ طریقے سے دوبارہ کوشش کے لیے Idempotency-Key بھیجیں۔ ہر سیشن پر زیادہ سے زیادہ 20 رپورٹس۔
  • یہی رپورٹ ڈیش بورڈ میں سیشن کے صفحے پر بس ایک کلک دور ہے۔
POST /v1/partner/sessions/:id/fraud-reports
POST /v1/partner/sessions/ses_.../fraud-reports
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: chargeback-8812

{
  "status": "confirmed_fraud",
  "category": "impostor",
  "note": "Chargeback on the first deposit"
}

201 Created
{
  "id": "flb_...",
  "sessionId": "ses_...",
  "status": "confirmed_fraud",
  "category": "impostor",
  "noteKept": true,
  "createdAt": 1767225600000
}

ڈیٹا حذف کرنا

DELETE /v1/partner/users/:externalUserId صارف کا ڈیٹا اس وقت حذف کرتا ہے جب وہ اکاؤنٹ بند کرے یا حذف کرنے کو کہے۔ جواب بتاتا ہے کہ ابھی کیا حذف ہوا، اور کیا رکھا گیا، کیوں اور کب تک۔

DELETE /v1/partner/users/:externalUserId
DELETE /v1/partner/users/user_42
Authorization: Bearer <partner key>

200 OK
{
  "deleted": ["images", "chip_files", "video", "personal_data", "address"],
  "retained": [
    { "category": "dedup_keys", "reason": "fraud_prevention", "until": 1924992000000 },
    { "category": "sanctions_screening_records", "reason": "legal_obligation", "until": 2082758400000 }
  ],
  "backupsPurgedWithinDays": 30
}

ایررز

ہر ایرر کی شکل ایک جیسی ہے اور ہر کوڈ کا HTTP اسٹیٹس طے ہے۔

error
{
  "error": {
    "code": "step_not_allowed",
    "message": "This step is not allowed now.",
    "requestId": "req_..."
  }
}
کوڈHTTPمطلب
bad_request400درخواست معاہدے سے میل نہیں کھاتی۔
unauthorized401کلید یا ٹوکن موجود نہیں یا غلط ہے۔
token_expired401کلائنٹ ٹوکن کی میعاد ختم ہو گئی۔ اپنے بیک اینڈ سے نیا بنائیں۔
forbidden403یہ کلید یہ کام نہیں کر سکتی۔
not_found404ایسا کوئی سیشن یا صارف نہیں۔
session_closed409سیشن حتمی ہے۔ مزید اپلوڈ نہیں۔
step_not_allowed409غلط ترتیب، یا ابھی اس قدم کی ضرورت نہیں۔
revision_conflict409سیشن کا ریویژن بدل گیا۔ دوبارہ پڑھیں۔
idempotency_conflict409ایک ہی Idempotency-Key کسی دوسری درخواست کے ساتھ استعمال ہوئی۔
challenge_expired410چیلنج کی میعاد ختم ہو گئی یا پہلے استعمال ہو چکا۔ نیا مانگیں۔
payload_too_large413اپلوڈ بہت بڑا ہے۔
unsupported_media_type415یہ فائل کی قسم قبول نہیں۔
rate_limited429بہت زیادہ درخواستیں۔ بعد میں دوبارہ کوشش کریں۔
internal500ہماری طرف کچھ غلط ہو گیا۔
dependency_unavailable503جس سروس کی ہمیں ضرورت ہے وہ بند ہے۔ بعد میں دوبارہ کوشش کریں۔

وجہ کے کوڈ

جب دوبارہ بھیجنا ہو یا رد ہو جائے تو سیشن میں reasonCodes ہوتے ہیں۔ ہر کوڈ کے لیے اپنا ترجمہ شدہ پیغام دکھائیں۔ صارف کو کبھی خام کوڈ نہ دکھائیں۔

  • consent_missing
  • consent_withdrawn
  • attestation_failed
  • document_expired
  • document_unreadable
  • document_not_supported
  • document_suspected_tampering
  • document_number_visible
  • chip_verification_failed
  • chip_proof_missing
  • chip_data_mismatch
  • mrz_invalid
  • photo_quality
  • face_not_found
  • face_mismatch
  • liveness_failed
  • underage
  • location_not_supported
  • residence_not_supported
  • sanctions_match
  • pep_review
  • duplicate_identity
  • possible_duplicate
  • declaration_requires_review
  • user_requested_review
  • reviewer_other
  • address_unreadable
  • address_document_too_old
  • address_document_not_accepted
  • address_name_mismatch
  • address_signature_required
  • funds_document_unreadable
  • funds_document_too_old
  • funds_document_not_accepted
  • funds_more_evidence
  • funds_evidence_rejected