OPT Retention
للمطورين

واجهة API والويب هوك

اربط الكاشير أو متجرك أو نظامك الخاص بـ OPT Retention: تسجيل العملاء والعمليات تلقائيًا، وإشعار فوري عند كل حدث. مفاتيح لكل تكامل، سجل طلبات، وإعادة إرسال عند الفشل.

البدء في 5 دقائق

واجهة API تتيح لنظام الكاشير أو المتجر أو أي برنامج لديك أن يسجّل العملاء والعمليات تلقائيًا في حسابك، وأن يستقبل إشعارًا فوريًا (ويب هوك) عند كل حدث مهم: تسجيل عميل، عملية، مكافأة، تقييم، موعد — وأن يزامن كاتالوج المنتجات والخدمات بصوره كروابط بدل إعادة إدخاله (التفاصيل).

  1. من لوحة التحكم: الإدارة ← API و Webhooks. الميزة ضمن باقة الاحتراف أو كإضافة «ربط API» على الباقات الأخرى.
  2. اضغط + مفتاح API، سمِّه (مثلًا «كاشير الفرع الرئيسي») وحدد النطاقات. السر يظهر مرة واحدة فقط — انسخه فورًا.
  3. جرّب أول طلب:
curl https://retention.aimenu.me/api/v1/customers?limit=5 \
  -H "Authorization: Bearer grs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
العنوان الأساسي: https://retention.aimenu.me/api/v1 · كل الطلبات والردود JSON بترميز UTF-8 · التواريخ بصيغة ISO 8601 (2026-09-22T14:05:00Z) · المبالغ بالهللات/القروش كأعداد صحيحة (amount_minor) أو كمبلغ عشري (amount).

المصادقة والنطاقات

أرسل المفتاح في ترويسة Authorization: Bearer <المفتاح> (أو X-Api-Key). المفتاح يخص منشأة واحدة ولا يرى غيرها.

النطاقيسمح بـ
customers:readعرض العملاء والبحث فيهم
customers:writeإنشاء عميل
transactions:writeتسجيل عملية شراء (تُحتسب نقاطها فورًا)
catalog:readقراءة المنتجات والخدمات المحفوظة (مزامنة الكاتالوج)
catalog:writeدفع/تحديث/أرشفة المنتجات والخدمات من نظام خارجي
customers:* / *كل نطاقات المجموعة / كل شيء

المفتاح يتوقف تلقائيًا إذا انتهى الاشتراك أو انتقلت المنشأة إلى باقة بلا API (ردّ 403).

العملاء

GET /api/v1/customers

قائمة العملاء مع ترقيم بالمؤشر (cursor).

معاملالنوعالوصف
limitعدد 1–100حجم الصفحة (افتراضي 50)
cursorنصقيمة meta.next_cursor من الصفحة السابقة
searchنصبحث في الاسم أو الجوال
lifecycle_stateنصnew · active · at_risk · churned
{
  "ok": true,
  "data": [
    { "id": "cust_8f2…", "name": "سارة العتيبي", "phone": "+9665xxxxxxxx",
      "lifecycle_state": "active", "visit_count": 12, "points": 340,
      "last_visit_at": "2026-09-18T16:20:00Z", "return_due_at": "2026-10-02T16:20:00Z" }
  ],
  "meta": { "limit": 50, "count": 1, "next_cursor": null, "has_more": false }
}

GET /api/v1/customers/{id}

عميل واحد بالشكل نفسه. غير موجود ← 404.

POST /api/v1/customers

حقلمطلوبالقواعد
nameنعمحتى 120 حرفًا
phoneنعمجوال؛ يُطبَّع تلقائيًا بمفتاح دولة المنشأة (05xxxxxxxx يصبح +9665xxxxxxxx). الجوال المكرر ← 422
expected_return_daysلا1–3650 (افتراضي 30) — دورة العودة المتوقعة لهذا العميل
branch_idلامعرّف فرع من فروعك
preferred_localeلاar-SA افتراضيًا
curl -X POST https://retention.aimenu.me/api/v1/customers \
  -H "Authorization: Bearer grs_…" -H "Content-Type: application/json" \
  -d '{"name":"سارة العتيبي","phone":"0551234567"}'
# 201 → { "ok": true, "data": { "id": "cust_…", … } }

الاستيراد بالجملة

لنقل عملاء أو فواتير سابقة من نظام قديم دفعة واحدة لا تحتاج API: صفحة استيراد البيانات في لوحة المنشأة تقبل ملف CSV أو لصقًا من Excel حتى 5000 صف، وتعرض معاينة قبل التنفيذ، وتسجّل الفواتير بتاريخها الحقيقي دون نقاط أو رسائل، ولكل استيراد زر تراجع. استعمل POST /customers وPOST /transactions أدناه للتكامل المستمر (عميل أو عملية لحظة حدوثها)، والاستيراد للدفعة الأولى.

تسجيل عملية شراء

POST /api/v1/transactions

يسجّل عملية للعميل ويحتسب نقاطه ومستواه ومكافآت الإحالة فورًا، ويشغّل الرسائل التلقائية (شكر بعد الزيارة، طلب التقييم).

ترويسة Idempotency-Key إلزامية (8–128 حرفًا، مثل رقم الفاتورة أو UUID). إعادة إرسال الطلب نفسه بالمفتاح نفسه لا تسجّل العملية مرتين، بل تعيد العملية الأصلية مع "duplicate": true. المفتاح نفسه ببيانات مختلفة ← 409.
حقلمطلوبالقواعد
customer_idنعممعرّف عميل من منشأتك
amount أو amount_minorنعم47.50 أو 4750 (هللات) — أكبر من صفر
currencyلاثلاثة أحرف؛ افتراضيًا عملة المنشأة (SAR)
occurred_atلاISO 8601؛ افتراضيًا الآن
external_transaction_idلارقم الفاتورة في نظامك — يُحفظ ويمنع التكرار أيضًا
branch_idلاافتراضيًا فرع العميل المفضل
curl -X POST https://retention.aimenu.me/api/v1/transactions \
  -H "Authorization: Bearer grs_…" -H "Content-Type: application/json" \
  -H "Idempotency-Key: INV-2026-000123" \
  -d '{"customer_id":"cust_8f2…","amount":180.00,"external_transaction_id":"INV-2026-000123"}'
# 200 → { "ok": true, "data": { "transaction": { "id": "…", "amount_minor": 18000, "currency": "SAR", "status": "completed" }, "duplicate": false } }

نصيحة للكاشير: أرسل العملية بعد إغلاق الفاتورة واستخدم رقم الفاتورة مفتاحًا للتكرار — فتستطيع إعادة المحاولة بأمان عند انقطاع الشبكة.

مزامنة الكاتالوج (المنتجات والخدمات)

نظامك — كاشير، قائمة رقمية، نظام حجز، ERP — يملك قائمة المنتجات والخدمات الأصلية، ولا نطلب من أحد أن يعيد إدخالها عندنا. المزامنة تجعل قائمتك هنا نسخة تابعة لقائمتك هناك: الاسم، الوصف، التصنيف، السعر، الرمز/الباركود، المدة (للخدمات)، الحالة، ورابط الصورة. الصور لا تُنسخ إلى خوادمنا أبدًا؛ نحفظ الرابط فقط ويعرضه متصفح العميل من مصدره.

طريقتان بنفس صيغة البيانات:
① الدفع (Push) — نظامك يستدعي PUT /api/v1/catalog/sync بكامل القائمة (أو POST /api/v1/catalog/items لعنصر واحد) كل مرة يتغير شيء أو دوريًا.
② الجلب (Pull) — تنشر ملف JSON على رابط HTTPS ثابت، ويسجّله صاحب المنشأة من التكاملات ← مزامنة الكاتالوج، فنجلبه نحن تلقائيًا كل 1–168 ساعة. لا يحتاج نظامك إلى مفتاح API في هذه الحالة.

النطاقان المطلوبان: catalog:write للدفع والأرشفة، catalog:read للقراءة. الميزة تتبع ميزة API نفسها؛ عناصر المنتجات تحتاج أن تكون «المنتجات والمخزون» ضمن باقة المنشأة، والخدمات تحتاج «الحجوزات» — وإلا يُرفض العنصر وحده بالرمز feature_not_available ويستمر الباقي.

هوية العنصر: source + external_id

كل عنصر تُعرّفه بـexternal_id — معرّفه الثابت في نظامك (رقم، UUID، SKU) حتى 64 حرفًا — تحت source واحد: معرّف نظامك (مثل main-pos؛ أحرف لاتينية صغيرة وأرقام و-/_، افتراضيًا api). الزوج (source, external_id) هو ما نحدّث به العنصر لاحقًا؛ لا تغيّر external_id إذا تغيّر الاسم أو السعر. مصدران مختلفان لا يتداخلان أبدًا.

التسكين الذكي (link_by_name، مفعّل افتراضيًا): إن كان صاحب المنشأة أدخل عناصر يدويًا قبل الربط، فأول مزامنة تتبنّى العنصر اليدوي الذي يطابق الاسم (أو SKU/الباركود للمنتجات) بدل تكراره، وتوسمه بمصدرك. أرسل "link_by_name": false لتعطيله.

حقول العنصر

حقلالنوعمطلوبالقواعد
external_idنصنعمثابت في نظامك، حتى 64 حرفًا
kindنصنعمproduct أو service
nameنصنعم2–80 حرفًا للمنتج، 2–120 للخدمة
descriptionنصلاحتى 300 حرف
categoryنصلااسم التصنيف حتى 40 حرفًا. للخدمات يُنشأ التصنيف تلقائيًا إن لم يوجد (بلا تكرار)
price أو price_minorعشري / عدد صحيحللمنتج نعم45.00 أو 4500 (هللات/قروش). الخدمة بلا سعر = 0 وprice_type: free
currencyنصلاإن أُرسلت فيجب أن تساوي عملة المنشأة، وإلا يُرفض العنصر
image_urlرابطلاhttps فقط، عام على الإنترنت، بلا بيانات دخول، حتى 600 حرف. يُحفظ كرابط ولا يُنزَّل. null أو غيابه = بلا صورة (تظهر الافتراضية للمنشأة)
statusنصلاactive (افتراضي) أو archived — الأرشفة تخفي العنصر من الكاشير وصفحة الحجز دون حذف تاريخه
sortعددلاترتيب العرض
updated_atISO 8601لايُحفظ للمرجع فقط
منتج فقط: sku (أحرف لاتينية/أرقام حتى 32، فريد داخل المنشأة) · barcode (4–32) · cost/cost_minor · vat_rate (15 أو 0) · track_stock (افتراضيًا false للعناصر المزامَنة — المخزون يبقى في نظامك)
خدمة فقط: duration_min (5–600، افتراضيًا 30) · price_type (fixed · from · free) · bookable_online (افتراضيًا true)

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

PUT /api/v1/catalog/sync — دفع القائمة كاملة أو جزئيًا

curl -X PUT https://retention.aimenu.me/api/v1/catalog/sync \
  -H "Authorization: Bearer grs_…" -H "Content-Type: application/json" \
  -d '{
    "source": "main-pos",
    "mode": "replace",
    "items": [
      { "external_id": "P-1001", "kind": "product", "name": "شامبو مرطّب 250 مل",
        "category": "عناية بالشعر", "price": 45.00, "sku": "SH-250", "barcode": "6281000101011",
        "image_url": "https://cdn.your-pos.com/img/sh-250.jpg", "status": "active", "sort": 1 },
      { "external_id": "S-77", "kind": "service", "name": "قص وتصفيف",
        "category": "شعر", "price": 80, "duration_min": 45, "price_type": "fixed",
        "image_url": "https://cdn.your-pos.com/img/cut.jpg" }
    ]
  }'
# 200 → { "ok": true, "data": { "source": "main-pos", "mode": "replace", "received": 2,
#         "created": 1, "updated": 1, "unchanged": 0, "archived": 3, "failed": 0, "errors": [] } }
معاملالوصف
sourceمعرّف نظامك (افتراضيًا api). استخدم معرّفًا ثابتًا واحدًا لكل نظام
modemerge (افتراضي): يضيف ويحدّث ما أرسلته فقط. replace: إضافةً إلى ذلك يؤرشف كل عنصر من نفس source لم يرد في هذه الدفعة — استخدمه عندما ترسل القائمة كاملة. لا يمس العناصر اليدوية ولا مصادر أخرى، ولا يحذف شيئًا
itemsقائمة حتى 500 عنصر في الطلب. القوائم الأكبر تُقسَّم إلى دفعات merge ثم دفعة أخيرة… أو تُبقي كل الدفعات merge وترسل الأرشفة صراحةً بـstatus: archived
link_by_nameالتسكين الذكي أعلاه (افتراضيًا true)

الرد دائمًا 200 ما دام الطلب سليمًا: العناصر المرفوضة تُعدّ في failed وتُفصَّل في errors (حتى 50) بمكان العنصر وسبب الرفض، ويُحفظ الباقي. الطلب كله يُرفض بـ422 فقط إذا كانت items ليست قائمة أو تجاوزت 500 أو كان mode/source غير صالح.

"errors": [
  { "index": 3, "external_id": "P-9", "field": "image_url", "code": "invalid",
    "message": "رابط الصورة يجب أن يبدأ بـ https://" },
  { "index": 7, "external_id": "S-2", "field": "kind", "code": "feature_not_available",
    "message": "الخدمات والحجز غير متاحة ضمن الباقة الحالية" }
]

POST /api/v1/catalog/items — عنصر واحد

نفس حقول العنصر مع source اختياري في الجسم. يعيد العنصر كما حُفظ: 201 عند الإنشاء، 200 عند التحديث أو بلا تغيير، 422 مع اسم الحقل عند الخطأ، 403 feature_not_available إن كان النوع خارج الباقة.

curl -X POST https://retention.aimenu.me/api/v1/catalog/items \
  -H "Authorization: Bearer grs_…" -H "Content-Type: application/json" \
  -d '{"source":"main-pos","external_id":"P-1001","kind":"product","name":"شامبو مرطّب 250 مل","price":49.5,"sku":"SH-250"}'
# 200 → { "ok": true, "data": { "result": "updated", "item": { "id": "…", "kind": "product", "external_id": "P-1001", "price_minor": 4950, … } } }

DELETE /api/v1/catalog/items/{external_id}?source=main-pos

يؤرشف العنصر (لا يحذفه): يختفي من الكاشير وصفحة الحجز ويبقى في التقارير القديمة. إعادة إرساله بـstatus: active تعيده. غير موجود لهذا المصدر ← 404.

GET /api/v1/catalog — قراءة ما هو محفوظ عندنا

معاملالوصف
kindproduct · service (افتراضيًا كلاهما)
sourceعناصر مصدر معيّن فقط. العناصر اليدوية لها "source": null
statusactive (افتراضي) · archived · all
updated_sinceISO 8601 — ما تغيّر بعد هذا الوقت
limit · cursor1–200 (افتراضي 100) وترقيم بالمؤشر كما في العملاء
{ "ok": true,
  "data": [ { "id": "…", "kind": "product", "source": "main-pos", "external_id": "P-1001",
              "name": "شامبو مرطّب 250 مل", "description": "", "category": "عناية بالشعر",
              "price": "45.00", "price_minor": 4500, "currency": "SAR", "sku": "SH-250", "barcode": "6281000101011",
              "image_url": "https://cdn.your-pos.com/img/sh-250.jpg", "status": "active", "sort": 1,
              "cost_minor": 2000, "vat_rate": 15, "track_stock": false,
              "updated_at": "2026-09-22T10:00:00Z", "last_synced_at": "2026-09-22T10:00:00Z" } ],
  "meta": { "limit": 100, "count": 1, "next_cursor": null, "has_more": false } }

image_url في القراءة هو الرابط الفعلي للصورة: الرابط الخارجي إن وُجد، وإلا رابط الصورة المرفوعة يدويًا، وإلا null (طبّق صورتك الافتراضية في نظامك).

الجلب (Pull): ملف كاتالوج ننسخه نحن

انشر على رابط HTTPS ثابت وعام (يجوز حمايته بترويسة مثل Authorization: Bearer … تُحفظ عندنا مشفرة) مستندًا بهذا الشكل — نفس حقول العنصر أعلاه، إما داخل items أو كمصفوفة مباشرة:

GET https://pos.example.com/retention/catalog.json
Content-Type: application/json

{ "version": "1", "generated_at": "2026-09-22T10:00:00Z",
  "items": [
    { "external_id": "P-1001", "kind": "product", "name": "شامبو مرطّب 250 مل", "price": 45.00,
      "category": "عناية بالشعر", "sku": "SH-250", "image_url": "https://cdn.your-pos.com/img/sh-250.jpg" },
    { "external_id": "S-77", "kind": "service", "name": "قص وتصفيف", "price": 80, "duration_min": 45 }
  ] }
  • الحد 5 ميجابايت و500 عنصر للملف؛ يُرفض localhost والعناوين الداخلية ولا نتبع التحويلات (redirects).
  • صاحب المنشأة يختار في اللوحة: source (المعرّف)، الوضع (merge/replace)، ودورة الجلب (كل 1/3/6/12/24/48 ساعة أو أسبوعيًا)، ويستطيع «مزامنة الآن» في أي وقت.
  • الأخطاء (رد غير 2xx، JSON غير صالح، غياب items) تُسجَّل في بطاقة المصدر مع آخر 20 محاولة، ولا تمس العناصر المحفوظة.
  • أفضل ممارسة: ولّد الملف من قاعدة بياناتك عند الطلب أو بمهمة مجدولة، ووفّر ETag/Cache-Control إن أمكن.

قواعد الصور

  • رابط مباشر إلى ملف صورة (jpg/png/webp) على https، قابل للفتح من أي متصفح بلا تسجيل دخول، لا يتغيّر مع كل طلب (لا روابط موقّعة قصيرة العمر).
  • الموصى به: مربعة 600×600 أو أكبر، أقل من 300 كيلوبايت. نحن لا نغيّر حجمها ولا ننسخها؛ نعرضها كما هي في اللوحة، شاشة الكاشير، صفحة الحجز، وصفحة الشريك.
  • الصور تُعرض فقط إذا فعّل صاحب المنشأة الإعدادات ← عرض الكاتالوج ← «مع الصور»؛ في وضع النصوص تُحفظ الروابط ولا تُعرض. العنصر بلا صورة يأخذ الصورة الافتراضية للمنشأة.
  • إن تعطّل رابط لاحقًا يعرض المتصفح الصورة الافتراضية تلقائيًا؛ أرسل الرابط الجديد في المزامنة التالية.

ما يحدث بعد المزامنة

  • العنصر يظهر فورًا في المنتجات/الخدمات بوسم «مزامَن: source»، وفي شاشة الكاشير، وفي صفحة الحجز (الخدمات المتاحة إلكترونيًا) وصفحة الشريك.
  • تعديل صاحب المنشأة اليدوي على حقل مزامَن (الاسم، السعر…) يُستبدل عند المزامنة التالية — الحقول المحلية تبقى. من الأفضل أن يعدّل في نظامك ثم يزامن.
  • كل دفعة تسجّل حدث catalog.synced (يصل إلى الويب هوك إن اشتركت فيه) بملخص الأعداد.
  • المخزون ليس جزءًا من المزامنة عمدًا: يبقى في نظامك، وعناصرك تأتي بـtrack_stock: false ما لم ترسل غير ذلك.

قائمة مزوّد الكاتالوج قبل الإطلاق

  • external_id ثابت لا يتغيّر بتغيّر الاسم أو السعر، وsource واحد ثابت لكل نظام.
  • ترسل القائمة كاملة بـreplace (أو تنشر ملف الجلب كاملًا) لتختفي العناصر المتوقفة من الكاشير تلقائيًا.
  • تعيد المحاولة بأمان: الطلب نفسه مرتين لا ينشئ تكرارًا (البصمة تتجاهل ما لم يتغيّر).
  • روابط الصور https مباشرة وثابتة، وتختبرها من متصفح خارج شبكتك.
  • تراقب failed/errors في الرد (أو بطاقة المصدر في اللوحة) وتصلح العناصر المرفوضة في المصدر.
  • عملة كل عنصر هي عملة المنشأة (أو لا ترسلها أصلًا).

الأخطاء والحدود

{ "error": { "code": "validation_failed", "message": "تعذر التحقق من الطلب.",
             "request_id": "req_…", "fields": { "phone": ["يجب أن يكون رقم هاتف صالحًا"] } } }
HTTPcodeالمعنى
401invalid_api_keyالمفتاح غير صالح أو محذوف
403insufficient_scope · feature_not_available · subscription_inactiveالنطاق لا يكفي / الباقة بلا API (أو نوع عنصر الكاتالوج خارج الباقة) / الاشتراك متوقف
400idempotency_key_requiredترويسة التكرار ناقصة
404not_foundالمسار أو العميل غير موجود
409idempotency_conflictالمفتاح نفسه ببيانات مختلفة
422validation_failedحقول غير صالحة — التفاصيل في fields
429rate_limitedأكثر من 240 طلبًا في الدقيقة للمفتاح — انتظر ثم أعد

أرفق request_id عند التواصل مع الدعم.

الويب هوك (إشعارات صادرة)

من الصفحة نفسها اضغط + Webhook: رابط HTTPS في نظامك + الأحداث التي تريدها (فارغ = كل الأحداث). يظهر السر whsec_… مرة واحدة — احفظه للتحقق من التوقيع.

كل حدث يُرسل بطلب POST بهذا الشكل:

POST https://your-system.example/hooks/retention
Content-Type: application/json
User-Agent: RetentionOS-Webhook/1.0
X-Retention-Event: transaction.completed
X-Retention-Event-Id: evt_…
X-Retention-Signature: sha256=3f1a…

{ "event": "transaction.completed", "timestamp": "2026-09-22T14:05:00Z",
  "data": { "transaction_id": "…", "customer_id": "cust_…", "amount_minor": 18000, "gross_minor": 18000, "discount_minor": 0, "currency": "SAR" },
  "meta": { "event_id": "evt_…", "correlation_id": null } }
  • ردّ بـ 2xx خلال ثوانٍ ثم عالج الحدث في الخلفية. أي ردّ آخر يُعاد إرساله تلقائيًا: بعد دقيقة، 5 دقائق، 15، ساعة، 6 ساعات (5 محاولات).
  • ردّ 4xx (عدا 408/429) يعني أن نظامك رفض الحدث فلا يُعاد؛ يظهر «فاشلة نهائيًا» في اللوحة مع زر إعادة الإرسال.
  • الأحداث قد تصل أكثر من مرة — اجعل المعالجة آمنة للتكرار باستخدام event_id.
  • الرابط يجب أن يكون HTTPS عامًا (لا localhost ولا عناوين داخلية).

جدول الأحداث

الحدثمتىأهم حقول data
customer.createdتسجيل عميل (QR، واي فاي، كاشير، API)customer_id, display_name
customer.mergedدمج عميلينsource_customer_id, target_customer_id
transaction.completedعملية مسجلة (كاشير/يدوي/API)transaction_id, customer_id, amount_minor, gross_minor, discount_minor, currency
transaction.voidedإلغاء عمليةtransaction_id, customer_id
sale.paid / sale.refundedبيع من كاشير المنصة / استردادnumber, total_minor, customer_id, invoice_id / amount_minor, full
reward.redeemedاستبدال مكافأةcustomer_id, reward_id, transaction_id
loyalty.adjustedتعديل نقاط يدويcustomer_id, delta
tier.changedتغيّر مستوى العميلcustomer_id, to, direction
coupon.issued / coupon.usedإصدار كوبون / استخدامهcustomer_id, coupon_id, code / transaction_id, discount_minor
referral.joined / referral.qualified / referral.rewardedصديق انضم / اشترى / مكافأة المُحيلreferrer_customer_id, referee_customer_id / transaction_id / grant_type
review.ratedتقييم بعد الزيارةcustomer_id, rating, positive
appointment.created / appointment.rescheduled / appointment.canceledالحجوزاتappointment_id, customer_id, starts_at, source / by
catalog.syncedدفعة مزامنة كاتالوج (دفع أو جلب)source, mode, received, created, updated, unchanged, archived, failed
game.wonجائزة من عجلة الحظcustomer_id, prize, kind

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

التوقيع = sha256= + HMAC-SHA256 لجسم الطلب الخام بالسر whsec_…. تحقق قبل أي معالجة:

// PHP
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_RETENTION_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
http_response_code(200); // ثم عالج $event في الخلفية
// Node.js (Express — استخدم الجسم الخام لا المُحلَّل)
app.post('/hooks/retention', express.raw({ type: '*/*' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const given = req.get('X-Retention-Signature') || '';
  if (expected.length !== given.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) return res.sendStatus(401);
  const event = JSON.parse(req.body);
  res.sendStatus(200); // ثم عالج event
});

قبل الإطلاق

  • مفتاح API منفصل لكل نظام (كاشير، متجر، ERP) بأقل نطاقات تكفيه؛ يمكن إيقاف أي مفتاح من اللوحة دون أن يتأثر غيره.
  • رقم الفاتورة هو Idempotency-Key وexternal_transaction_id معًا.
  • سجّل العميل أولًا (أو ابحث عنه بالجوال) ثم أرسل العملية بمعرّفه.
  • الكاتالوج يأتي من نظامك بـPUT /catalog/sync أو ملف جلب، بـexternal_id ثابت وروابط صور https — لا إدخال يدوي مزدوج.
  • معالِج الويب هوك يرد 2xx فورًا ويتجاهل الأحداث المكررة بـevent_id.
  • راقب صفحة «API و Webhooks»: آخر استخدام لكل مفتاح، وحالة تسليم كل ويب هوك، وإعادة الإرسال عند الفشل.

الدعم الفني للتكامل: تواصل معنا مع request_id أو event_id.