للمطورين

توثيق واجهة API

أصدر فواتير إلكترونية متوافقة مع المرحلة الثانية من نظامك مباشرة — نفس محرك التوقيع والربط الذي تستخدمه المنصة.

BASE URL https://zatcatools.com/api/v1

الاستيثاق

كل طلب يتطلب مفتاح API في ترويسة Authorization. ولّد مفتاحك من الإعدادات ← واجهة API — يظهر مرة واحدة فقط عند التوليد، وصلاحيته كاملة على فوترة منشأتك فعامله كسر: خزّنه في متغير بيئة ولا تضعه في الكود أو المستودع.

curl https://zatcatools.com/api/v1/account \
  -H "Authorization: Bearer ztk_live_..."

الأخطاء

كل الأخطاء ترجع بشكل موحّد:

{
  "error": {
    "code": "quota_exceeded",
    "message": "Free-plan invoice quota exhausted. Contact us to upgrade."
  }
}
HTTP code المعنى
401 unauthenticated مفتاح API مفقود أو غير صحيح
402 quota_exceeded استهلكت رصيد باقتك — تواصل معنا للترقية
404 not_found لا يوجد مورد بهذا المعرّف ضمن منشأتك
409 not_connected المنشأة غير مرتبطة بـ ZATCA بعد — أكمل الربط أولًا
422 validation_failed … بيانات ناقصة أو غير صالحة — الرسالة توضح السبب
429 too_many_requests تجاوزت حد الطلبات (60/دقيقة) — أعد المحاولة لاحقًا
GET /v1/account

بيانات المنشأة، حالة الربط، والرصيد المتبقي من الباقة.

{
  "name": "مؤسسة النخبة",
  "vat_number": "310000000000003",
  "cr_number": "1010101010",
  "zatca": {
    "connected": true,
    "environment": "production",
    "certificate_expires_at": "2027-07-01"
  },
  "quota": { "used": 12, "limit": 50, "remaining": 38 }
}
POST /v1/invoices

يبني الفاتورة، يوقّعها رقميًا، ويرسلها إلى ZATCA في طلب واحد — تخليصًا (Clearance) للضريبية أو إبلاغًا (Reporting) للمبسطة. الفاتورة الضريبية standard تتطلب كائن customer كاملًا؛ المبسطة simplified لا تتطلبه. العملاء يُطابَقون تلقائيًا بالرقم الضريبي (أو يُنشأون إن لم يوجدوا).

الطلب — فاتورة ضريبية (B2B)
curl -X POST https://zatcatools.com/api/v1/invoices \
  -H "Authorization: Bearer ztk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "standard",
    "branch_id": 3,                    // البيع من فرع؟ سمِّ الفرع وجهازه معًا —
    "device_id": 7,                    // القاعدة نفسها للضريبية والمبسطة سواء
    "customer": {
      "name": "شركة المثال للتجارة",
      "vat_number": "311111111101113",
      "cr_number": "1010101010",
      "short_address": "RRRD2929"
    },
    "lines": [
      { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000 }
    ],
    "discount": 0,
    "issue_date": "2026-07-19"
  }'
الطلب — فاتورة مبسطة (B2C)
{
  "type": "simplified",
  "external_id": "order-5501",
  "customer_email": "[email protected]",
  "prices_include_vat": true,          // أسعار الرف كما هي — نشتق الصافي
  "branch_id": 3,                      // البيع من فرع؟ سمِّ الفرع…
  "device_id": 7,                      // …وجهازه معًا (القائمة على GET /account)
  "lines": [
    { "name": "قهوة مختصة 250غ", "quantity": 2, "unit_price": 55,
      "description": "حبوب إثيوبية — تحميصة فاتحة" },   // يُطبع تحت اسم البند
    { "name": "أدوات تحضير", "quantity": 1, "unit_price": 120,
      "discount": 10, "discount_type": "percent" },       // خصم على البند: نسبة أو مبلغ
    { "name": "دواء", "quantity": 1, "unit_price": 80,
      "tax_category": "Z", "tax_reason_code": "VATEX-SA-35" }
  ]
}
حقول اختيارية مفيدة للتكامل:
  • external_id — معرّف الطلب في نظامك. إعادة إرسال نفس القيمة تُرجع الفاتورة نفسها (200) بدل إصدار مكرر. تقدر بدلًا منه تمرّر ترويسة Idempotency-Key.
  • customer_email — نرسل نسخة PDF من الفاتورة (مع رمز QR) لهذا البريد تلقائيًا.
  • lines[].description — شرح يُطبع تحت اسم البند (حتى 500 حرف). وlines[].discount — خصم على البند نفسه، مبلغًا افتراضيًا أو نسبة مع "discount_type": "percent". يُخصم من سعر الوحدة ويُطبع السعر المتفق عليه وبجانبه الخصم؛ وحقل discount على مستوى الفاتورة يبقى كما هو لخصمٍ على الإجمالي.
  • customer.short_addressالعنوان الوطني المختصر للمشتري (٤ أحرف + ٤ أرقام، مثل RRRD2929). نستخرج منه الشارع ورقم المبنى والحي والمدينة والرمز البريدي التي تطلبها الهيئة للفاتورة الضريبية — فلا تحتاج تجمعها بنفسك. وتقدر بدلًا منه ترسل الحقول صريحة: street, building_number, subdivision, city, postal_zone.
  • due_date — تاريخ استحقاق السداد للفاتورة الضريبية (B2B)، بصيغة YYYY-MM-DD. يظهر على الفاتورة ويُستخدم في متابعة السداد.
  • notes — ملاحظات حرة للعميل (حتى 2000 حرف): شروط السداد، ملاحظة تسليم، شكر. تُطبع أسفل جدول الفاتورة في ملف PDF ولا تدخل في XML الموقّع.
الاستجابة 201
{
  "uuid": "8d9f6e0a-4b7c-4f2e-9a1d-3c5b7e9f1a2b",
  "number": "INV-2026-00042",
  "kind": "invoice",
  "type": "standard",
  "status": "accepted",
  "issue_date": "2026-07-19",
  "totals": {
    "lines": "1000.00", "discount": "0.00",
    "vat": "150.00", "grand": "1150.00", "currency": "SAR"
  },                                   // + "line_discounts" عند وجود خصم على البنود
  "customer": { "name": "شركة المثال للتجارة", "vat_number": "311111111101113" },
  "lines": [                           // كما أُرسلت: description و discount و net_unit_price عند وجودها
    { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000, "total": 1000 }
  ],
  "due_date": null, "notes": null,
  "zatca": {
    "channel": "clearance",
    "icv": 42,
    "submitted_at": "2026-07-19T09:30:00+03:00",
    "warnings": []
  },
  "links": {
    "xml": "https://zatcatools.com/api/v1/invoices/{uuid}/xml",
    "pdf": "https://zatcatools.com/api/v1/invoices/{uuid}/pdf",
    "qr":  "https://zatcatools.com/api/v1/invoices/{uuid}/qr.svg"
  },
  "lines": [
    { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000, "total": 1000 }
  ]
}
تنبيه: الاستجابة 201 تعني أن الفاتورة أُنشئت ووُقّعت وأُرسلت — افحص الحقل status دائمًا: accepted مقبولة · warnings مقبولة مع تحذيرات (مفصّلة في zatca.warnings) · rejected رفضتها الهيئة (الأسباب في zatca.rejection).

الضريبة: الفئات، الشامل، والفروع

أسعار شاملة الضريبة

"prices_include_vat": true يعني أن كل سعر في الطلب — البنود والخصم — هو ما يدفعه العميل فعلًا (سعر الرف)، ونحن نشتق الصافي لكل بند بنسبة ذلك البند نفسه. المستند المخزَّن والموقَّع صافٍ دائمًا كما يلزم النظام — الخيار يغيّر معنى الريال المُرسل، لا شكل الفاتورة. بدونه (الافتراضي) الأسعار صافية ونضيف الضريبة فوقها.

فئة الضريبة لكل بند

كل بند يقبل tax_category — والافتراضي S (خاضعة 15%). الفئات غير الخاضعة تتطلب tax_reason_code من قائمة الهيئة — الهيئة ترفض بندًا صفريًا أو معفى بلا سبب، ونرسل نص السبب الرسمي تلقائيًا مع كوده. الفاتورة الواحدة تجمع فئات مختلفة، ويُبنى لكل فئة إجماليها الفرعي كما تطلب الهيئة.

tax_category المعنى tax_reason_code
S · 15% خاضعة 15% لا يلزم
Z · 0% صفرية 0%
VATEX-SA-32 — تصدير سلع VATEX-SA-33 — تصدير خدمات VATEX-SA-34-1 — نقل دولي للبضائع VATEX-SA-34-2 — نقل دولي للركاب VATEX-SA-34-3 — خدمات مرتبطة بالنقل الدولي للركاب VATEX-SA-34-4 — توريد وسيلة نقل مؤهلة VATEX-SA-34-5 — خدمات متعلقة بنقل البضائع أو الركاب VATEX-SA-35 — أدوية ومستلزمات طبية VATEX-SA-36 — معادن مؤهلة (ذهب/فضة/بلاتين استثماري) VATEX-SA-EDU — تعليم خاص لمواطن VATEX-SA-HEA — رعاية صحية خاصة لمواطن
E · 0% معفاة
VATEX-SA-29 — خدمات مالية (المادة 29) VATEX-SA-29-7 — تأمين على الحياة (المادة 29) VATEX-SA-30 — تعاملات عقارية (المادة 30)
O · 0% خارج نطاق الضريبة
VATEX-SA-OOS — خارج نطاق الضريبة
الفروع والأجهزة

كل فروعك تحت رقم ضريبي واحد، لكن كل فرع يُصدر عبر جهازه الخاص — نموذج الهيئة نفسه (EGS unit): كل جهاز يُفعَّل بـ OTP خاص به تولّده من بوابة فاتورة (إلحاق حل فوترة جديد — رمز لكل جهاز، يعمل مرة واحدة خلال ساعة)، ويحمل شهادته وسلسلة فواتيره المستقلة، فتُصدر فروعك بالتوازي بلا طابور واحد. المقر الرئيسي جهازه هو ربط المنشأة الأساسي. الفرع لا يُصدر حتى يتصل له جهاز، والفرع الواحد يتحمّل عدة أجهزة.

POST /v1/branches إنشاء فرع — name وshort_address إلزاميان (الشارع والحي والمدينة والرمز البريدي تُستخرج من المختصر تلقائيًا)، cr_number اختياري. حقول العنوان الصريحة لحالة واحدة: تعذر حلّ الرمز (address_unresolved)
POST /v1/branches/{id}/devices تفعيل جهاز للفرع — otp من بوابة فاتورة. يرجع 202 فورًا والتفعيل يكمل في الخلفية
GET /v1/devices/{id} تتبّع الحالة حتى connected — تمر بفحوص التوافق الستة؛ failed مع otp_invalid = ولّد رمزًا جديدًا وأعد
POST /v1/devices/{id}/connect إعادة تفعيل جهاز فشل أو تجمّد بعد التخرج — otp جديد، وسلسلته تبدأ من جديد
DELETE /v1/devices/{id} سحب جهاز — وثائقه المُصدرة تبقى كما هي

بعدها فاتورة الفرع تسمّي فرعها وجهازها معًا — للضريبية (B2B) والمبسطة (B2C) سواء: branch_id + device_id — نقطة البيع تعرف جهازها، والمستند يحمل عنوان الفرع وسجله التجاري ويوقَّع بذلك الجهاز تحديدًا وعلى سلسلته. أحدهما بلا الآخر يُرفض (device_required / branch_required)، والاسم القانوني والرقم الضريبي يبقيان للمنشأة دائمًا. بلا الاثنين: المقر الرئيسي. الإشعارات (POST /notes) تتبع فرع فاتورتها الأصل تلقائيًا، وdevice_id عليها اختياري. القائمة الكاملة للفروع وأجهزتها على GET /account، وتديرها أيضًا من الإعدادات ← ملف المنشأة.

GET /v1/invoices

الأحدث أولًا. معاملات اختيارية: status (accepted | warnings | rejected | draft) · type (standard | simplified) · page · per_page (الأقصى 100).

{
  "data": [ { "uuid": "...", "number": "INV-2026-00042", "status": "accepted", ... } ],
  "meta": { "page": 1, "per_page": 25, "total": 128, "last_page": 6 }
}
GET /v1/invoices/{uuid}

التفاصيل الكاملة لفاتورة أو إشعار — نفس شكل استجابة الإصدار، شاملة البنود وأسباب الرفض إن وجدت.

الملفات — XML · PDF · QR

لكل مستند مُصدَر ثلاثة تنزيلات (بنفس ترويسة الاستيثاق):

GET /v1/invoices/{uuid}/xml الـ XML الموقّع — كما استلمته ZATCA حرفيًا
GET /v1/invoices/{uuid}/pdf PDF عربي جاهز للطباعة مع رمز QR
GET /v1/invoices/{uuid}/qr.svg رمز QR وحده بصيغة SVG (حمولة TLV الموقّعة)
POST /v1/notes

إشعار دائن (credit) أو مدين (debit) مرتبط بفاتورة أصلية عبر invoice_uuid. السبب reason إلزامي (متطلب الهيئة BR-KSA-17)، وقيمة إشعار الدائن لا تتجاوز الرصيد المتبقي للفاتورة بعد الإشعارات السابقة (المادة 40).

{
  "kind": "credit",
  "invoice_uuid": "8d9f6e0a-4b7c-4f2e-9a1d-3c5b7e9f1a2b",
  "reason": "إرجاع جزئي للبضاعة",
  "lines": [
    { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 500 }
  ]
}
إلغاء فاتورة بالكامل: الفاتورة الصادرة موقّعة ومُبلَّغة — لا تُحذف، بل تُعكس بإشعار دائن. أرسل "full": true بدون lines، ونحسب نحن الرصيد المتبقي للفاتورة بعد أي إشعارات سابقة — فلا تحتاج تحسبه بنفسك ولا يمكن أن يتجاوز الحد النظامي. فاتورة مغطّاة بالكامل ترجع 422 nothing_to_credit.

وإعادة تفعيلها: نفس الطلب بـ"kind": "debit" و"full": true يصدر إشعار مدين بمقدار ما خُصم سابقًا فتعود الفاتورة مستحقة. لا شيء مخصوم ⟵ 422 nothing_to_restore. استخدم external_id مختلفًا لكل دورة إلغاء/تفعيل، وإلا أرجعنا لك الإشعار السابق نفسه.

حدود الاستخدام

  • 60 طلبًا في الدقيقة لكل مفتاح — التجاوز يرجع 429.
  • إصدار الفواتير يستهلك من رصيد باقتك — تابع المتبقي عبر GET /v1/account.
  • مفتاح واحد فعّال لكل منشأة — إعادة التوليد تبطل المفتاح السابق فورًا.
سؤال أو مشكلة تكامل؟ تواصل معنا — نساعدك خلال ساعات العمل.