Madar Merchant API
واجهة برمجية لربط نظام متجرك (موقعك، WooCommerce، Shopify، نظام نقاط البيع…) بمنصة مدار مباشرة: أنشئ طلبات التوصيل تلقائياً، واعرف المنطقة والرسوم قبل الإنشاء، وتابع الحالة، واستقبل إشعارات فورية عبر Webhooks عند كل تغيير — دون إدخال يدوي في البوابة.
https://wasselni.naseir.site/api/v1
camelCaseyyyy-MM-ddTHH:mm:ssZILS — المبالغ أرقام عشرية برقمينكل استجابة — نجاحاً أو فشلاً — تأتي بنفس الغلاف، وتحمل ترويسة X-Request-Id (نفس قيمة requestId) أرسلها للدعم عند أي مشكلة:
/openapi/v1.json.
البدء السريع
mk_test_… واحفظه فوراً في متغير بيئة على خادمك — يُعرض مرة واحدة فقط./orders — لا يصل لأي كابتن ولا يمسّ رصيدك.POST /orders/{idOrRef}/simulate واستقبل حدث order.status_changed على رابط الـ Webhook الذي سجّلته في «ربط API».المصادقة
أرسل مفتاحك في الترويسة X-Api-Key مع كل طلب (ما عدا GET /health).
لا تُقبل كوكيز تسجيل الدخول على /api، ولا يوجد تحويل لصفحة الدخول — الأخطاء دائماً JSON.
- من خادمك فقط: لا تضع المفتاح أبداً في كود المتصفح أو تطبيق الجوال أو مستودع Git — احفظه في متغير بيئة (
MADAR_API_KEY). - يُعرض مرة واحدة: نخزّن بصمة المفتاح فقط (SHA-256)، فلا يمكن استرجاعه لاحقاً؛ تظهر في البوابة بادئته فقط (مثل
mk_live_7Hq2). - إعادة التوليد والإلغاء: من «ربط API» — إعادة التوليد تلغي المفتاح القديم فوراً وتُصدر جديداً؛ الإلغاء يوقف المفتاح نهائياً. الإدارة تستطيع إلغاء مفتاحك أيضاً.
- HTTPS إلزامي: الطلب عبر HTTP يُرفض بـ
400 https_requiredبلا تحويل.
| الحالة | HTTP | الكود |
|---|---|---|
لا توجد ترويسة X-Api-Key | 401 | missing_api_key |
| مفتاح بشكل خاطئ أو غير موجود أو ملغى | 401 | invalid_api_key |
| حساب المتجر غير مفعّل أو موقوف | 403 | merchant_not_active |
| محاولات مصادقة فاشلة كثيرة من نفس الـ IP | 429 | too_many_auth_failures |
الوضع التجريبي (Sandbox)
لكل متجر نوعان من المفاتيح، ومفتاح فعّال واحد من كل نوع في الوقت نفسه. نوع المفتاح وحده يحدد الوضع — نفس الـ Base URL ونفس الـ endpoints.
mk_live_…"livemode": true.mk_test_…"livemode": false.عزل الطلبات التجريبية
- لا تصل أبداً إلى الإدارة أو الكباتن، ولا تُنشئ رحلات جمع أو توصيل.
- لا تؤثر على الرصيد إطلاقاً: GET
/me/balanceبمفتاح اختبار يُرجع رصيد0وبلا حركات. - كل استجابة طلب وكل payload لـ Webhook تحمل الحقل
"livemode": false— تحقّق منه في نظامك قبل أي إجراء فعلي (شحن، فوترة…). - نفس
externalRefيمكن أن يوجد مرة واحدة في كل وضع — طلب تجريبيSHOP-10482لا يتعارض مع طلب حقيقي بنفس الرقم. - مفتاح الاختبار يرى الطلبات التجريبية فقط، ومفتاح الإنتاج يرى الحقيقية فقط.
محاكي الحالات POST /orders/{idOrRef}/simulate
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
idOrRef | string | نعم | معرّف الطلب التجريبي أو رقمه في المسار. |
status | string | نعم | الحالة المطلوبة من القيم المبيّنة أدناه. |
بما أن الطلب التجريبي لا يمر بكابتن، يمكنك تحريك حالته يدوياً لاختبار تكاملك وتلقّي Webhooks. المحاكي متاح أيضاً كأزرار في بوابة التاجر.
- لمفاتيح الاختبار فقط — استدعاؤه بمفتاح
mk_live_يُرجع403 test_mode_only. - الانتقال للأمام فقط ومن حالة غير نهائية — وإلا
409 invalid_transition(مثلاً محاكاةprocessingبعدout_for_delivery، أو أي محاكاة بعدdelivered/returned/cancelled). - كل محاكاة ناجحة تُطلق حدث
order.status_changedعلى الـ Webhook كأي تغيير حقيقي.
status المُرسَل | ما يراه التاجر merchantStatus | التسمية |
|---|---|---|
processing | processing | قيد التجهيز |
out_for_delivery | out_for_delivery | في الطريق |
delivered | delivered | تم التسليم |
returned | returned | مرتجع |
cancelled | cancelled | ملغي |
الاستجابة
بيانات الطلب التجريبي بعد الانتقال إلى الحالة المطلوبة.
الأخطاء
403 test_mode_only409 invalid_transition404 order_not_found
الطلبات
| الطريقة | المسار | الوصف |
|---|---|---|
| POST | /orders | إنشاء طلب |
| POST | /orders/bulk | إنشاء حتى 100 طلب دفعة واحدة |
| GET | /orders/{idOrRef} | طلب واحد مع سجل الحالات |
| GET | /orders | قائمة بفلاتر وتقسيم صفحات |
| POST | /orders/{idOrRef}/cancel | إلغاء طلب في حالة الانتظار |
| POST | /orders/{idOrRef}/simulate | محاكاة الحالة — مفاتيح الاختبار فقط (التفاصيل) |
إنشاء طلب POST /orders
يُرجع 201 مع ترويسة Location: /api/v1/orders/{id} لطلب جديد، أو 200 عند إعادة إرسال نفس externalRef (منع التكرار). حد حجم الجسم 64KB.
المعاملات
| الحقل | النوع | إلزامي | الوصف |
|---|---|---|---|
externalRef | string | موصى به | رقم الطلب في نظامك — مفتاح منع التكرار ≤64، [A-Za-z0-9_\-#./] |
recipientName | string | نعم | اسم المستلم 2–100 حرف |
recipientPhone | string | نعم | جوال المستلم، مثال 0599123456يُطبَّع (مسافات، +970/+972…) ثم ^05[69]\d{7}$ |
address | string | نعم | العنوان الوصفي ≤300 |
zone | string | لا | المنطقة؛ إن غابت تُحسب من الموقع اسم منطقة موجودة (مطابقة تامة) |
latitude | number | لا | خط العرض −90..90، مع longitude معاً |
longitude | number | لا | خط الطول −180..180 |
mapUrl | string | لا | رابط واتساب / Google Maps ≤500؛ تُستخرج منه الإحداثيات إن غابت |
paymentType | string | نعم | نوع التحصيل: الدفع عند الاستلام / مدفوع مسبقاًcod | prepaid |
codAmount | number | نعم | COD: قيمة الطلب التي تُحصَّل؛ Prepaid: قيمة إشارية لا تدخل الحسابات 0.01–99999 |
deliveryFeeTarget | string | لا | من يدفع رسوم التوصيلmerchant (افتراضي) | customer |
codCollectionTarget | string | لا | من يستلم مبلغ COD — لـ COD فقطwasselni_direct (افتراضي) | captain | merchant_pre_collected |
notes | string | لا | ملاحظات للكابتن ≤500 |
الاستجابة 201 Created:
totalCollect هو ما يجمعه الكابتن من الزبون: cod + رسوم على التاجر = codAmount؛
cod + رسوم على الزبون = codAmount + deliveryFee؛ prepaid + رسوم على الزبون = deliveryFee؛ prepaid + رسوم على التاجر = 0.
تحذيرات غير قاتلة تظهر في meta.warnings: zone_mismatch، cod_collection_target_ignored، phone_normalized.
الاستجابة
طلب جديد مع معرّف ورقم تتبّع ورسوم التوصيل. عند التكرار تعاد بيانات الطلب الأصلي.
الأخطاء
422 validation_failed422 zone_required409 external_ref_conflict
خيارات الدفع وأثرها المالي
cod + wasselni_direct الافتراضيawaitingDirectTransfer: true حتى التأكيد). عند التسليم: رصيدك += codAmount، ثم −= deliveryFee إن كان deliveryFeeTarget=merchant.cod + captaincodAmount − deliveryFee (إن كانت عليك).cod + merchant_pre_collectedcodAmount لرصيدك؛ يُخصم deliveryFee فقط إن كانت عليك.prepaidcodAmount قيمة إشارية فقط (الكابتن يُخبر الزبون بها) ولا تدخل أي حساب؛ عند التسليم يُخصم deliveryFee إن كانت عليك. codCollectionTarget يُتجاهل (تحذير cod_collection_target_ignored).deliveryFee دائماً (بغض النظر عن deliveryFeeTarget) ويُخصم returnFee إن كان أكبر من صفر.تحديد المنطقة
المنطقة تحدد رسوم التوصيل والمرتجع. ثلاث طرق، بالأولوية:
zone— اسم المنطقة كما يظهر في GET/zones(مطابقة تامة)؛ اسم غير موجود →422 zone_not_found. إن أرسلت معه إحداثيات لمنطقة أخرى يُعتمدzoneمع تحذيرzone_mismatch.latitude+longitude— تُكتشف المنطقة من حدودها المرسومة.mapUrl— رابط خريطة (واتساب / Google Maps) تُستخرج منه الإحداثيات؛ إن تعذّر →422 invalid_map_url.
لا منطقة ولا موقع → 422 zone_required. موقع خارج كل المناطق المرسومة → 422 zone_not_detected (أرسل zone صراحة). المناطق التي hasBoundary=false لا تُكتشف من الإحداثيات.
منع التكرار بـ externalRef
- أرسل رقم طلبك في
externalRef. إعادة إرسال نفس القيمة (بعد انقطاع شبكة أو timeout) تُرجع الطلب الموجود بـ200وmeta.replayed=trueوترويسةIdempotent-Replayed: true— دون إنشاء طلب ثانٍ. - المحتوى لا يُقارَن: الإعادة تُرجع الطلب الأصلي كما هو حتى لو اختلفت الحقول.
- إن كان
externalRefمستخدماً لطلب محذوف →409 external_ref_conflict. - الفرادة لكل متجر ولكل وضع (live / test) على حدة.
جلب طلب GET /orders/{idOrRef}
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
idOrRef | string | نعم | معرّف UUID أو رقم الطلب في المسار. |
idOrRef = المعرّف (uuid) أو refNumber (WP-…). يتضمن timeline من سجل الحالات تصاعدياً. طلب لمتجر آخر → 404 order_not_found. للبحث برقم طلبك استخدم GET /orders?externalRef=SHOP-10482.
الاستجابة
بيانات الطلب وسجل تغيّر الحالات في timeline.
الأخطاء
404 order_not_found
قائمة الطلبات GET /orders
| Query | النوع | الوصف |
|---|---|---|
status | string | مبسّطة، عدة قيم بفاصلة: pending,processing,out_for_delivery,delivered,returned,cancelled |
from, to | ISO date / datetime UTC | على createdAt؛ to بتاريخ فقط يشمل اليوم كاملاً |
externalRef | string | مطابقة تامة |
source | string | api | portal | app |
page | int ≥ 1 | افتراضي 1 |
pageSize | int 1..100 | افتراضي 20 |
ترتيب ثابت createdAt تنازلياً، بدون timeline. meta: page، pageSize، total، totalPages. قيمة غير صالحة (حالة غير معروفة، from > to، pageSize خارج النطاق) → 400 validation_failed.
الاستجابة
قائمة طلبات مع معلومات الصفحات في meta.
الأخطاء
400 validation_failed
إلغاء طلب POST /orders/{idOrRef}/cancel
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
idOrRef | string | نعم | معرّف الطلب أو رقمه في المسار. |
reason | string | لا | سبب الإلغاء، حتى 200 حرف. |
- مسموح للطلب في حالة
pendingفقط (قبل بدء رحلة الجمع). - جسم اختياري:
{ "reason": "الزبون ألغى" }(≤200 حرف). - ملغى مسبقاً →
200معmeta.alreadyCancelled=true(آمن للإعادة). - أي حالة أخرى →
409 order_not_cancellableمعdetails.status؛ غير موجود →404 order_not_found.
الاستجابة
بيانات الطلب بعد الإلغاء، مع meta.alreadyCancelled لتوضيح الإعادة.
الأخطاء
404 order_not_found409 order_not_cancellable
إنشاء جماعي POST /orders/bulk
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
orders | array | نعم | من 1 إلى 100 عنصر، لكل عنصر معاملات إنشاء طلب. |
الجسم { "orders": [ ... ] } من 1 إلى 100 طلب (وإلا 400 bulk_empty / 400 bulk_limit_exceeded)، وحد الحجم 1MB.
كل عنصر يمر بنفس تحقق POST /orders ويُحفظ مستقلاً — نجاح جزئي ممكن: الرد 200 وsuccess=true ما دامت البنية صالحة حتى لو فشلت كل العناصر؛ المرجع هو summary.failed وresults[].error.
مع externalRef تصبح إعادة إرسال الدفعة كاملة آمنة.
الاستجابة
ملخّص النتائج ونتيجة مستقلة لكل عنصر؛ قد تنجح بعض العناصر فقط.
الأخطاء
400 bulk_empty400 bulk_limit_exceeded413 payload_too_large
المناطق والرسوم
GET /zones
name هو القيمة التي تُرسل في حقل zone. أضف ?includeBoundary=true لإرفاق حدود GeoJSON Polygon ([lng, lat]) للمناطق المرسومة.
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
includeBoundary | boolean | لا | إرفاق حدود المناطق بصيغة GeoJSON عند true؛ الافتراضي false. |
الاستجابة
قائمة المناطق مع رسوم التوصيل والمرتجع ووجود الحدود.
الأخطاء
401 invalid_api_key
GET /zones/detect?lat=…&lng=…
نفس خوارزمية الإنشاء — لعرض الرسوم في صفحة الدفع عندك قبل إنشاء الطلب. إحداثيات غير صالحة → 400 validation_failed؛ خارج كل المناطق → 422 zone_not_detected.
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
lat | number | نعم | خط العرض بين −90 و90. |
lng | number | نعم | خط الطول بين −180 و180. |
الاستجابة
المنطقة المطابقة ورسوم التوصيل والمرتجع.
الأخطاء
400 validation_failed422 zone_not_detected
الحساب
GET /me
ملف المتجر + المفتاح المستخدم (البادئة فقط) + حالة الـ Webhook. مناسب كأول طلب لاختبار المفتاح. بمفتاح اختبار: livemode=false وbalance = 0.
المعاملات
لا توجد معاملات؛ أرسل ترويسة المصادقة فقط.
الاستجابة
ملف المتجر والمفتاح المستخدم وحالة ربط Webhook والوضع الفعّال.
الأخطاء
401 invalid_api_key403 merchant_not_active
GET /me/balance?limit=20
الرصيد وآخر الحركات (limit من 1 إلى 100، افتراضي 20). type: cod_credit، delivery_fee، return_fee، withdrawal، adjustment؛
amount بإشارته (الخصومات سالبة). بمفتاح اختبار: رصيد 0 وبلا حركات.
المعاملات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
limit | integer | لا | عدد الحركات، من 1 إلى 100؛ الافتراضي 20. |
الاستجابة
الرصيد والعملة وآخر الحركات؛ الوضع التجريبي يعيد صفراً بلا حركات.
الأخطاء
400 validation_failed401 invalid_api_key
Webhooks
سجّل رابط HTTPS واحداً من «ربط API» واحفظ السر whsec_… (يُعرض مرة واحدة). نرسل POST بجسم JSON موقّع عند كل حدث،
ويمكنك إرسال حدث تجريبي من زر «اختبار» أو عبر POST /webhooks/test (بلا جسم، حد 5 طلبات/دقيقة؛ 422 webhook_not_configured / webhook_disabled).
| الحدث | متى | data |
|---|---|---|
order.created | إنشاء طلب (من الـ API أو البوابة) | order مع change: null |
order.status_changed | كل تغيير حالة (حقيقي أو عبر المحاكي) | order + change |
webhook.test | زر «اختبار» أو POST /webhooks/test | { "message": "اختبار من مدار" } |
الترويسات والـ Payload
| الترويسة | المعنى |
|---|---|
X-Madar-Signature | t=<unix>,v1=<hex> — HMAC-SHA256 للنص "{t}.{rawBody}" بسر الـ Webhook |
X-Madar-Event | نوع الحدث |
X-Madar-Event-Id | معرّف الحدث (= id في الجسم) — ثابت عبر إعادات المحاولة: استخدمه لتجاهل التكرار |
X-Madar-Delivery | معرّف محاولة التسليم هذه |
التحقق من التوقيع
احسب HMAC على الجسم الخام كما وصل (قبل أي JSON parse)، وقارن بمقارنة ثابتة الزمن، وارفض أي t أبعد من 5 دقائق عن وقتك (حماية من إعادة الإرسال).
الترويسة قد تحمل أكثر من v1 عند تدوير السر — يكفي تطابق واحد.
التسليم وإعادة المحاولة
- ردّ بـ 2xx خلال 10 ثوانٍ — أي كود آخر أو انقطاع أو تجاوز المهلة يُعدّ فشلاً. استلم وأكّد سريعاً ثم عالج في الخلفية.
- جدول إعادة المحاولة بعد الفشل: 1 دقيقة 5 دقائق 30 دقيقة 2 ساعة 12 ساعة ← متوقف (6 محاولات كحد أقصى). يمكنك «إعادة الإرسال» يدوياً من سجل التسليم في البوابة.
- إيقاف تلقائي: بعد 25 فشلاً متتالياً يُعطَّل الـ Webhook — أصلح الرابط ثم فعّله من «ربط API».
- قد يصل الحدث أكثر من مرة (إعادة محاولة أو إعادة إرسال يدوية) وقد لا يصل بالترتيب — تجاهل التكرار بـ
X-Madar-Event-Id، واعتمدoccurredAtأو GET/orders/{idOrRef}للحالة النهائية. - الرابط يجب أن يكون HTTPS عاماً — العناوين الخاصة/الداخلية مرفوضة ولا تُتبع التحويلات (redirects).
الحالات
كل طلب يحمل status (الحالة التشغيلية الداخلية) وmerchantStatus (الحالة المبسّطة التي يراها التاجر) وmerchantStatusLabel (التسمية العربية).
ننصح ببناء منطقك على merchantStatus — قيمه ثابتة ولن تتغير. المسار:
pending ← collecting ← at_warehouse ← out_for_delivery ← delivered / returned.
الحالة الداخلية status | merchantStatus | ما يراه المتجر |
|---|---|---|
pending | pending | انتظار |
collecting / at_warehouse / assigned | processing | قيد التجهيز |
out_for_delivery | out_for_delivery | في الطريق |
delivered | delivered | تم التسليم |
returned | returned | مرتجع |
cancelled | cancelled | ملغي |
الحالات delivered وreturned وcancelled نهائية لا تتغير. القيمة الاحتياطية settled تُعرض للتاجر كـ delivered.
الأخطاء
عند الفشل: success=false وdata=null وerror يحمل code (ثابت بصيغة snake_case — ابنِ منطقك عليه)
وmessage (عربي للعرض، قد يتغير) وdetails اختيارية. أخطاء التحقق تُفصّل في details.fields[]. لا تظهر أي تفاصيل داخلية — أرسل requestId للدعم.
| HTTP | code | الرسالة |
|---|---|---|
| 400 | https_required |
يجب استخدام HTTPS |
| 400 | invalid_json |
جسم الطلب ليس JSON صالحاً |
| 400 | validation_failed |
بيانات غير صالحة |
| 400 | bulk_empty |
قائمة الطلبات فارغة |
| 400 | bulk_limit_exceeded |
الحد الأقصى 100 طلب في الدفعة |
| 401 | missing_api_key |
مفتاح API مفقود — أرسل ترويسة X-Api-Key |
| 401 | invalid_api_key |
مفتاح API غير صالح أو ملغى |
| 403 | merchant_not_active |
حساب المتجر غير مفعّل أو موقوف |
| 403 | test_mode_only |
هذه العملية متاحة في الوضع التجريبي فقط |
| 404 | order_not_found |
الطلب غير موجود |
| 404 | not_found |
المسار غير موجود |
| 405 | method_not_allowed |
طريقة HTTP غير مسموحة لهذا المسار |
| 409 | order_not_cancellable |
لا يمكن إلغاء الطلب بعد بدء رحلة الجمع |
| 409 | external_ref_conflict |
رقم الطلب الخارجي مستخدم لطلب محذوف |
| 409 | invalid_transition |
لا يمكن نقل الطلب إلى هذه الحالة |
| 413 | payload_too_large |
حجم الطلب أكبر من المسموح |
| 422 | zone_required |
حدّد المنطقة أو أرسل الموقع |
| 422 | zone_not_found |
المنطقة غير موجودة — راجع GET /zones |
| 422 | zone_not_detected |
الموقع خارج كل مناطق التوصيل المرسومة — أرسل حقل zone صراحة |
| 422 | invalid_map_url |
تعذّر استخراج الإحداثيات من رابط الخريطة |
| 422 | invalid_phone |
رقم الجوال غير صالح — مثال: 0599123456 |
| 422 | webhook_not_configured |
لم يُضبط رابط Webhook بعد |
| 422 | webhook_disabled |
الـ Webhook معطّل — فعّله من البوابة |
| 429 | too_many_auth_failures |
محاولات مصادقة فاشلة كثيرة — حاول لاحقاً |
| 429 | rate_limited |
تجاوزت الحد المسموح من الطلبات — حاول بعد قليل |
| 500 | internal_error |
خطأ داخلي — أرسل requestId للدعم |
| 503 | service_unavailable |
الخدمة غير متاحة مؤقتاً |
الحدود
| الحد | القيمة | عند التجاوز |
|---|---|---|
| لكل مفتاح API | 120 طلب / دقيقة | 429 rate_limited |
لكل عنوان IP (كل /api) | 300 طلب / دقيقة | 429 rate_limited |
| محاولات مصادقة فاشلة لكل IP | 20 خلال 5 دقائق | 429 too_many_auth_failures |
POST /webhooks/test | 5 طلبات / دقيقة | 429 rate_limited |
| حجم الجسم | 64KB | 413 payload_too_large |
حجم جسم POST /orders/bulk | 1MB، حتى 100 طلب | 413 payload_too_large / 400 bulk_limit_exceeded |
pageSize في القوائم | 1..100 | 400 validation_failed |
ردود 429 تحمل ترويسة Retry-After بالثواني — انتظرها قبل الإعادة، ويُفضّل استخدام تراجع أُسّي (exponential backoff) مع الأخطاء 5xx.
سجل التغييرات
externalRef واكتشاف المنطقة من الموقع،
جلب/قائمة/إلغاء الطلبات، المناطق والرسوم، الحساب والرصيد، Webhooks موقّعة مع إعادة المحاولة،
والوضع التجريبي بمفاتيح mk_test_ ومحاكي الحالات.