الكتابات/tutorial/2026/08
Tutorial20 أغسطس 2026·26 دقيقة

واجهة واثق في TypeScript: التحقق من الأعمال السعودية (KYB)

ابنِ خدمة KYB سعودية جاهزة للإنتاج فوق واجهة السجل التجاري في واثق بلغة TypeScript: عميل مُنمَّط لنقاط النهاية الثماني، قاعدة الرقم الوطني 700، تخزين مؤقت واعٍ بالتكلفة، محرك تحقق من مطابقة المالك، وسجل إعادة تحقق دوري.

كل منصة سعودية تستقبل أعمالاً تجارية — الأسواق الإلكترونية، منصات B2B SaaS، جهات التمويل، أنظمة المشتريات — تصل في النهاية إلى السؤال نفسه: هل هذا السجل التجاري حقيقي وساري المفعول ومملوك فعلاً للشخص الذي أمامي؟ الجواب البرمجي الرسمي هو واثق (Wathq)، بوابة بيانات وزارة التجارة على developer.wathq.sa. وخلافاً لمعظم المنصات الحكومية السعودية، تقدّم واثق واجهة برمجة عامة بخدمة ذاتية حقيقية: تسجّل حساباً، تشترك في الخدمة، تحصل على مفتاح API، وتستدعي نقاط نهاية REST مباشرة.

غطّينا لماذا يهم التحقق من السجل التجاري — انتقال شارة معروف، وأنماط الفشل الأربعة، والمبررات التجارية — في دليل التحقق من الأعمال عبر معروف وواثق. ذلك المقال هو قراءة مرحلة اتخاذ القرار. أمّا هذا الدليل فهو التنفيذ: عميل TypeScript مُنمَّط، قواعد المعرّفات التي تولّد معظم الاستدعاءات المرفوضة، تخزين مؤقت واعٍ بالتكلفة (كل استعلام يكلّف ريالات حقيقية)، محرك تحقق من مطابقة المالك، وسجل إعادة التحقق الذي يلتقط السجل التجاري الذي انتهت صلاحيته بعد أن قبلت التاجر على منصتك.

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 20+ و TypeScript 5+
  • حساب واثق على developer.wathq.saالباقة التجريبية مجانية: 100 استعلام خلال 30 يوماً بمعدل 5 طلبات في الثانية، وهي كافية لهذا الدليل بأكمله
  • Redis (أو أي مخزن مؤقت) لطبقة ضبط التكلفة
  • إلمام أساسي بـ fetch والأنواع المميَّزة (discriminated unions)

ملاحظة حول النطاق. تقدّم واثق خدمات عديدة (السجل التجاري، العنوان الوطني، العقارات، المحامون). يغطي هذا الدليل واجهة السجل التجاري — مواصفة بيئة الاختبار v6.7.0 وقت الكتابة. المسار الأساسي الموثّق علناً للإنتاج هو https://api.wathq.sa/v5/commercialregistration؛ ولوحة اشتراكك تعرض المسار المُرقَّم الدقيق وملف OpenAPI YAML الخاص بباقتك. اقرأ كليهما من الإعدادات (configuration)، لا من مقال في مدونة — بما في ذلك هذا المقال.

ما الذي ستبنيه

خدمة verifyBusiness() تستقبل معرّف سجل تجاري ورقم هوية وطنية للمفوَّض بالتوقيع، وتعيد واحداً من ثلاثة أحكام مُنمَّطة: verified، أو rejected (مع رموز أسباب مقسّمة بين ما يمكن للتاجر إصلاحه وما هو نهائي)، أو needs_review. وتحتها: عميل واثق مُنمَّط، طبقة توحيد للمعرّفات، تخزين مؤقت يتعامل مع رصيد الـ API بوصفه المورد النادر الذي هو عليه فعلاً، وحلقة إعادة تحقق مجدولة.

الخطوة 1: الوصول والمصادقة وثمن الاستعلام

المصادقة في واثق هي ترويسة apiKey واحدة تصدر لكل اشتراك. ما تتجاوزه معظم الأدلة هو النموذج التجاري، وهو ما يشكّل البنية المعمارية:

  • التجريبية: مجانية، 100 استعلام، 30 يوماً، 5 طلبات/ثانية.
  • الدفع المسبق: تبدأ من 5,000 ريال سعودي، تُخصم الاستعلامات من الرصيد حتى نفاده أو انتهاء صلاحيته؛ وتُسعَّر الاستدعاءات الفردية لكل استعلام (تصل إلى عشرات الريالات للخدمات الأثقل).
  • المؤسسات: أحجام مخصصة، 50–100 طلب/ثانية.

نتيجتان معماريتان. أولاً، استجابة 429 من واثق تعني "Quota Violation" — أنت تستنزف حد المعدل أو رصيدك، وإعادة المحاولة في حلقة تحوّل خطأً برمجياً إلى فاتورة. ثانياً، كل استدعاء يمكن تجنبه هو مال، لذا فالتخزين المؤقت في الخطوة 4 ليس تحسيناً للأداء، بل هو جدار الحماية المالي.

// src/wathq/config.ts
export interface WathqConfig {
  baseUrl: string;      // e.g. https://api.wathq.sa/v5/commercialregistration
  apiKey: string;
  timeoutMs: number;
}
 
export function loadWathqConfig(): WathqConfig {
  const baseUrl = process.env.WATHQ_CR_BASE_URL;
  const apiKey = process.env.WATHQ_API_KEY;
  if (!baseUrl || !apiKey) {
    throw new Error("WATHQ_CR_BASE_URL and WATHQ_API_KEY must be set");
  }
  return { baseUrl, apiKey, timeoutMs: 10_000 };
}

الخطوة 2: نموذج المعرّفات — قاعدة الرقم 700 هي اللعبة كلها

أكثر أخطاء التكامل شيوعاً على الإطلاق هو إرسال نوع الرقم الخاطئ. فقد وحّد نظام السجل التجاري السعودي الجديد السجلات الفرعية ونقل السجلات النشطة إلى الرقم الوطني الموحّد للسجل التجاري — عشرة أرقام تبدأ بـ 700. أما أرقام السجل القديمة المكوّنة من 10 أرقام فما زالت موجودة على الوثائق القديمة والفواتير وفي الذاكرة العضلية لعملائك.

تفرض الواجهة ذلك بخطأ عمل محدد:

400.1.5 — Active and pending records can be retrieved using the commercial registration national number (700) only

فيلصق التاجر رقم السجل المطبوع على شهادته الصادرة عام 2022، ويمرّره تكاملك كما هو، فترفضه واثق رغم أن المنشأة حقيقية تماماً. صمّم هذا التمييز في نظام الأنواع بدلاً من اكتشافه في سجلات الإنتاج:

// src/wathq/identifiers.ts
export type CrIdentifier =
  | { kind: "national"; value: string }   // 700xxxxxxx — active/pending records
  | { kind: "legacy"; value: string };    // pre-unification CR number
 
export class CrIdentifierError extends Error {
  constructor(public readonly code: "NOT_DIGITS" | "NOT_TEN_DIGITS") {
    super(code);
  }
}
 
export function parseCrIdentifier(raw: string): CrIdentifier {
  // Merchants paste from PDFs: strip spaces, dashes, and Arabic-Indic digits.
  const ARABIC_INDIC = "٠١٢٣٤٥٦٧٨٩";
  const normalized = [...raw.trim()]
    .map((ch) => {
      const i = ARABIC_INDIC.indexOf(ch);
      return i === -1 ? ch : String(i);
    })
    .join("")
    .replace(/[\s-]/g, "");
 
  // Mirror the API's own validation: 400.1.2 (digits only), 400.1.3 (10 digits)
  if (!/^\d+$/.test(normalized)) throw new CrIdentifierError("NOT_DIGITS");
  if (normalized.length !== 10) throw new CrIdentifierError("NOT_TEN_DIGITS");
 
  return normalized.startsWith("700")
    ? { kind: "national", value: normalized }
    : { kind: "legacy", value: normalized };
}

توحيد الأرقام الهندية-العربية ليس افتراضياً: المعرّفات المنسوخة من ملفات PDF العربية ورسائل SMS الحكومية تصل بصيغة ٧٠٠١٢٣٤٥٦٧ بتكرار كافٍ لأن يضمن تجاوز هذا السطر طابور دعم فني. كما أن التحقق محلياً قبل الاستدعاء يعني أن أرخص فئتين من الأخطاء (400.1.2 و400.1.3) لن تستهلكا أبداً استعلاماً مدفوعاً.

وعندما يكون بين يديك معرّف legacy لمنشأة يدّعي التاجر أنها نشطة، لا تستدعِ نقاط نهاية السجلات به — بل حُلَّه أولاً عبر واجهة التسجيل لديك ("أدخل الرقم الموحّد الذي يبدأ بـ 700 كما يظهر في شهادتك الحالية") أو عبر استعلام /related انطلاقاً من هوية المالك. حرق استعلام لتتلقى 400.1.5 لا يعلّمك شيئاً لم تكن تعرفه أصلاً من البادئة.

الخطوة 3: العميل المُنمَّط

تعرض واجهة السجل التجاري (مواصفة بيئة الاختبار v6.7.0) ثماني نقاط نهاية للقراءة:

نقطة النهايةما تعيده
GET /info/{id}البيانات الأساسية: التواريخ، الحالة، الأنشطة
GET /fullinfo/{id}السجل الكامل: الأطراف، رأس المال، العنوان
GET /owners/{id}مالك المؤسسة، أو الشركاء مع حصصهم
GET /managers/{id}المديرون وأعضاء مجلس الإدارة
GET /capital/{id}تفاصيل رأس المال
GET /branches/{id}سجلات الفروع
GET /related/{id}/{idType}كل السجلات التجارية المرتبطة بهوية معيّنة
GET /owns/{id}/{idType}قيمة منطقية: هل تملك هذه الهوية سجلاً تجارياً

كل نقاط نهاية السجلات تقبل مُعاملاً language مقيّداً بـ ar أو en (وإلا فالخطأ 400.1.4)؛ وتستقبل /related و/owns رقم هوية من 3–20 رقماً (400.1.7) مع idType مُتحقَّق منه (400.1.6). العميل أدناه يحوّل تصنيف أخطاء العمل الموثّق إلى نوع خطأ مميَّز، فيتفرّع المستدعون على المعنى لا على مطابقة النصوص:

// src/wathq/client.ts
import type { WathqConfig } from "./config";
 
const BUSINESS_ERRORS = {
  "400.1.1": "INPUT_REQUIRED",
  "400.1.2": "NOT_DIGITS",
  "400.1.3": "NOT_TEN_DIGITS",
  "400.1.4": "BAD_LANGUAGE",
  "400.1.5": "NEEDS_NATIONAL_700_NUMBER",
  "400.1.6": "INVALID_ID_TYPE",
  "400.1.7": "BAD_ID_LENGTH",
  "404.2.1": "NO_RESULTS",
} as const;
 
export type WathqBusinessCode =
  (typeof BUSINESS_ERRORS)[keyof typeof BUSINESS_ERRORS];
 
export class WathqError extends Error {
  constructor(
    public readonly kind:
      | { type: "business"; code: WathqBusinessCode }
      | { type: "auth" }              // 401 / 403 — key invalid or lacks scope
      | { type: "quota" }             // 429 — do NOT blind-retry: costs money
      | { type: "upstream"; status: number }, // 5xx gateway family
  ) {
    super(JSON.stringify(kind));
  }
}
 
export class WathqClient {
  constructor(private readonly config: WathqConfig) {}
 
  private async get<T>(path: string): Promise<T> {
    const res = await fetch(`${this.config.baseUrl}${path}`, {
      headers: { apiKey: this.config.apiKey, Accept: "application/json" },
      signal: AbortSignal.timeout(this.config.timeoutMs),
    });
 
    if (res.ok) return (await res.json()) as T;
 
    if (res.status === 401 || res.status === 403)
      throw new WathqError({ type: "auth" });
    if (res.status === 429) throw new WathqError({ type: "quota" });
    if (res.status >= 500)
      throw new WathqError({ type: "upstream", status: res.status });
 
    // 400/404 carry a business code in the body
    const body = (await res.json().catch(() => null)) as
      | { code?: string }
      | null;
    const mapped =
      body?.code && body.code in BUSINESS_ERRORS
        ? BUSINESS_ERRORS[body.code as keyof typeof BUSINESS_ERRORS]
        : undefined;
    if (mapped) throw new WathqError({ type: "business", code: mapped });
    throw new WathqError({ type: "upstream", status: res.status });
  }
 
  info(id: string, language: "ar" | "en" = "ar") {
    return this.get<CrInfo>(`/info/${id}?language=${language}`);
  }
  fullInfo(id: string, language: "ar" | "en" = "ar") {
    return this.get<CrFullInfo>(`/fullinfo/${id}?language=${language}`);
  }
  owners(id: string, language: "ar" | "en" = "ar") {
    return this.get<CrOwners>(`/owners/${id}?language=${language}`);
  }
  managers(id: string, language: "ar" | "en" = "ar") {
    return this.get<CrManagers>(`/managers/${id}?language=${language}`);
  }
}

حول أنواع الاستجابات. النسخة العامة من مواصفة بيئة الاختبار توثّق نقاط النهاية ورموز الأخطاء بدقة، لكنها لا توثّق نماذج الاستجابة كاملة. ولّد CrInfo / CrFullInfo / CrOwners / CrManagers من ملف OpenAPI YAML المرفق بـ اشتراكك أنت (npx openapi-typescript wathq-cr.yaml) بدلاً من نسخ الواجهات يدوياً من مقال لأي كان. الأشكال أدناه تُظهر فقط الحقول التي يعتمد عليها محرك التحقق.

// src/wathq/types.ts — minimal shapes the engine depends on
export interface CrInfo {
  crNationalNumber: string;
  name: string;
  status: { name: string };        // e.g. active / suspended / cancelled
  expiryDate?: string;             // present on records that still carry one
  activities: Array<{ id: string; name: string }>;
}
export interface CrOwners {
  parties: Array<{
    identity: { id: string; type: string };
    name: string;
    sharesPercentage?: number;
  }>;
}
export interface CrManagers {
  parties: Array<{ identity: { id: string; type: string }; name: string }>;
}
export type CrFullInfo = CrInfo & { owners?: CrOwners["parties"] };

الخطوة 4: التخزين المؤقت هو جدار الحماية المالي

سجل تجاري لا يتغيّر من دقيقة إلى أخرى، وكل استدعاء لـ fullinfo يُخصم من رصيد مدفوع مسبقاً. خزّن الاستعلامات الإيجابية مؤقتاً لمدة 24 ساعة؛ وخزّن NO_RESULTS لمدة ساعة واحدة (الأخطاء الإملائية تُصحَّح ويُعاد إرسالها)؛ ولا تخزّن أبداً أخطاء المصادقة أو الحصة أو الخوادم البعيدة.

// src/wathq/cached-client.ts
import type { Redis } from "ioredis";
import { WathqClient, WathqError } from "./client";
import type { CrFullInfo } from "./types";
 
const POSITIVE_TTL = 24 * 60 * 60; // seconds
const NEGATIVE_TTL = 60 * 60;
 
export class CachedWathqClient {
  constructor(
    private readonly inner: WathqClient,
    private readonly redis: Redis,
  ) {}
 
  async fullInfo(id: string): Promise<CrFullInfo | null> {
    const key = `wathq:cr:fullinfo:${id}`;
    const hit = await this.redis.get(key);
    if (hit !== null) {
      return hit === "" ? null : (JSON.parse(hit) as CrFullInfo);
    }
    try {
      const fresh = await this.inner.fullInfo(id);
      await this.redis.set(key, JSON.stringify(fresh), "EX", POSITIVE_TTL);
      return fresh;
    } catch (err) {
      if (
        err instanceof WathqError &&
        err.kind.type === "business" &&
        err.kind.code === "NO_RESULTS"
      ) {
        await this.redis.set(key, "", "EX", NEGATIVE_TTL);
        return null;
      }
      throw err; // quota/auth/upstream: never cached, always surfaced
    }
  }
}

تفصيلة دقيقة واحدة: العلامة المميِّزة للنتيجة السلبية المخزّنة هي السلسلة الفارغة، لا null بصيغة JSON، بحيث تتميّز إصابة التخزين المؤقت عن غيابه دون جولة شبكة ثانية. وعند 5 طلبات في الثانية على الباقة التجريبية، التخزين المؤقت هو أيضاً ما يمنع دفعة مفاجئة من طلبات التسجيل من تجاوز حد المعدل.

الخطوة 5: محرك التحقق

التحقق ليس "أعادت الواجهة 200". فقد يوجد السجل التجاري وهو معلَّق؛ وقد يكون نشطاً وتاريخ انتهائه خلفه؛ وقد يكون نشطاً لكنه مسجَّل لنشاط المقاولات العامة بينما يبيع التاجر مستحضرات تجميل في سوقك الإلكتروني؛ وقد يكون حقيقياً لكنه مملوك لشخص آخر غير من يوقّع شروطك. كل واحدة من هذه الحالات محادثة مختلفة مع التاجر، لذا يعيد المحرك رموز أسباب مجمَّعة بحسب مَن يستطيع إصلاحها:

// src/verify/engine.ts
import type { CachedWathqClient } from "../wathq/cached-client";
import { parseCrIdentifier, CrIdentifierError } from "../wathq/identifiers";
 
// Same national-ID shape used in our Nafath tutorial: 10 digits, 1=citizen, 2=resident
const NID = /^[12]\d{9}$/;
 
export type Verdict =
  | { outcome: "verified"; crNationalNumber: string; verifiedAt: string }
  | { outcome: "rejected"; reasons: RejectReason[] }
  | { outcome: "needs_review"; reasons: RejectReason[] };
 
export type RejectReason =
  // merchant-fixable — ask for corrected input
  | "MALFORMED_CR_INPUT"
  | "LEGACY_NUMBER_PROVIDED"
  | "CR_NOT_FOUND"
  // terminal — do not onboard
  | "CR_NOT_ACTIVE"
  | "CR_EXPIRED"
  // judgement calls — route to a human
  | "ACTIVITY_MISMATCH"
  | "SIGNATORY_NOT_OWNER_OR_MANAGER";
 
export async function verifyBusiness(
  client: CachedWathqClient,
  input: { crRaw: string; signatoryNid: string; requiredActivityIds: string[] },
  asOf: Date, // injected, never new Date() inside — keeps replays honest
): Promise<Verdict> {
  if (!NID.test(input.signatoryNid)) {
    return { outcome: "rejected", reasons: ["MALFORMED_CR_INPUT"] };
  }
 
  let id;
  try {
    id = parseCrIdentifier(input.crRaw);
  } catch (err) {
    if (err instanceof CrIdentifierError) {
      return { outcome: "rejected", reasons: ["MALFORMED_CR_INPUT"] };
    }
    throw err;
  }
  if (id.kind === "legacy") {
    // Don't spend an inquiry to be told 400.1.5 — we already know.
    return { outcome: "rejected", reasons: ["LEGACY_NUMBER_PROVIDED"] };
  }
 
  const record = await client.fullInfo(id.value);
  if (record === null) {
    return { outcome: "rejected", reasons: ["CR_NOT_FOUND"] };
  }
 
  const reasons: RejectReason[] = [];
 
  if (record.status.name.toLowerCase() !== "active") {
    reasons.push("CR_NOT_ACTIVE");
  }
  if (record.expiryDate && new Date(record.expiryDate) < asOf) {
    reasons.push("CR_EXPIRED");
  }
  if (reasons.length > 0) return { outcome: "rejected", reasons };
 
  const activityOk = record.activities.some((a) =>
    input.requiredActivityIds.includes(a.id),
  );
  if (!activityOk) reasons.push("ACTIVITY_MISMATCH");
 
  const parties = record.owners ?? [];
  const signatoryListed = parties.some(
    (p) => p.identity.id === input.signatoryNid,
  );
  if (!signatoryListed) reasons.push("SIGNATORY_NOT_OWNER_OR_MANAGER");
 
  if (reasons.length > 0) return { outcome: "needs_review", reasons };
 
  return {
    outcome: "verified",
    crNationalNumber: record.crNationalNumber,
    verifiedAt: asOf.toISOString(),
  };
}

ثلاثة قرارات تصميمية تستحق الدفاع عنها:

  1. عدم تطابق النشاط وعدم تطابق المالك يذهبان إلى needs_review لا إلى rejected. قائمة الأنشطة في السجل التجاري خشنة التفصيل، ومَن يدير شركة بصفة مشروعة قد يكون مديراً مفوَّضاً لا مالكاً مدرجاً (تحقّق من /managers/{id} قبل التصعيد). الرفض الآلي على هاتين الحالتين ينتج تجاراً غاضبين شرعيين؛ والقبول الآلي ينتج احتيالاً. طابور المراجعة البشرية هو الجواب الأمين.
  2. asOf مُعامل مُمرَّر. يمكن إعادة تشغيل المحرك على سجلات الأمس المخزّنة مؤقتاً في الاختبارات وعمليات التدقيق فيصدر الأحكام ذاتها. نمط المطابقة في دليل تكامل ناجز للتنفيذ يستخدم المبدأ نفسه للسبب نفسه.
  3. مطابقة الحالة متحفّظة عمداً. طابِق مفردات الحالة من مواصفة اشتراكك أنت، وسجّل كل قيمة لم ترها من قبل، وعامل الحالات المجهولة كـ needs_review. ظهور حالات جديدة دون إشعار أمر طبيعي في المنصات الحكومية.

الخطوة 6: سجل إعادة التحقق

من بين أنماط الفشل الأربعة في مقال المدونة يبرز أخبثها: سجل تجاري منتهي الصلاحية خلف شارة متجر حيّة. التحقق يتآكل مع الزمن. تاجر تحقّقت منه في مارس قد يُعلَّق سجله في يونيو، ولا شيء يستدعيك ليخبرك. الحل هو سجل (ledger) يجعل من التقادم عموداً من الدرجة الأولى:

// src/verify/ledger.ts
export interface VerificationRow {
  crNationalNumber: string;
  lastVerifiedAt: string;       // ISO date of last confirmed-good check
  lastOutcome: "verified" | "rejected" | "needs_review";
}
 
const RECHECK_AFTER_DAYS = 30;
 
export function dueForRecheck(rows: VerificationRow[], asOf: Date) {
  const cutoff = new Date(asOf);
  cutoff.setUTCDate(cutoff.getUTCDate() - RECHECK_AFTER_DAYS);
  return rows.filter(
    (r) => r.lastOutcome === "verified" && new Date(r.lastVerifiedAt) < cutoff,
  );
}

شغّل dueForRecheck يومياً، ومرّر الصفوف المستحقة من جديد عبر verifyBusiness، والأهم — نبّه على التحوّلات لا على الحالات. الإشارة هي "هذا السجل كان مُتحقَّقاً منه وأصبح الآن معلَّقاً"، تُرفع مرة واحدة لحظة الانقلاب. أما إعادة التنبيه يومياً على كل صف متقادم فتدرّب فريق العمليات على تجاهل التقرير خلال أسبوع؛ وقد رأينا شكل الفشل نفسه في مطابقة الرواتب والتسويات. ملاحظة ميزانية: مع إعادة التحقق كل 30 يوماً، تكلّف محفظة من 3,000 تاجر نحو 100 استعلام يومياً من رصيدك المدفوع مسبقاً — إنفاق مرئي ومخطَّط له بدلاً من مفاجأة.

الاختبار دون حرق الاستعلامات

ضع العميل خلف منفذ (port) بدالة واحدة واختبر المحرك على بيانات ثابتة (fixtures) — بما فيها الحالات المكلفة أو المستحيلة الاستنساخ على الواجهة الحية:

// test/engine.test.ts
import { describe, it, expect } from "vitest";
import { verifyBusiness } from "../src/verify/engine";
 
const ASOF = new Date("2026-08-20T00:00:00.000Z");
 
function stubClient(record: unknown) {
  return { fullInfo: async () => record } as never;
}
 
const ACTIVE = {
  crNationalNumber: "7001234567",
  name: "مؤسسة المثال التجارية",
  status: { name: "active" },
  activities: [{ id: "4791", name: "Retail via internet" }],
  owners: [{ identity: { id: "1234567890", type: "nid" }, name: "صاحب السجل" }],
};
 
describe("verifyBusiness", () => {
  it("rejects a legacy CR number without spending an inquiry", async () => {
    const v = await verifyBusiness(
      stubClient(ACTIVE),
      { crRaw: "1010123456", signatoryNid: "1234567890", requiredActivityIds: ["4791"] },
      ASOF,
    );
    expect(v).toEqual({ outcome: "rejected", reasons: ["LEGACY_NUMBER_PROVIDED"] });
  });
 
  it("normalizes Arabic-Indic digits before classifying", async () => {
    const v = await verifyBusiness(
      stubClient(ACTIVE),
      { crRaw: "٧٠٠١٢٣٤٥٦٧", signatoryNid: "1234567890", requiredActivityIds: ["4791"] },
      ASOF,
    );
    expect(v.outcome).toBe("verified");
  });
 
  it("routes an unlisted signatory to review, not rejection", async () => {
    const v = await verifyBusiness(
      stubClient(ACTIVE),
      { crRaw: "7001234567", signatoryNid: "2999999999", requiredActivityIds: ["4791"] },
      ASOF,
    );
    expect(v).toEqual({
      outcome: "needs_review",
      reasons: ["SIGNATORY_NOT_OWNER_OR_MANAGER"],
    });
  });
});

الاختبار الأول هو الذي يدفع ثمن نفسه: فهو يثبّت الوعد بأن المدخلات المشوّهة والقديمة تُعالَج قبل الشبكة، وهذه خاصية صحّة وخاصية تكلفة في آن واحد.

استكشاف الأخطاء وإصلاحها

العرَضالسببالحل
400.1.5 على منشأة تعرف أنها نشطةإرسال رقم سجل قديم لسجل نشطاجمع الرقم الموحّد 700؛ شهادة التاجر الحالية تعرضه
404.2.1 لسجل تجاري صادر حديثاًتأخر انتشار البيانات في السجلخزّن النتيجة السلبية لساعة على الأكثر؛ أعد المحاولة في اليوم التالي قبل التصعيد
429 Quota Violation على دفعاتذروة تسجيل تتجاوز 5 طلبات/ثانية، أو نفاد الرصيدضع الاستعلامات في طابور خلف التخزين المؤقت؛ راجع الرصيد المتبقي في اللوحة — لا تُعِد المحاولة عمياء
401/403 بعد تدوير المفتاحمفتاح غير صالح، أو صالح لكنه غير مشترك في هذه الخدمةالمفاتيح مقيّدة بالاشتراك؛ تأكد أن خدمة السجل التجاري ضمن باقتك النشطة
فشل مطابقة المالك لمالك حقيقيمطابقة بالاسم بدلاً من الهويةطابِق على identity.id؛ لا تطابق أبداً على الأسماء العربية — تنوّع الإملاء يجعل الأسماء مفاتيح غير موثوقة

الخطوات التالية

الخلاصة

واجهة السجل التجاري في واثق هي السطح الحكومي السعودي النادر ذو الوصول الذاتي الحقيقي، والتكامل معها ليس صعباً — الصعب هو حسن التقدير. قاعدة الرقم 700 مكانها نظام الأنواع، والتخزين المؤقت أداة ضبط مالي قبل أن يكون أداة أداء، والأحكام تحتاج مساراً لـ needs_review لأن بيانات السجل أخشن من أسئلة المخاطر لديك، والتحقق جدول زمني لا حدث يقع مرة واحدة.

إذا كنت تبني تسجيل تجار، أو KYB للموردين، أو طبقة امتثال فوق بيانات السجلات السعودية وتريد عيناً ثانية على البنية المعمارية قبل أن تلتزم برصيد مدفوع مسبقاً، تواصل معنا — نبني بالضبط هذا النوع من طبقات التكامل بين أنظمة ERP والمنصات والواجهات الحكومية السعودية.