> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sahlfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# جولة متكاملة من البداية إلى النهاية

> من كشف راتب وبطاقة التعريف الوطنية المغربية (CIN) إلى تقييم المخاطر: أربعة استدعاءات، وشيفرة كاملة بلغات cURL وJavaScript وPython، والنتيجة المتوقعة.

تنقل هذه الجولة عميلاً وهمياً باسم `Test Client` من وثيقتين إلى تقييم للمخاطر. وهي تعمل على بيئة الاختبار (sandbox) بالمفتاح الذي أنشأتَه في [الاختبار في بيئة sandbox](/ar/test-in-sandbox).

## ما تحتاج إليه

| العنصر | التفصيل |
| - | - |
| المفتاح | النطاقان `kyc:extract` و`kyc:verify`. في `SAHL_API_KEY`. |
| `payslip-test.pdf` | كشف راتب وهمي أعددتَه بنفسك، بجهة عمل مختلقة. ليس لشخص حقيقي. |
| `cin-test.jpg` | صورة وهمية لبطاقة CIN أعددتَها بنفسك. لا تستخدم نموذجاً يحمل كلمة SPECIMEN أو صاحباً باسم `John Doe`: فهذه الحالات تُرصد عمداً (راجع [فحص النموذج التوضيحي](/ar/guides/verification)). |
| الأدوات | `curl` و`jq`، أو Node 18 فما فوق، أو Python 3 مع `requests`. |

استخدم بيانات وهمية فقط. هذه الاستدعاءات فترسل `reference`، ولذلك تُحفظ الملفات في ملفّ الحالة الخاص بك.

## الخطة

```mermaid theme={null}
flowchart LR
    A[payslip-test.pdf] -->|extract| B[fields: employer, occupation]
    C[cin-test.jpg] -->|extract| D[fields: name, date of birth, ID number, expiry]
    B --> E[values + documents]
    D --> E
    F[Your form: income, answers] --> E
    E -->|assess| G[verification + assessment]
```

1. `POST /v1/kyc/extract` مع كشف الراتب.
2. `POST /v1/kyc/extract` مع بطاقة CIN.
3. بناء `values` مما قُرئ من الوثائق وما صرّح به العميل.
4. `POST /v1/kyc/assess` مع `values` وعناصر `documents` دون تعديل.

لماذا قراءتان لا قراءة واحدة: يسري `doc_type` على كل الملفات في الاستدعاء الواحد، فيُخصَّص كل استدعاء لنوع وثيقة واحد.

لماذا بطاقة CIN في جولة كشف الراتب: بحسب القواعد الافتراضية يتطلب التحقق وجود هوية رسمية مصحوبة بصورة بين الوثائق (`required:photo_id` حرج). وكشف الراتب وحده سيُحجب. فكشف الراتب دليل داعم وليس إثباتاً للهوية.

لماذا يُدخَل `annual_income` يدوياً في الخطوة 3: القارئ مُلزَم بألا يقدّر أي قيمة. وكشف الراتب يبيّن أجر فترة واحدة، فلا يعود `annual_income` إلا إذا كان مطبوعاً في الكشف. وتأخذ هذه الجولة الدخل من استمارة طلب العميل نفسه، ويوفّر كشف الراتب جهة العمل والمهنة.

## الشيفرة الكاملة

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  set -euo pipefail
  BASE="https://app.sahlfinancial.com/api"
  REF="client-0001"

  read_doc() { # file, mime, doc_type
    curl -sS --fail-with-body -X POST "$BASE/v1/kyc/extract" \
      -H "Authorization: Bearer $SAHL_API_KEY" \
      -F "files=@$1;type=$2" -F "doc_type=$3" -F "reference=$REF" \
      -F "environment=sandbox" -F "subject=Test Client" -F "kind=individual"
  }

  # 1 and 2. Read the payslip and the CIN.
  read_doc payslip-test.pdf application/pdf payslip > payslip.json
  read_doc cin-test.jpg image/jpeg national_id > cin.json
  jq -r '"payslip fields: " + (.fields | keys | join(", "))' payslip.json
  jq -r '"ID checks: " + ([.checks[] | "\(.id)=\(.passed)"] | join(" "))' cin.json

  # 3. Build the request body. Income and answers come from your own form.
  jq -n --slurpfile pass cin.json --slurpfile slip payslip.json '
    ($slip[0].fields + $pass[0].fields) as $r | {
      reference: "client-0001", environment: "sandbox", subject: "Test Client", kind: "individual",
      values: {
        first_name: $r.first_name, last_name: $r.last_name, date_of_birth: $r.date_of_birth,
        citizenship: $r.citizenship, country: "MA", id_type: $r.id_type, id_number: $r.id_number,
        id_expiry: $r.id_expiry, occupation: $r.occupation, employer_name: $r.employer_name,
        annual_income: "84000", net_liquid_assets: "20000", total_net_worth: "60000",
        objective: "Balanced", horizon: "5-10 years",
        investment_knowledge: "Good", investment_experience: "< 5 years"
      },
      documents: ($pass[0].documents + $slip[0].documents)
    }' > assess-body.json

  # 4. Assess.
  curl -sS --fail-with-body -X POST "$BASE/v1/kyc/assess" \
    -H "Authorization: Bearer $SAHL_API_KEY" -H "Content-Type: application/json" \
    -d @assess-body.json > assess.json

  jq -r '"passed: \(.verification.passed)",
         "flags: \([.verification.flags[].id] | join(", "))",
         "risk profile: \(.assessment.risk_profile.score) \(.assessment.risk_profile.band)",
         "capacity: \(.assessment.capacity.score) \(.assessment.capacity.band)",
         "compliance risk: \(.assessment.compliance_risk.level) (\(.assessment.compliance_risk.score) points)",
         "suitability: \(.assessment.suitability)",
         "case: \(.case_id)"' assess.json
  ```

  ```javascript JavaScript theme={null}
  import { readFile } from "node:fs/promises";

  const BASE = "https://app.sahlfinancial.com/api";
  const KEY = process.env.SAHL_API_KEY;
  const REFERENCE = "client-0001";

  async function call(path, init) {
    const res = await fetch(`${BASE}${path}`, {
      ...init,
      headers: { Authorization: `Bearer ${KEY}`, ...init.headers },
    });
    const text = await res.text();
    if (!res.ok) throw new Error(`${path} -> ${res.status} ${text}`);
    return JSON.parse(text);
  }

  async function extract(file, mime, docType) {
    const form = new FormData();
    form.append("files", new Blob([await readFile(file)], { type: mime }), file);
    form.append("doc_type", docType);
    form.append("reference", REFERENCE);
    form.append("environment", "sandbox");
    form.append("subject", "Test Client");
    form.append("kind", "individual");
    return call("/v1/kyc/extract", { method: "POST", body: form });
  }

  // 1 and 2. Read the payslip and the CIN.
  const payslip = await extract("payslip-test.pdf", "application/pdf", "payslip");
  const cin = await extract("cin-test.jpg", "image/jpeg", "national_id");
  console.log("payslip fields:", Object.keys(payslip.fields).join(", "));
  console.log("ID checks:", cin.checks.map((c) => `${c.id}=${c.passed}`).join(" "));
  if (payslip.reader_unavailable || cin.reader_unavailable) throw new Error("a file was not read, retry later");

  // 3. Build the profile. Read values come from /extract. Income and answers come from your own form.
  const read = { ...payslip.fields, ...cin.fields };
  const values = {
    first_name: read.first_name,
    last_name: read.last_name,
    date_of_birth: read.date_of_birth,
    citizenship: read.citizenship,
    country: "MA",
    id_type: read.id_type,
    id_number: read.id_number,
    id_expiry: read.id_expiry,
    occupation: read.occupation,
    employer_name: read.employer_name,
    annual_income: "84000", // declared by the client: the payslip did not state it
    net_liquid_assets: "20000",
    total_net_worth: "60000",
    objective: "Balanced",
    horizon: "5-10 years",
    investment_knowledge: "Good",
    investment_experience: "< 5 years",
  };

  // 4. Assess: verification plus the risk assessment.
  const result = await call("/v1/kyc/assess", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      reference: REFERENCE,
      environment: "sandbox",
      subject: "Test Client",
      kind: "individual",
      values,
      documents: [...cin.documents, ...payslip.documents], // unchanged, strongest ID first
    }),
  });

  const { verification, assessment } = result;
  console.log("passed:", verification.passed);
  console.log("flags:", verification.flags.map((f) => f.id).join(", ") || "none");
  console.log("risk profile:", assessment.risk_profile.score, assessment.risk_profile.band);
  console.log("capacity:", assessment.capacity.score, assessment.capacity.band);
  console.log("compliance risk:", assessment.compliance_risk.level, `(${assessment.compliance_risk.score} points)`);
  console.log("suitability:", assessment.suitability);
  console.log("case:", result.case_id);
  ```

  ```python Python theme={null}
  import os
  import requests

  BASE = "https://app.sahlfinancial.com/api"
  HEADERS = {"Authorization": f"Bearer {os.environ['SAHL_API_KEY']}"}
  REFERENCE = "client-0001"


  def extract(path, mime, doc_type):
      with open(path, "rb") as f:
          res = requests.post(
              f"{BASE}/v1/kyc/extract",
              headers=HEADERS,
              files=[("files", (path, f, mime))],
              data={
                  "doc_type": doc_type,
                  "reference": REFERENCE,
                  "environment": "sandbox",
                  "subject": "Test Client",
                  "kind": "individual",
              },
              timeout=120,
          )
      res.raise_for_status()
      return res.json()


  # 1 and 2. Read the payslip and the CIN.
  payslip = extract("payslip-test.pdf", "application/pdf", "payslip")
  cin = extract("cin-test.jpg", "image/jpeg", "national_id")
  print("payslip fields:", ", ".join(payslip["fields"]))
  print("ID checks:", " ".join(f"{c['id']}={c['passed']}" for c in cin["checks"]))
  if payslip["reader_unavailable"] or cin["reader_unavailable"]:
      raise SystemExit("a file was not read, retry later")

  # 3. Build the profile. Read values come from /extract. Income and answers come from your own form.
  read = {**payslip["fields"], **cin["fields"]}
  values = {
      "first_name": read["first_name"],
      "last_name": read["last_name"],
      "date_of_birth": read["date_of_birth"],
      "citizenship": read["citizenship"],
      "country": "MA",
      "id_type": read["id_type"],
      "id_number": read["id_number"],
      "id_expiry": read["id_expiry"],
      "occupation": read["occupation"],
      "employer_name": read["employer_name"],
      "annual_income": "84000",  # declared by the client: the payslip did not state it
      "net_liquid_assets": "20000",
      "total_net_worth": "60000",
      "objective": "Balanced",
      "horizon": "5-10 years",
      "investment_knowledge": "Good",
      "investment_experience": "< 5 years",
  }

  # 4. Assess: verification plus the risk assessment.
  res = requests.post(
      f"{BASE}/v1/kyc/assess",
      headers=HEADERS,
      json={
          "reference": REFERENCE,
          "environment": "sandbox",
          "subject": "Test Client",
          "kind": "individual",
          "values": values,
          "documents": cin["documents"] + payslip["documents"],  # unchanged, strongest ID first
      },
      timeout=60,
  )
  res.raise_for_status()
  result = res.json()

  verification, assessment = result["verification"], result["assessment"]
  print("passed:", verification["passed"])
  print("flags:", ", ".join(f["id"] for f in verification["flags"]) or "none")
  print("risk profile:", assessment["risk_profile"]["score"], assessment["risk_profile"]["band"])
  print("capacity:", assessment["capacity"]["score"], assessment["capacity"]["band"])
  print("compliance risk:", assessment["compliance_risk"]["level"], f"({assessment['compliance_risk']['score']} points)")
  print("suitability:", assessment["suitability"])
  print("case:", result["case_id"])
  ```
</CodeGroup>

شغّلها:

```bash theme={null}
export SAHL_API_KEY="paste your key here"
bash walkthrough.sh        # or: node walkthrough.mjs   or: python3 walkthrough.py
```

شُغّلت النسخ الثلاث كلها على خادم محلي بديل يعيد إجابات `/extract` جاهزة، ويشغّل شيفرة التحقق والتسجيل الخاصة بـ Sahl على الجسم الذي ترسله، ولذلك فإن الأرقام المطبوعة أدناه هي ما يحسبه المحرّك لهذا المُدخل. أما القارئ الحي فيعيد الحقول التي يراها في ملفاتك، وقد تختلف.

## المخرجات المتوقعة

```text theme={null}
payslip fields: first_name, last_name, document_holder_name, employer_name, occupation, document_date
ID checks: legible:national_id=true expiry:national_id=true format:cin:national_id=true adult:national_id=true
passed: true
flags: completeness
risk profile: 58 Balanced
capacity: 20 Low
compliance risk: Low (1 points)
suitability: Suitable
case: 00000000-0000-4000-8000-000000000001
```

قيمة `case` معرّف حقيقي في إجابتك. وقد يختلف ترتيب حقول كشف الراتب. وفي بيئة إنتاج محمّل فيها السجل الكامل للعقوبات، يكون سطر الفحص هو فحص المعلومات `Sanctions screening — no matches; PEP not list-screened`. وإذا كانت بيئتك لا تحوي سوى القائمة النموذجية من 30 اسماً، فإن `flags` تعرض أيضاً `screening` ويصبح خطر الامتثال `Medium` (نقطتان).

## ما أعادته كل خطوة

### 1. كشف الراتب

```json theme={null}
{
  "fields": {
    "first_name": "Test",
    "last_name": "Client",
    "document_holder_name": "Test Client",
    "employer_name": "Test Employer SARL",
    "occupation": "Analyst",
    "document_date": "2026-09-30"
  },
  "documents": [
    {
      "filename": "payslip-test.pdf",
      "doc_type": "payslip",
      "step_hint": "payslip",
      "step_key": null,
      "fields": {
        "first_name": "Test",
        "last_name": "Client",
        "document_holder_name": "Test Client",
        "employer_name": "Test Employer SARL",
        "occupation": "Analyst",
        "document_date": "2026-09-30"
      },
      "meta_created": "2026-10-01",
      "meta_provenance": {
        "producer": "Example Payroll 4.2",
        "revisions": 1
      },
      "mapped": 6,
      "notes": [],
      "document_id": "22222222-2222-4222-8222-222222222222"
    }
  ],
  "field_count": 6,
  "checks": [],
  "reader_unavailable": false,
  "policy": {
    "id": null,
    "version": 0,
    "source": "legacy",
    "regime": "none",
    "regulator": null,
    "purpose": "onboarding",
    "overrides_refused": []
  },
  "case_id": "00000000-0000-4000-8000-000000000001",
  "document_ids": [
    "22222222-2222-4222-8222-222222222222"
  ]
}
```

لا يخضع كشف الراتب لفحوص وثائقية، ولذلك تكون `checks` فارغة. فهو يعطي جهة العمل والمهنة واسم صاحب الوثيقة. وهو أيضاً ما يُبلَّغ عنه في `kyc.documents_read`.

### 2. بطاقة التعريف الوطنية (CIN)

تعرض `checks` أربعة فحوص ناجحة: قُرئت حقولها الأساسية، وهي غير منتهية الصلاحية، ورقم CIN سليم الصيغة (`format:cin:`، حرف أو حرفان ثم خمسة إلى سبعة أرقام، مثل `BK123456`)، وصاحبها بالغ. وتستخدم العيّنة البلد `MA` و`id_type` بقيمة `National ID`، ويقبلهما المحرّك كما هما.

### 4. التقييم

```json theme={null}
{
  "verification": {
    "passed": true,
    "critical_failures": [],
    "flags": [
      { "id": "completeness", "label": "KYC/KYB data completeness (61%)", "severity": "warning", "passed": false,
        "detail": "missing 11 required data point(s): street1, city, province, postal_code, phone, email, source_of_funds, account_type" }
    ],
    "completeness": { "required": 28, "present": 17, "percent": 61 }
  },
  "assessment": {
    "risk_profile": { "score": 58, "band": "Balanced", "missing": [] },
    "capacity": { "score": 20, "band": "Low", "missing": [] },
    "compliance_risk": { "level": "Low", "score": 1, "factors": ["Flag: KYC/KYB data completeness (61%) (missing 11 required data point(s): ...)"] },
    "suitability": "Suitable",
    "risk_level": "Balanced"
  },
  "registry": null,
  "case_id": "00000000-0000-4000-8000-000000000001"
}
```

مختصر. فالإجابة الكاملة تكرر كل فحص وكتلة السياسة.

كيف تقرأه:

* `passed: true`: لم يفشل أي فحص حرج.
* العلامة الوحيدة هي `completeness` بنسبة 61 بالمئة: لم ترسل الجولة عنواناً ولا هاتفاً ولا بريداً إلكترونياً ولا جواباً عن الشخص المعرّض سياسياً (PEP). ينبغي أن يملأها شخص. وقائمة نقاط البيانات المطلوبة في المحرّك أمريكية شمالية: فهي تطلب أيضاً `province` و`sin/ssn`، وهما ما لا يستطيع الملف المغربي توفيره دائماً، ولذلك يبقى الملف المغربي دون 100 بالمئة. وهذا يكلّف نقطة خطر واحدة، فيكون خطر الامتثال `Low` (والحد الأعلى لـ Low هو نقطة واحدة).
* درجة الملف الاستثماري 58 هي العملية الحسابية الواردة في [تقييم المخاطر](/ar/guides/risk-assessment). وتأتي القدرة 20 من دخل قدره 84,000 وأصول سائلة قدرها 20,000 وصافي ثروة قدره 60,000، وتُقرأ هنا بالدرهم المغربي (MAD). ويقرأ المحرّك المبالغ أرقاماً مجردة مقابل شرائح ثابتة لا ترتبط بعملة معينة.

## جرّب مسارات الفشل

| التغيير | النتيجة |
| - | - |
| اجعل تاريخ انتهاء بطاقة CIN في ملفك تاريخاً ماضياً | يفشل `expiry:national_id` و`expiry:recorded:id_expiry` برسالة `expired YYYY-MM-DD`، وتصبح `passed` قيمتها `false`، ويكون خطر الامتثال `High` والملاءمة `Blocked — document verification failed`. |
| احذف CIN من `documents` | يفشل `required:photo_id`: `no readable government photo ID among the uploads`. |
| احذف `objective` من `values` | تصبح في `risk_profile` القيم `score: null` و`band: null` و`missing: ["objective"]`، وتصبح الملاءمة `Incomplete — suitability answers missing` (إن لم يطابق شيء آخر قبلها). |
| أرسل كشف حساب بنكي قديماً دليلاً على العنوان | يفشل `recency:*`. وهو حرج في خطوة إثبات العنوان، وتحذير في غيرها. |

## في وحدة التحكم

افتح **Cases**، وابحث عن `client-0001` في `sandbox`. تُحفظ هناك الوثائق والفحوص والنتيجة. وتجد الاستدعاءات نفسها في **Developers, Call log**.

## التالي

<CardGroup cols={2}>
  <Card title="وصفات" icon="book-open" href="/ar/guides/recipes">خمس حالات استخدام عملية.</Card>
  <Card title="الانتقال إلى الإنتاج" icon="rocket" href="/ar/go-live">ما يجب التحقق منه قبل الإنتاج.</Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.