وصلتك المهمة في سطر واحد: «اجعل نظامنا متوافقًا مع المرحلة الثانية من الفوترة الإلكترونية». والنظام نظامك — ERP بنيتموه، أو نقطة بيع، أو منصة حجوزات، أو فوترة اشتراكات، أو نظام عيادة أو مدرسة — وعليه من الآن أن يُصدر فواتير تُجيزها هيئة الزكاة والضريبة والجمارك أو تُبلَّغ إليها. أمامك طريقان: أن تتحدث مع واجهات منصة فاتورة مباشرة، أو أن ترسل بيانات الفاتورة بصيغة JSON إلى واجهة API تتولّى الباقي. هذا المقال عن الطريق الثاني: ماذا يعطيك بالضبط، وكيف تبدو الاستدعاءات، ومتى لا يناسبك.
هل لهيئة الزكاة API؟ نعم، لكنها لا تستقبل «بيانات فاتورة»
منصة فاتورة تتيح واجهات برمجية للربط والامتثال والإجازة والتبليغ. لكن ما تستقبله ملف XML مبني وموقّع، لا أسطر وأسعار. وقبل أن ترسل أول فاتورة حقيقية عليك أن تبني هذا كله:
- زوج مفاتيح وطلب شهادة (CSR) يحمل الرقم الضريبي وبيانات المنشأة بالحقول التي تشترطها الهيئة.
- شهادة امتثال (Compliance CSID) تبادلها برمز OTP تولّده المنشأة من بوابة فاتورة، ثم فحوص الامتثال: مستندات اختبارية يُنتجها كودك نفسه ويجب أن تجتاز قواعد التحقق.
- شهادة إنتاج (Production CSID) توقّع بها الفواتير الحقيقية، ولها تاريخ انتهاء وتجديد لا يحدث وحده.
- ملف XML بصيغة UBL 2.1 بترتيب عناصر إلزامي وقوائم رموز مغلقة، ثم التوقيع والبصمة ورمز QR.
- عدّاد الفواتير (ICV) وبصمة الفاتورة السابقة (PIH) لكل جهاز: حالة دائمة تتأثر بالتزامن، وكسرها لا يُصلَح.
- مساران للإرسال: الفاتورة الضريبية (B2B) تُجاز قبل أن تصل إلى المشتري، والمبسّطة (B2C) تُبلَّغ خلال 24 ساعة — نقطتا نهاية مختلفتان، وردّان مختلفان، وطابور إعادة محاولة لا يضيع مع إعادة التشغيل.
هذا طريق ممكن ومفهوم، وقد كتبناه خطوة خطوة في دليل الربط مع واجهة ZATCA من CSR إلى أول فاتورة موقّعة، مع ما يفشل عادة في كل خطوة، وللتجديد مقاله: شهادتك لها تاريخ انتهاء. فإن كان الامتثال هو منتجك نفسه فابدأ من هناك. وإن كان ميزة في منتجك لا المنتج كله، فتابع.
البديل: طلب واحد إلى POST /v1/invoices
في واجهة ZATCA Tools يبقى نظامك مصدر الحقيقة، ويصير الامتثال استدعاءً واحدًا. ترسل الأسطر، والمشتري إن كانت الفاتورة ضريبية، فتبني الواجهة ملف XML بصيغة UBL 2.1، وتوقّعه بشهادة منشأتك نفسها، وترسله إلى الهيئة وتحفظ النتيجة. الفاتورة الضريبية (standard) تُرسَل للإجازة، والمبسّطة (simplified) تُوقَّع وتُبلَّغ — وكلاهما داخل الطلب نفسه، فيصلك الرد وفيه حكم الهيئة. هذا طلب فاتورة ضريبية لشركة:
POST https://zatcatools.com/api/v1/invoices
Authorization: Bearer ztk_live_…
Content-Type: application/json
{
"type": "standard",
"external_id": "SO-2026-1188",
"customer": {
"name": "شركة المثال للتجارة",
"vat_number": "311111111101113",
"short_address": "RRRD2929"
},
"lines": [
{ "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000 }
]
}
والرد مختصرًا:
201 Created
{
"uuid": "8d9f6e0a-4b7c-4f2e-9a1d-3c5b7e9f1a2b",
"number": "INV-2026-00042",
"type": "standard",
"status": "accepted",
"totals": { "taxable": "1000.00", "vat": "150.00", "grand": "1150.00", "currency": "SAR" },
"zatca": { "channel": "clearance", "icv": 42, "warnings": [] },
"links": {
"view": "https://zatcatools.com/invoice/…",
"xml": "https://zatcatools.com/api/v1/invoices/8d9f6e0a-…/xml",
"pdf": "https://zatcatools.com/api/v1/invoices/8d9f6e0a-…/pdf",
"qr": "https://zatcatools.com/api/v1/invoices/8d9f6e0a-…/qr.svg"
}
}
أربعة حقول تكفي لقراءة الرد:
status: القيمةacceptedأوwarningsتعني أن الفاتورة صدرت — الثانية مع ملاحظات من الهيئة فيzatca.warnings— وrejectedتعني أن الهيئة ردّتها، ولها قسم خاص أدناه.totals: نصوص بمنزلتين عشريتين، وtaxableوvatوgrandهي الأرقام الموقّعة نفسها، فخزّنها كما هي ولا تُعِد حسابها في نظامك.zatca.channel:clearanceللإجازة أوreportingللتبليغ، ومعه رقم العدّادicv.links: الرابطviewهو ما تضعه أمام العميل، يُفتح في أي متصفح بلا مفتاح؛ أماxmlوpdfوqrفتحتاج المفتاح. وملف XML هو نفسه، بايتًا ببايت، ما استلمته الهيئة.
التوقيع والإجازة يستغرقان من ثانية إلى ثلاث ثوانٍ، فاجعل مهلة الطلب عندك 30 ثانية. وإن كانت أسعارك في النظام شاملة الضريبة كأسعار الرف، فأرسلها كما هي مع "prices_include_vat": true ولا تقسمها على 1.15 بنفسك؛ الصافي يُستخرج لكل سطر بنسبته. ولكل سطر أن يحمل معاملته الضريبية (S أو Z أو E أو O) مع رمز السبب حين يلزم، والمشتري الذي لا رقم ضريبيًا له — جهة حكومية أو منشأة تحت حد التسجيل — يُعرَّف برقم آخر مع نوعه. وتفصيل ذلك كله في التوثيق.
أعد المحاولة بلا خوف من فاتورة مكررة
أخطر لحظة في أي تكامل مع الفوترة ليست الرفض بل الصمت: انقطع الاتصال بعد أن أرسلت الطلب، ولا تعرف هل صدرت الفاتورة أم لا. لو أعدت الإرسال بلا حماية فقد تصدر فاتورتان لطلب واحد، والثانية مستند حقيقي في سلسلتك لا يُحذف، بل يُعالَج بإشعار دائن.
لذلك أرسل رقم الطلب عندك في external_id، أو في ترويسة Idempotency-Key. أي طلب يتكرر بالقيمة نفسها يعيد الفاتورة التي صدرت أول مرة — برمز 200 بدل 201 وبالمحتوى نفسه — ولا يُصدر فاتورة ثانية أبدًا، حتى لو وصل الطلبان في اللحظة نفسها. فإعادة المحاولة بعد انتهاء المهلة آمنة دائمًا، وهذا ما يجعل ربط الواجهة بطابور مهام أو بحدث «تم الدفع» في نظامك أمرًا بسيطًا.
الأخطاء: ما ترفضه الواجهة، وما ترفضه الهيئة
حالتان مختلفتان تمامًا، ويجب أن يفرّق بينهما كودك.
رفض الواجهة (4xx). كل خطأ يعود في غلاف واحد: كائن error فيه code ثابت يقرؤه برنامجك، وmessage يقرؤه الإنسان ويسمّي الحقل الذي يجب إصلاحه، مثل lines[1].quantity. ولا يُوقَّع شيء ولا يُرقَّم قبل أن يجتاز الطلب كل الفحوص، فالخطأ من هذه الفئة لا يترك أثرًا على المنشأة: تصلحه وتعيد الإرسال. والسطر الذي لا يُقرأ يُرفض معه الطلب كله؛ لا يسقط سطر من فاتورة موقّعة بصمت. وبعض قواعد الهيئة تُفحص هنا قبل التوقيع، مثل تاريخ الإصدار المستقبلي (BR-KSA-04) وسطر معفى بوصفه تعليمًا أو صحة لمشترٍ بلا هوية وطنية (BR-KSA-49)، فيصلك خطأ 422 بدل مستند مرفوض يأخذ رقمًا.
رفض الهيئة (201 مع rejected). هذا ليس خطأ من الواجهة: المستند وصل إلى الهيئة فردّته. الأسباب في zatca.rejection بأكواد الهيئة كما قالتها، والمستند المرفوض يحتفظ برقمه وموضعه في السلسلة كما يشترط النظام، فتُصدر فاتورة مصحّحة جديدة ولا تعدّل المرفوضة. والمستند الذي ترفضه الهيئة يعيد وحدته إلى رصيد باقتك. ولكل كود تحقق صفحة بالعربية في مرجع أكواد أخطاء الفاتورة الإلكترونية تشرح معناه وطريقة إصلاحه، ولقراءة الرفض من أوله: فاتورتك مرفوضة؟ اقرأ السبب وأصلحه.
الإشعارات الدائنة والمدينة: POST /v1/notes
الفاتورة الصادرة لا تُحذف ولا تُعدَّل؛ تُصحَّح بإشعار. ترسل إلى /v1/notes نوع الإشعار (credit أو debit) ومعرّف الفاتورة الأصلية invoice_uuid والسبب — وهو إلزامي، فالإشعار بلا سبب ترفضه الهيئة (BR-KSA-17). والأسطر تُسعَّر بالصافي الذي دفعه المشتري فعلًا، والكمية المرتجعة لا تتجاوز كمية الفاتورة، والإشعار الدائن لا يتجاوز الرصيد المفتوح للفاتورة بعد الإشعارات السابقة (المادة 40) — وإلا عاد الخطأ exceeds_open_balance قبل أن يُوقَّع شيء.
ولإلغاء فاتورة كاملة أرسل إشعارًا دائنًا مع "full": true بلا أسطر، فيُحسب الرصيد المفتوح عنك ولا يتجاوز السقف النظامي. وللتراجع عن الإلغاء الطلب نفسه بنوع debit. والإشعار يصدر من فرع فاتورته نفسه، والفرق بين النوعين مشروح في الإشعار الدائن والمدين.
الفروع والأجهزة: رقم ضريبي واحد ومنافذ كثيرة
إن كان نظامك يخدم منشأة لها فروع أو صناديق متعددة، فالقاعدة عند الهيئة أن الشهادة تخص الجهاز لا الشركة. في الواجهة يُصدر كل فرع من جهازه الخاص (وحدة EGS بلغة الهيئة): يُفعَّل برمز OTP مستقل من بوابة فاتورة — لمرة واحدة وصالح لساعة — ويحمل شهادته وسلسلة فواتيره، فتُصدر الفروع بالتوازي دون أن تتنازع موضعًا في سلسلة واحدة.
تنشئ الفرع بـPOST /v1/branches، ثم تضيف جهازه بـPOST /v1/branches/{id}/devices مع الرمز، فيعود الرد 202 فورًا، ويكتمل طلب الشهادة وفحوص الامتثال الستة في الخلفية، عادة في أقل من دقيقة. وفاتورة الفرع تسمّي branch_id وdevice_id معًا، ويبقى الاسم القانوني والرقم الضريبي للمنشأة. ويعرض GET /v1/account الفروع وأجهزتها وتاريخ انتهاء الشهادة، فيستطيع نظامك أن ينبّه قبل الانتهاء لا بعده.
جرّب قبل الرقم الضريبي: وضع التجربة
لا تحتاج رقمًا ضريبيًا حقيقيًا لتكتب أول سطر. ابدأ في وضع التجربة: يُربط حسابك ببيئة الاختبار لدى الهيئة بهويتها الاختبارية، فتُصدر عبر الواجهة مستندات تُبنى وتُوقَّع وتُرسَل إلى تلك البيئة فعلًا، وتقرأ ردودها كما ستقرأ ردود الإنتاج. مستندات بيئة الاختبار ليست فواتير نظامية، وحين تنفد تجيب الواجهة 402 trial_exhausted، وعندها تربط المنشأة برقمها الضريبي ورمز OTP من بوابة فاتورة. وقبل أول فاتورة حقيقية تأكد أن الربط اكتمل: GET /v1/account يعيد "connected": true حين تكون المنشأة جاهزة للإصدار.
لا تريد كتابة كود؟ تكاملات جاهزة
الواجهة نفسها هي المحرك خلف لوحة ZATCA Tools وخلف تكاملاتها الجاهزة، فإن كان نظامك واحدًا منها فلا حاجة لأن تكتب شيئًا:
- إضافة WooCommerce: كل طلب مدفوع يصير فاتورة.
- تطبيق Shopify: الطلبات والمرتجعات تُفوتر تلقائيًا.
- وحدة WHMCS: لفواتير الاستضافة والاشتراكات.
- عقدة (node) على n8n: تُصدر الفواتير من أي سير عمل آلي.
لشركات البرمجيات: مفتاح واحد لكل عملائك
إن كنت شركة برمجيات تخدم عشرات المنشآت من نظام واحد — ERP تنفّذه لعملائك، أو منصة حجوزات أو فوترة يستخدمها تجّار كثيرون — فالمفتاح العادي لا يكفيك لأنه يخص منشأة واحدة. Partner API تعطيك مفتاح شريك واحدًا يبدأ بـztkp_ يعمل باسم كل تاجر تنشئه، وكل نقطة نهاية في الواجهة تقبله مع ترويسة X-Merchant-Id. والمقارنة الكاملة بين بناء التكامل لكل عميل وتشغيل مكتبة مفتوحة واستخدام طبقة جاهزة في مقال شركات تنفيذ ERPNext. وإن كانت منصتك تُصدر طلبات لتجّار آخرين فاقرأ قبل الربط من يُصدر الفاتورة — المنصة أم التاجر؟.
متى لا تناسبك هذه الواجهة
نقولها قبل أن تجرّب، لأن اكتشافها بعد أسبوع من العمل أسوأ:
- المبالغ بالريال السعودي فقط. كل مبلغ في الطلب والرد بالريال بمنزلتين عشريتين. إن كنت تُصدر فواتير بعملة أخرى فالواجهة لا تغطيها اليوم.
- لا نُصدر فاتورة الدفعة المقدّمة (النوع
386). إن كان نشاط عملائك يقوم على دفعات مقدّمة تُخصم لاحقًا من الفاتورة النهائية، فاتفق مع المحاسب على معالجتها قبل أن تبني عليها. - لسنا نظام محاسبة. لا دفتر أستاذ ولا قيود يومية ولا مخزون ولا رواتب. نحن طبقة الامتثال: التوقيع والإجازة والتبليغ وحفظ الملف الموقّع. إن كانت المهمة «نحتاج محاسبة» فهذا ليس ذاك — راجع هل تحتاج برنامج محاسبة للفوترة الإلكترونية؟
- إن كان مورّد نظامك يشحن وحدة مرحلة ثانية تعمل فعلًا، فقد لا تحتاج طبقة ثانية فوقها؛ جهازان وشهادتان لمهمة واحدة عمل مكرر لا فائدة منه.
- إن كانت سياستك تمنع خروج بيانات الفواتير من خوادمك — عقد حكومي أو سياسة أمن معلومات — فالقرار محسوم: ابنِ التكامل أو شغّل مكتبة مفتوحة المصدر، والدليل يبدأ معك.
وللوضوح: ZATCA Tools منصة مستقلة لا تتبع هيئة الزكاة والضريبة والجمارك. الهيئة هي التي تُجيز كل فاتورة ضريبية وتستلم تبليغ كل مبسّطة وتتحقق منهما، وصحة ما ترسله — من الأسعار إلى بيانات المشتري — تبقى مسؤولية المنشأة التي تُصدر الفاتورة أيًّا كان الطريق الذي تختاره.
ابدأ من التوثيق
التوثيق الكامل في صفحة الواجهة، بالإنجليزية كما يُكتب توثيق الواجهات عادة: المصادقة، وجدول الأخطاء، وإعادة المحاولة، والأسطر والخصومات، وتعريف المشتري، والمعاملات الضريبية، والفروع والأجهزة، وكل نقطة نهاية بطلبها وردّها، مع أمثلة جاهزة بـcURL وNode.js وPHP وPython. وأسماء الأسطر في الأمثلة عربية كما سترسلها أنت.
المفتاح تولّده من إعدادات حسابك بعد ربط المنشأة، يبدأ بـztk_live_ ويُعرض مرة واحدة، فاحفظه في متغير بيئة لا في الكود. والواجهة متاحة من اليوم الأول، ضمن البداية المجانية نفسها لا في باقة أعلى. أنشئ حسابك من هنا — أسبوع مجانًا بدون دفع من يوم ربط منشأتك، ثم من 49 ريالًا شهريًا.