API v1

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

واجهة REST تغطي 28 مصدرًا: الفواتير الضريبية المتوافقة مع هيئة الزكاة والضريبة والجمارك، والعملاء والمنتجات والمخزون والمحاسبة والرواتب والأصول ونقاط البيع. كل الردود JSON، وكل المسارات تحت https://fawtar.app/api/v1.

المصادقة

كل طلب يحمل مفتاحًا تنشئه من التكاملات. المفتاح يُعرض مرة واحدة عند الإنشاء ويُخزَّن عندنا مجزّأً (SHA-256)، فلا يمكن استرجاعه — لو ضاع، ألغه وأنشئ غيره. الواجهة متاحة في باقة الأعمال.

الترويستان مقبولتان، وتحملان المفتاح نفسه:

Authorization: Bearer fwt_live_xxxxxxxxxxxxxxxxxxxx
# أو، بنفس الأثر
API-KEY: fwt_live_xxxxxxxxxxxxxxxxxxxx

لكل مفتاح صلاحيات محددة، والقراءة منفصلة عن الكتابة: مفتاح قراءة يرد 401 على أي محاولة كتابة. الصلاحيات المتاحة:

accounts:readaccounts:writeassets:readassets:writebills:readbills:writecustomers:readcustomers:writeinvoices:readinvoices:writejournal:readjournal:writepayments:readpayments:writepayroll:readpayroll:writepos:readpos:writeproducts:readproducts:writequotes:readquotes:writevendors:readvendors:writewebhooks:readwebhooks:write

الانتقال من قيود

الواجهة مصممة لتقبل عميلًا مكتوبًا لقيود دون إعادة كتابته: أسماء الحقول نفسها، وقواعد التصفية والفرز نفسها (ransack)، وترويسة API-KEY نفسها. في الغالب يكفي تغيير عنوان الخادم والمفتاح.

- https://www.qoyod.com/api/2.0/invoices
+ https://fawtar.app/api/v1/invoices

ما يختلف: فوتّر يرقّم الفواتير تسلسليًا بلا فجوات ويحسب رمز QR المتوافق مع الهيئة عند الإصدار، ويرحّل القيد إلى دفتر الأستاذ في العملية نفسها. لا حاجة لاستدعاء إضافي لأي من ذلك.

التقسيم والفرز

كل قائمة مقسّمة إلى صفحات: page يبدأ من 1، وlimit افتراضه 50 وحدّه الأعلى 100. الرد يحمل الإجمالي في meta.total لتعرف كم صفحة بقيت.

curl "https://fawtar.app/api/v1/invoices?page=2&limit=25&q[s]=issued_at desc" \
  -H "Authorization: Bearer $FAWTAR_KEY"

{
  "data": [ { "id": "…", "number": "INV-0042", "total": 1150.00 } ],
  "meta": { "page": 2, "limit": 25, "total": 318 }
}

التصفية

التصفية بصيغة q[الحقل_الشرط]. الحقول المسموح بها محددة لكل مصدر أدناه، وأي حقل خارجها يُتجاهل ولا يُمرَّر إلى قاعدة البيانات.

الشرطالمعنىمثال
eqيساويq[status_eq]=issued
not_eqلا يساويq[status_not_eq]=draft
contيحتويq[name_cont]=محمد
startيبدأ بـq[sku_start]=ACC
endينتهي بـq[sku_end]=-01
gtأكبر منq[total_gt]=1000
gteqأكبر من أو يساويq[issued_at_gteq]=2026-01-01
ltأصغر منq[total_lt]=500
lteqأصغر من أو يساويq[issued_at_lteq]=2026-12-31
inضمن قائمة مفصولة بفاصلةq[status_in]=issued,paid
nullفارغ أو غير فارغq[due_date_null]=true

الأخطاء

كل خطأ يرد بنفس الشكل: رمز ثابت في error صالح للتفريع البرمجي، ورسالة للبشر عند وجودها. أخطاء التحقق تحمل التفاصيل لكل حقل.

{
  "error": "validation_failed",
  "details": {
    "iban": ["iban must be a Saudi IBAN"],
    "basic_salary": ["Expected number, received string"]
  }
}
الحالةالرمزالمعنى
400invalid_jsonجسم الطلب ليس JSON صالحًا.
401unauthorizedالمفتاح مفقود أو ملغى أو منتهٍ أو لا يحمل الصلاحية المطلوبة.
404not_foundلا يوجد سجل بهذا المعرّف ضمن منشأتك.
405method_not_allowedالعملية غير مدعومة على هذا المسار.
409delete_refusedالحذف مرفوض لأن السجل مرتبط بشيء مرحَّل.
422validation_failedفشل التحقق. التفاصيل في details لكل حقل.
429rate_limitedتجاوزت الحد. أعد المحاولة بعد المدة في ترويسة Retry-After.
500query_failedخطأ غير متوقع من جانبنا.

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

120 طلبًا لكل 60 ثانية لكل مفتاح. عند التجاوز يرد 429 مع ترويسة Retry-After بالثواني — انتظر المدة المذكورة بدل إعادة المحاولة فورًا.

المصادر

كل مصدر يذكر عملياته، وصلاحيته، والحقول التي يمكن التصفية والفرز بها.

المبيعات

الفواتير /api/v1/invoices

invoices:read · invoices:write

إصدار الفواتير الضريبية وقراءتها.

GETGET /:idPOST

  • taxTreatment"tax" | "ordinary"ضريبية أم عادية لمنشأة غير مسجلة
  • type"simplified" | "standard"مبسطة للأفراد، قياسية للمنشآت
  • pricesIncludeVatbooleanهل الأسعار المرسلة شاملة الضريبة
  • buyerobjectname, vatNumber, crNumber, address, phone, email
  • lines[]arraydescription, quantity, unitPrice, vatRate
  • dueDateYYYY-MM-DD

تصفية: number, status, paid_status, type, tax_treatment, buyer_name, issued_at, due_date, total, created_at · فرز: number, issued_at, due_date, total, created_at

الفاتورة الصادرة وثيقة نظامية ورمز QR محسوب من محتواها، فلا تُعدَّل — تُصحَّح بإشعار دائن عبر POST /credit_notes. الترقيم متسلسل بلا فجوات ويُخصَّص عند الإنشاء.

الإشعارات الدائنة /api/v1/credit_notes

invoices:read

تصحيح فاتورة صادرة بالخصم.

GETGET /:id

تصفية: number, status, buyer_name, issued_at, total, created_at · فرز: number, issued_at, total, created_at

الإشعارات المدينة /api/v1/debit_notes

invoices:read

تصحيح فاتورة صادرة بالزيادة.

GETGET /:id

تصفية: number, status, buyer_name, issued_at, total, created_at · فرز: number, issued_at, total, created_at

عروض الأسعار /api/v1/quotes

quotes:read · quotes:write

عروض الأسعار قبل تحوّلها إلى فواتير.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: number, status, buyer_name, created_at · فرز: number, created_at, total

الطلبات /api/v1/orders

invoices:read · invoices:write

طلبات المتجر قبل الفوترة.

GETGET /:idPOSTPUT · PATCH /:id

تصفية: status, created_at · فرز: created_at

العملاء والمنتجات

العملاء /api/v1/customers

customers:read · customers:write

بيانات العملاء وأرقامهم الضريبية والعنوان الوطني.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

  • namestringمطلوب
  • vat_numberstring15 رقمًا للفوترة B2B
  • street / building / district / city / postal_codestringالعنوان الوطني المطلوب للفواتير القياسية

تصفية: name, vat_number, cr_number, email, phone, city, created_at · فرز: name, created_at, city

المنتجات /api/v1/products

products:read · products:write

المنتجات والخدمات وأسعارها.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

  • namestring
  • skustring
  • unit_pricenumberغير شامل الضريبة
  • vat_ratenumber0.15 لخمسة عشر بالمئة

تصفية: name, sku, barcode, unit_price, created_at · فرز: name, unit_price, created_at

التصنيفات /api/v1/categories

products:read · products:write

تصنيفات المنتجات.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: name · فرز: name

وحدات القياس /api/v1/product_unit_types

products:read · products:write

وحدات بيع المنتجات.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: name · فرز: name

الموردون /api/v1/vendors

vendors:read · vendors:write

بيانات الموردين.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: name, vat_number, email, phone · فرز: name, created_at

المشتريات والمخزون

فواتير المشتريات /api/v1/bills

bills:read · bills:write

فواتير الموردين ببنودها.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: status, vendor_id, created_at · فرز: created_at, total

المصروفات /api/v1/simple_bills

bills:read · bills:write

مصروف بمبلغ واحد دون بنود.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: vendor, category, spent_at, amount · فرز: spent_at, amount, created_at

مدفوعات المشتريات /api/v1/bill_payments

payments:read · payments:write

سداد فواتير الموردين.

GETGET /:idPOST

تصفية: bill_id, paid_at · فرز: paid_at

المستودعات /api/v1/inventories

products:read · products:write

المستودعات والفروع.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: name · فرز: name

أرصدة المخزون /api/v1/inventory_levels

products:read

الكميات المتاحة لكل منتج في كل مستودع.

GETGET /:id

تصفية: product_id, inventory_id · فرز: product_id

تسويات المخزون /api/v1/inventory_adjustments

products:read · products:write

زيادة أو إنقاص كمية بسبب جرد أو تلف.

GETPOST

تصفية: product_id, created_at · فرز: created_at

تحويلات المخزون /api/v1/inventory_transfers

products:read · products:write

نقل كمية بين مستودعين.

GETPOST

تصفية: product_id, created_at · فرز: created_at

التحصيل والمحاسبة

الدفعات /api/v1/invoice_payments

payments:read · payments:write

تسجيل دفعة على فاتورة.

GETGET /:idPOST

تصفية: invoice_id, method, paid_at · فرز: paid_at, amount

سندات القبض /api/v1/receipts

payments:read

الدفعات نفسها بصيغة سند القبض.

GETGET /:id

تصفية: invoice_id, method, amount, paid_at · فرز: paid_at, amount

دليل الحسابات /api/v1/accounts

accounts:read · accounts:write

شجرة الحسابات وأنواعها.

GETGET /:idPOSTPUT · PATCH /:id

تصفية: code, name_ar, type, is_postable · فرز: code, type

القيود المحاسبية /api/v1/journal_entries

journal:read · journal:write

القيود المرحّلة إلى دفتر الأستاذ.

GETGET /:idPOST

تصفية: entry_no, entry_date, source_type, status · فرز: entry_date, entry_no

القيد يجب أن يتوازن: مجموع المدين يساوي مجموع الدائن بالهللة. القيد غير المتوازن يُرفض بـ 422 وليس بقيد ناقص.

القيود المتكررة /api/v1/recurring_journals

journal:read

جدولة القيود التي تتكرر كل فترة.

GETGET /:id

تصفية: name, cadence, active, next_run · فرز: next_run, name, created_at

للقراءة فقط: سطور القيد لا بد أن تتوازن، وهو شرط لا يفرضه مسار عام.

الرواتب والأصول

الموظفون /api/v1/employees

payroll:read · payroll:write

بيانات الموظفين ورواتبهم وبدلاتهم.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

  • national_idstring10 أرقام
  • ibanstringSA ثم 22 رقمًا — يُرفض غير ذلك
  • nationality"saudi" | "non-saudi"
  • gosi_scheme"existing" | "new"
  • basic_salary / housing_allowance / transport_allowancenumberوعاء التأمينات = الأساسي + السكن فقط

تصفية: name, national_id, nationality, is_active, created_at · فرز: name, basic_salary, created_at

مسيّرات الرواتب /api/v1/payroll_runs

payroll:read

المسيّرات الشهرية وإجمالياتها.

GETGET /:id

تصفية: period_year, period_month, status, created_at · فرز: period_year, period_month, created_at

للقراءة فقط. المسيّر يُنشأ من التطبيق، وقسائمه تحتفظ بأرقامها عند نسب التأمينات التي كانت سارية في شهرها.

قسائم الرواتب /api/v1/payslips

payroll:read

قسيمة كل موظف داخل المسيّر.

GETGET /:id

تصفية: run_id, employee_id, employee_name · فرز: employee_name, net

الأصول الثابتة /api/v1/assets

assets:read · assets:write

سجل الأصول وإهلاكها.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

  • costnumber
  • salvage_valuenumberلا تتجاوز التكلفة
  • useful_life_monthsinteger
  • method"straight_line" | "declining_balance"
  • accumulatednumberللقراءة فقط — يُحدَّث بالإهلاك الشهري

تصفية: name, code, category, status, acquired_on · فرز: name, acquired_on, cost, created_at

accumulated و last_depreciated_on تُقرأ ولا تُكتب: هي سجل ما رُحِّل فعلًا إلى دفتر الأستاذ.

نقاط البيع

الطاولات /api/v1/pos_tables

pos:read · pos:write

طاولات صالة المطعم.

GETGET /:idPOSTPUT · PATCH /:idDELETE /:id

تصفية: name, area, is_active · فرز: name, created_at

الطلبات /api/v1/pos_orders

pos:read

طلبات نقاط البيع المفتوحة والمحصّلة.

GETGET /:id

تصفية: order_no, status, order_type, table_id, opened_at · فرز: opened_at, order_no, settled_at

للقراءة فقط: تحصيل الطلب يُصدر فاتورة ضريبية ويحرّك المخزون، وتحديث الصف مباشرة يتجاوز ذلك كله.

مثال: إصدار فاتورة

curl -X POST "https://fawtar.app/api/v1/invoices" \
  -H "Authorization: Bearer $FAWTAR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taxTreatment": "tax",
    "type": "simplified",
    "pricesIncludeVat": false,
    "buyer": { "name": "شركة المثال", "vatNumber": "300000000000003" },
    "lines": [
      { "description": "اشتراك سنوي", "quantity": 1, "unitPrice": 1000, "vatRate": 0.15 }
    ],
    "dueDate": "2026-10-01"
  }'

{
  "id": "8f14e45f-…",
  "number": "INV-0042",
  "url": "https://fawtar.app/app/invoices/8f14e45f-…"
}

الويبهوك

بدل أن يسأل نظامك فوتّر كل دقيقة عن جديد، يرسل فوتّر طلب POST إلى رابطك لحظة وقوع الحدث. أضف الرابط من التكاملات واحفظ مفتاح التوقيع الذي يظهر مرة واحدة.

الأحداث

  • invoice.createdإصدار فاتورة
  • invoice.paidسداد فاتورة بالكامل
  • payment.receivedاستلام دفعة
  • customer.createdإضافة عميل
  • quote.acceptedقبول عرض سعر
  • expense.createdتسجيل مصروف
  • payroll.postedترحيل مسيّر رواتب
  • pos.order_settledتحصيل طلب مطعم
  • journal.postedترحيل قيد

شكل الرسالة

POST /your-endpoint
Content-Type: application/json
Fawtar-Event: invoice.paid
Fawtar-Delivery: 3c9e6f7a-…
Fawtar-Signature: t=1800000000,v1=5f2b…

{
  "event": "invoice.paid",
  "created_at": "2026-08-28T09:14:22.031Z",
  "data": { "invoice_id": "…", "invoice_number": "INV-0042", "total": 1150 }
}

التحقق من التوقيع

التوقيع HMAC-SHA256 على النص `${t}.${rawBody}` بمفتاحك. الطابع الزمني موقَّع مع الجسم تحديدًا كي لا تُعاد رسالة قديمة صحيحة التوقيع لاحقًا — تحقق من عمرها، وارفض ما تجاوز خمس دقائق. قارن التوقيعين بمقارنة ثابتة الزمن، ووقّع على الجسم الخام قبل أي تحليل JSON.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=", 2))
  );
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(parts.v1 ?? "", "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

إعادة المحاولة

أي رد خارج نطاق 2xx يُعد فشلًا. نعيد المحاولة حتى 6 مرات بفواصل متزايدة (1، 5، 30، 120، 360 دقيقة). الرابط الذي يفشل مرارًا يُوقَف تلقائيًا، ويظهر ذلك في صفحة التكاملات مع زر لإعادة تفعيله.

ردّ بـ 200 فور الاستلام وعالج الرسالة بعد ذلك: المهلة عشر ثوانٍ. وتعامل مع التكرار — قد تصل الرسالة نفسها مرتين، لذا استخدم Fawtar-Delivery مفتاحًا للتفرد.