المطورون

واجهة الفواتير البرمجية

أنشئ المسودات والفواتير الضريبية، وتعامل مع العملاء والعملات والخصومات والإعفاءات الضريبية، وتابع كل فاتورة خلال الاعتماد أو الإبلاغ لدى الهيئة.

نقاط النهاية

POST /api/v1/invoices
متاح إنشاء مسودة أو إصدار فاتورة ضريبية.
GET /api/v1/invoices/{id}
متاح قراءة فاتورة مع حالتها لدى الهيئة.
GET /api/v1/invoices
متاح عرض الفواتير وتصفيتها.
GET /api/v1/invoices/{id}/status
متاح حالة مختصرة: الرقم والسداد وحالة الهيئة.
POST /api/v1/invoices/{id}/validate
متاح التحقق من إمكان إصدار مسودة.
POST /api/v1/invoices/{id}/issue
متاح إصدار مسودة أُنشئت سابقًا.
GET /api/v1/invoices/{id}/pdf
متاح تنزيل ملف PDF.
GET /api/v1/invoices/{id}/xml
متاح تنزيل ملف XML النظامي لفاتورة صادرة.
POST /api/v1/invoices/{id}/notes
متاح إنشاء إشعار دائن أو مدين.
GET /api/v1/invoices/{id}/payments
متاح دفعات العميل على الفاتورة والمتبقي عليها.
POST /api/v1/invoices/{id}/payments
متاح تسجيل دفعة عميل على فاتورة صادرة.

مسودة أم فاتورة صادرة

طلب الإنشاء يصدر الفاتورة ما لم ترسل "issue": false.

مسودة (`"issue": false`) صادرة (الافتراضي)
الرقم مرجع DRAFT-…، وnumber_is_final: false الرقم التالي في تسلسلك، وnumber_is_final: true
الصلاحيات invoices:write invoices:write وinvoices:issue
الهيئة لا يُرسل شيء تُعتمد أو يُبلَّغ عنها تلقائيًا إن كانت المنشأة مرتبطة بالهيئة
التعديل تُصدر عبر POST /invoices/{id}/issue، أو تُعدَّل أو تُصدر أو تُلغى من لوحة التحكم نهائية؛ لا تُصحح إلا بإشعار دائن أو مدين

عند إصدار الفاتورة يصبح تاريخ إصدارها يوم العمل الحالي في المملكة، وإن كان تاريخ الاستحقاق سيسبقه فإنه يتقدم بنفس مدة السداد. الأرقام لا تُمنح إلا عند الإصدار، فلا تترك المسودات فجوات في تسلسلك النظامي.

لإصدار مسودة أُنشئت سابقًا افحصها أولًا بـ POST /invoices/{id}/validate (لا يتغير شيء)، ثم استدعِ POST /invoices/{id}/issue بمفتاح يملك invoices:issue. وإصدار فاتورة صادرة أصلًا يرد بـ 200 مع already_issued: true.

حقول الطلب

القائمة الكاملة في مرجع الواجهة البرمجية. الأساسيات:

الحقل ملاحظات
issue_date مطلوب. بصيغة YYYY-MM-DD.
items مطلوب، من 1 إلى 200 بند. يحتاج كل بند description وquantity (أكبر من 0) وunit_price (قبل الضريبة).
items[].vat_rate نسبة مئوية، الافتراضي 15. أو tax_rate_id لاستخدام إحدى نسب الضريبة المحفوظة لديك.
items[].discount مبلغ يُخصم من البند قبل الضريبة.
customer اختياري: {"id": …} لعميل موجود، أو بيانات مباشرة. راجع العملاء.
external_reference معرّفك الخاص للعملية، ويجعل الإنشاء آمنًا عند إعادة المحاولة.
currency، exchange_rate SAR (الافتراضي) أو USD أو AED. العملة الأخرى تحتاج exchange_rate بالريال لكل وحدة.
document_discount_type، document_discount_value خصم على الفاتورة كلها (amount أو percent) يوزَّع على البنود قبل الضريبة.
due_date، notes، terms، locale اختيارية. locale (ar أو en) لغة مستند الفاتورة.

مدفوعة مسبقًا: فاتورة تصل مدفوعة

إذا كان موقعك يستلم الدفع عبر بوابة الدفع الخاصة به ثم يصدر الفاتورة، فأرسل الدفعة معها في payment. تُصدر الفاتورة وتُسجَّل الدفعة في الطلب نفسه، فتصل paid (أو partial إذا كانت الدفعة جزءًا من الإجمالي)، مع سند قبض مرقّم.

الحقل ملاحظات
payment.method cash (الافتراضي) أو bank_transfer أو mada أو visa_mastercard أو apple_pay أو stc_pay أو cheque أو other، أو رمز طريقة دفع أضافتها منشأتك.
payment.reference رقم العملية في بوابة الدفع. يظهر في سند القبض.
payment.amount اختياري. الافتراضي إجمالي الفاتورة، ولا يزيد عليه.
payment.paid_on اختياري. الافتراضي اليوم، ولا يكون يومًا لاحقًا.
payment.notes وpayment.account_id اختياريان. account_id حساب النقدية أو البنك الذي دخل فيه المبلغ.
  • يحتاج المفتاح إلى صلاحية payments:write، ويجب أن تكون الفاتورة صادرة (لا مع "issue": false).
  • إذا رُفضت الدفعة (payment_exceeds_balance أو validation_failed) لا يُنشأ شيء: لا تُصدر فاتورة دون دفعتها.
  • مع external_reference تعيد إعادة المحاولة الفاتورة نفسها ولا تسجل دفعة ثانية.
  • المبلغ الذي يصل لاحقًا (تحويل بنكي، أو تأكيد متأخر من البوابة) يُسجَّل بـ POST /invoices/{id}/payments. وكل دفعة ترسل حدث invoice.payment_recorded عبر Webhooks.
# Your gateway confirmed the payment: issue the invoice already paid.
curl -X POST https://fatoora.tw.sa/api/v1/invoices \
  -H "Authorization: Bearer wbk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "external_reference": "ORDER-1001",
    "issue_date": "2026-11-12",
    "customer": {"name": "Walk-in customer"},
    "items": [{"description": "Annual plan", "quantity": 1, "unit_price": 100}],
    "payment": {"method": "mada", "reference": "GW-TXN-55"}
  }'
// After your gateway's success callback, never before.
$response = $client->post('invoices', [
    'json' => [
        'external_reference' => $order->id,          // safe to retry
        'issue_date'         => date('Y-m-d'),
        'items'              => $order->lines,
        'payment'            => [
            'method'    => 'mada',                     // or visa_mastercard, apple_pay, stc_pay …
            'reference' => $gateway->transactionId,   // shown on the receipt
            // 'amount' defaults to the invoice total
        ],
    ],
    'http_errors' => false,
]);
$body = json_decode((string) $response->getBody(), true);
// $body['data']['invoice']['status'] === 'paid'
const res = await fetch("https://fatoora.tw.sa/api/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WONDERFATOORA_API_KEY}`,
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({
    external_reference: order.id, // safe to retry
    issue_date: new Date().toISOString().slice(0, 10),
    items: order.items,
    payment: { method: "mada", reference: gatewayTxn.id }, // amount defaults to the total
  }),
});
const body = await res.json();
// body.data.invoice.status === "paid", body.data.payment.number is the receipt
res = requests.post(
    "https://fatoora.tw.sa/api/v1/invoices",
    headers={"Authorization": f"Bearer {os.environ['WONDERFATOORA_API_KEY']}",
             "Accept": "application/json"},
    json={
        "external_reference": order["id"],  # safe to retry
        "issue_date": order["date"],
        "items": order["items"],
        "payment": {"method": "mada", "reference": txn["id"]},  # amount defaults to the total
    },
    timeout=60,
)
body = res.json()
# body["data"]["invoice"]["status"] == "paid"

إعادة المحاولة بأمان مع external_reference

الشبكات تتعطل. حين تنتهي مهلة طلب الإنشاء لا تعرف هل أُنشئت الفاتورة، والإصدار مرتين يعني فاتورتين ضريبيتين لا يمكن سحبهما. أرسل external_reference فريدًا من عندك مع كل طلب إنشاء:

  • الطلب الأول يرد بـ 201 Created مع الفاتورة الجديدة.
  • أي طلب لاحق بالمرجع نفسه يرد بـ 200 OK مع الفاتورة المنشأة سابقًا و"replayed": true. لا يُنشأ شيء جديد، ولا يُحتسب التكرار من باقتك.
  • في الطلب المكرر، المفتاح الذي لا يملك invoices:read لا يتلقى إلا id وnumber وstatus.
الطلب المكرر يعيد الفاتورة الأصلية حتى لو اختلف المحتوى الجديد. استخدم مرجعًا جديدًا لكل عملية بيع جديدة فعلًا.
import os, time, uuid
import requests

API = "https://fatoora.tw.sa/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WONDERFATOORA_API_KEY']}",
           "Accept": "application/json"}

def create_invoice(order):
    payload = {
        # Your own order id: a retry after a timeout returns the invoice
        # the first attempt created instead of issuing a second one.
        "external_reference": order["id"],
        "issue_date": order["date"],
        "items": order["items"],
    }
    for attempt in range(5):
        try:
            res = requests.post(f"{API}/invoices", json=payload, headers=HEADERS, timeout=60)
        except requests.RequestException:
            time.sleep(2 ** attempt)          # network error: same payload, try again
            continue
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "5")))
            continue
        if res.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        return res.json()                     # 200/201, or a 4xx to fix, not retry
    raise RuntimeError("WonderFatoora API unavailable, will retry later")
async function createInvoice(order) {
  const payload = {
    external_reference: order.id, // safe to retry
    issue_date: order.date,
    items: order.items,
  };
  for (let attempt = 0; attempt < 5; attempt++) {
    let res;
    try {
      res = await fetch("https://fatoora.tw.sa/api/v1/invoices", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.WONDERFATOORA_API_KEY}`,
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(60_000),
      });
    } catch {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      continue;
    }
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 5) * 1000));
      continue;
    }
    if (res.status >= 500) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      continue;
    }
    return res.json(); // 200/201, or a 4xx to fix rather than retry
  }
  throw new Error("WonderFatoora API unavailable, retry later");
}
تقبل كل عمليات الكتابة أيضًا ترويسة Idempotency-Key (من 8 إلى 255 حرفًا مرئيًا من ASCII): إعادة الطلب بالمفتاح والمحتوى نفسيهما تعيد الرد الأول مع Idempotent-Replayed: true، والمفتاح نفسه مع محتوى مختلف يُرفض بـ 422 idempotency_key_reused. تُحفظ المفاتيح 24 ساعة لكل بيانات اعتماد على حدة: لا يُعاد الرد إلا لمفتاح API (أو ربط التطبيق) الذي أرسل الطلب، أما مفتاح آخر للمنشأة يرسل Idempotency-Key نفسه فيُنفَّذ طلبه كطلب جديد، وكذلك بديل المفتاح بعد تجديده.

العملات الأجنبية والإعفاءات الضريبية

للتصدير أو أي بند آخر خاضع للنسبة الصفرية أو معفى، اجعل vat_rate يساوي 0 وضع رمز سبب الهيئة في vat_exemption_code، مثل VATEX-SA-32 (تصدير السلع) أو VATEX-SA-33 (تصدير الخدمات) أو VATEX-SA-35 (الأدوية والمعدات الطبية). في المنشأة المرتبطة بالهيئة يحتاج هذا البند سببه قبل إصدار الفاتورة، إلا إذا كانت العملية نفسها تدل عليه (التصدير).

{
  "issue_date": "2026-11-03",
  "currency": "USD",
  "exchange_rate": 3.75,
  "customer": { "name": "Example Inc.", "country": "US" },
  "items": [{
    "description": "Software licence (export of services)",
    "quantity": 1,
    "unit_price": 400,
    "vat_rate": 0,
    "vat_exemption_code": "VATEX-SA-33"
  }]
}

حالة الفاتورة لدى الهيئة

تحمل كل فاتورة كائن zatca يبين موقفها لدى هيئة الزكاة والضريبة والجمارك:

الحقل المعنى
flow clearance (الاعتماد) للفواتير الضريبية بين المنشآت، وreporting (الإبلاغ) للفواتير المبسطة، أو null.
status pending أو cleared أو reported أو rejected أو retrying أو not_submitted أو unknown؛ وnull حين لا يُرسل شيء (مسودة، أو منشأة غير مرتبطة بالهيئة).
with_warnings true حين قبلت الهيئة الفاتورة مع تحذيرات.
uuid المعرّف الفريد للفاتورة بعد إرسالها.
deliverable true حين يجوز تسليم الفاتورة للمشتري. الفاتورة الضريبية بين المنشآت تُحجز حتى تعتمدها الهيئة.
errors رسائل الهيئة نفسها حين ترفض الفاتورة (حتى 5 رسائل).
الحالة المعنى ماذا تفعل
pending أُرسلت أو في الانتظار، ولا رد بعد. اقرأ الفاتورة مجددًا بعد قليل.
retrying تعذر الوصول إلى الهيئة، وتُعاد المحاولة تلقائيًا. لا شيء؛ اقرأها لاحقًا.
cleared، reported قبلتها الهيئة. سلّمها (public_url).
rejected رفضتها الهيئة، ويبيّن errors السبب. عالج السبب (غالبًا بيانات المنشأة أو المشتري) من لوحة التحكم.
not_submitted حُجزت قبل وصولها إلى الهيئة، مثلًا لعدم اكتمال ربط المنشأة بالهيئة. راجع إعدادات الهيئة في لوحة التحكم.
unknown أُرسلت لكن النتيجة لم تُسجل. يطابقها فريق الدعم؛ تواصل معنا إن استمرت.

اشترك في Webhook الحدث invoice.zatca_status_changed ليصلك إشعار عند تغيرها. ومن دون Webhooks اقرأ GET /invoices/{id} بفواصل متزايدة (مثلًا بعد 5 ثوانٍ ثم 30 ثانية ثم دقيقتين ثم 10 دقائق) حتى يصبح deliverable مساويًا true أو تصبح الحالة نهائية.

مشاركة الفاتورة

public_url صفحة يفتحها عميلك مع إمكانية تنزيل PDF. وما دامت الفاتورة الضريبية بانتظار الاعتماد، تعرض الصفحة تنبيهًا بدل المستند.

جاهز للبدء؟

الوصول إلى الواجهة البرمجية ضمن باقات: الأعمال والمؤسسات. أنشئ مفتاحًا من الإعدادات ← مفاتيح API وأرسل أول طلب خلال دقائق.