دليل التكامل لسوق متعدد البائعين
لمطوّري سوقٍ يشحن طلبات بائعيه عبر شركة توصيل تعمل على منصة توصيل. العقد الكامل في
openapi.yaml، وكل الطلبات جاهزة في مجموعة Postman بجانبه.
١. المبدأ في سطرين
أنتم حساب واحد عند شركة التوصيل (مفتاح API واحد)، وتحتكم بائعون كثيرون. الطلب الذي يضم بضاعة من أكثر من بائع يصير طرداً لكل بائع: يُستلم من باب البائع، يصل للزبون، ويُدفع عند الباب بحصته. المال المحصَّل يُسوّى معكم أنتم (تسوية أسبوعية)، وأنتم تسوّون مع بائعيكم.
٢. الاتصال
| العنوان | https://{نطاق شركة التوصيل}/partner/v1 — تعطيكم إياه الشركة |
| المصادقة | ترويسة X-API-Key: <prefix>.<secret> — تولّده الشركة من لوحتها |
| الصلاحيات | shipments:write (الإرسال والإلغاء والبائعون)، shipments:read (القراءة والملصقات) |
| تكرار آمن | كل كتابة تقبل Idempotency-Key (UUID لكل محاولة منطقية). نفس المفتاح بنفس الجسم ← الرد المحفوظ |
| الأخطاء | 422 وجسم {"message": "...", "errors": {...}} بالعربية، تسمّي الحقل |
٣. البائعون — مرة لكل بائع
POST /partner/v1/sellers
{
"ref": "SL-17", // رقم البائع عندكم
"name": "أحذية الشام",
"phone": "0944111222",
"lat": 33.5060, "lng": 36.2600, // باب الاستلام — أو "tawseel_code"
"landmark": "المزة، جانب الجامع"
}
201 أول مرة، و200 حين تحدّثونه بنفس ref (غيّر موقعه ← تتغيّر نقطة الاستلام للطلبات التالية).
الموقع إحداثيات أو رمز توصيل — نص العنوان وحده لا يكفي ليجد الكابتن البائع. GET /partner/v1/sellers للقائمة.
يمكن أيضاً إرسال البائع كاملاً داخل الطلب (seller: {...}) فيُسجَّل معه.
٤. الطلب
POST /partner/v1/store-orders
{
"external_id": "MP-5001", // هوية الطلب عندكم — إعادة الإرسال تحدّثه ولا تكرّره
"external_number": "ORD-5001", // ما يُطبع على الملصق ويقرؤه الكابتن
"status": "processing", // الشحن يبدأ عند هذه الحالة (انظر ٧)
"recipient": { "name": "رنا", "phone": "0933555666" },
"delivery": {
"lat": 33.5250, "lng": 36.2900, // الأفضل؛ أو "tawseel_code"؛ أو "address_text" + "city" + "zone"
"address_text": "المالكي، بناء 4",
"notes": "الطابق الثالث"
},
"payment": { "method": "cod", "cod_amount": 75000, "currency": "SYP" },
"packages": [
{ "ref": "P1", "seller_ref": "SL-17", "cod_amount": 45000, "weight_kg": 1.2,
"items": [{ "name": "حذاء رياضي", "qty": 1, "price": 45000 }] },
{ "ref": "P2", "seller_ref": "SL-42", "cod_amount": 30000,
"items": [{ "name": "حقيبة يد", "qty": 1, "price": 30000 }] }
]
}
- مجموع
cod_amountللطرود =payment.cod_amount، وإلا422برسالة تذكر الرقمين. إن لم ترسلواpayment.cod_amountيُحسب من الطرود. الطلب المدفوع مسبقاً (prepaid) لا يحمل أي طرد فيه مبلغاً. - بائع واحد؟ بدل
packagesأرسلوا"seller_ref": "SL-17"في جذر الطلب. - بائع غير معروف يوقف الطلب كله:
status: failedوreasonيسمّيه. سجّلوه ثم أعيدوا إرسال الطلب نفسه فيُشحن — لا يُشحن نصف طلب أبداً. - عنوان ضعيف (نص بلا منطقة واضحة): الطلب
needs_address، ونرسل للزبون رسالة يضع فيها دبوسه، ويُشحن تلقائياً حين يفعل. لا شيء عليكم.
الرد 202 (وصل وسُجّل — اقرؤوا data.status):
{
"store_order_id": 812,
"status": "shipped",
"data": {
"external_id": "MP-5001",
"status": "shipped",
"reason": null,
"payment": { "method": "cod", "cod_amount": 75000, "currency": "SYP" },
"packages": [
{ "ref": "P1", "seller_ref": "SL-17", "cod_amount": 45000,
"tracking_code": "TW-8QK2M4", "reference": "ORD-5001/P1", "status": "awaiting_pickup",
"tracking_url": "https://track.cargox.sy/track/TW-8QK2M4",
"label_url": "https://…/partner/v1/shipments/TW-8QK2M4/label.pdf" },
{ "ref": "P2", "seller_ref": "SL-42", "…": "…" }
]
}
}
tracking_url صفحة تتبع جاهزة للزبون باسم شركة التوصيل — أرسلوها له أو اعرضوا الحالة في تطبيقكم.
٥. الملصقات والاستلام
GET /partner/v1/store-orders/{external_id}/labels.pdf— ملصقات الطلب كله، أوlabel_urlلكل طرد. اعرضوها في لوحة البائع ليطبعها ويلصقها على الطرد.- طرود البائع تُستلم من بابه: طلب استلام واحد مفتوح لكل باب، وطرود اليوم من البائع نفسه تنضم إليه، فيأتي الكابتن مرة ويمسحها كلها.
٦. ما يحدث بعد ذلك — webhooks
سجّلوا نقطة مرة: POST /partner/v1/webhooks {"url": "https://…", "events": ["store_order.updated", "shipment.returned", "settlement.closed"]}
— الرد يحمل secret، احفظوه.
| الحدث | متى | ماذا يحمل |
|---|---|---|
store_order.updated |
عند إنشاء الطرود وعند كل تغيّر لأي طرد | الطلب كاملاً (نفس شكل data أعلاه) + changed (الطرد الذي تغيّر) |
shipment.status_changed / shipment.delivered |
لكل طرد | tracking_code، reference (ORD-5001/P1)، from_status، status، note |
shipment.returned |
طرد عاد للبائع بعد فشل التسليم | tracking_code، reference، return_tracking_code، reason |
express.location_updated |
بالاسم فقط (لا يصل لمن اشترك بـ*): موقع الكابتن من الإسناد حتى الباب، مرة كل 10 ث على الأكثر لكل طلب سريع |
code، external_ref، status، position (lat، lng، heading، speed_kmh، recorded_at)، heading_to (pickup/dropoff)، target، eta |
settlement.closed / settlement.paid |
التسوية الأسبوعية للتحصيل | الإجمالي والرسوم والصافي والعملة وعدد البنود، وlinks إلى البنود وحصص البائعين (§٩) |
كل إرسال:
POST https://your-app/hooks
X-Tawseel-Signature: <hex HMAC-SHA256 للجسم الخام بالـsecret>
X-Tawseel-Event-Id: evt_93812
X-Tawseel-Timestamp: 1793102400
{"id": "evt_93812", "event": "store_order.updated", "occurred_at": "2026-10-28T10:40:00+03:00", "data": {…}}
- تحقّقوا من التوقيع على الجسم كما وصل، قبل أي تحليل.
idثابت في كل إعادة محاولة — تجاهلوا ما رأيتموه. نعيد حتى 3 مرات إن لم يصل2xx.- ردّوا بسرعة (
2xxخلال 8 ثوانٍ) وعالجوا في طابوركم.
الحالات الأهم للطرد: awaiting_pickup (ينتظر الكابتن عند البائع) ← picked_up ← at_hub / in_transit ←
out_for_delivery ← delivered؛ أو failed (محاولة فاشلة، مع السبب في note) ← محاولة أخرى أو returned.
٧. الإلغاء، والقراءة، والحالة التي تبدأ الشحن
POST /partner/v1/store-orders/{external_id}/cancel {"reason": "ألغى الزبون"}— يلغي كل طرود الطلب ما دامت لم تدخل شبكة التشغيل. إن دخلها أيٌّ منها يُرفض كله (422) — الزبون سيصله الطرد على أي حال.GET /partner/v1/store-orders/{external_id}— الطلب وطروده في أي وقت.GET /partner/v1/shipments?reference=ORD-5001— طرود طلب بمرجعكم.- الحالة التي تبدأ الشحن
processing: أرسلوا الطلب بأي حالة لتسجيله، ثم أعيدوا إرساله بـprocessingحين يكون البائعون جاهزين — يُشحن حينها دون أي شيء آخر.
٨. أنتم تقررون — حقول المرونة
كل رقم نحسبه يمكنكم أن ترسلوه أنتم بدلاً عنّا؛ ما لا ترسلونه نحسبه.
الطلب السريع (أكل وماركت، POST /partner/v1/express):
| الحقل | ما يفعله |
|---|---|
discount |
كوبون أو خصم بالمبلغ — يُطرح مما يقبضه الكابتن. لا يتجاوز الطلب مع الرسم |
discount_funded_by |
merchant (أنتم — المتجر يُدفع له كاملاً، الافتراضي) أو store (يُطرح أيضاً مما يدفعه الكابتن للمتجر) |
cod_amount |
ما يقبضه الكابتن بالضبط — جزء دُفع من محفظة، إكرامية، أي قاعدة عندكم |
store_payment |
ما يدفعه الكابتن للمتجر بالضبط عند الاستلام (لمتجر captain_pays) |
delivery_fee |
رسم التوصيل كما تحددونه، بدل قواعد الشركة |
ready_at |
طلب مجدول (خلال 14 يوماً) — لا يُعرض على كابتن إلا قرب موعده |
metadata |
أي بيانات لكم (حتى 4 ك.ب) — تعود كما هي في كل رد وwebhook |
يُرفض ما لا يمكن أن يكون صحيحاً: خصم أكبر من الطلب، أو مبلغ تحصيل على طلب مدفوع مسبقاً.
طلب الطرود (/store-orders): cod_amount لكل طرد، وmetadata تعود في الرد وفي store_order.updated.
الـwebhooks — تديرونها بأنفسكم:
GET /partner/v1/webhooks |
نقاطكم (بلا السر) |
PATCH /partner/v1/webhooks/{id} |
تغيير العنوان أو الأحداث، أو active: false |
DELETE /partner/v1/webhooks/{id} |
إيقاف — يبقى سجل ما أُرسل |
POST /partner/v1/webhooks/{id}/rotate-secret |
سر جديد فوراً (يظهر مرة واحدة) |
POST /partner/v1/webhooks/{id}/test |
حدث webhook.test للتأكد من العنوان والتوقيع |
GET /partner/v1/webhooks/{id}/deliveries?status=failed |
ما أُرسل وكيف أجبتم (أول 500 حرف من ردّكم) |
POST /partner/v1/webhook-deliveries/{id}/redeliver |
إعادة حدث بعد إصلاح جهتكم — بنفس event_id |
٩. التسوية — ما يخص كل بائع
نسوّي التحصيل معكم أنتم، أسبوعياً، وأنتم تسوّون مع بائعيكم. كل أسبوع يُغلق نرسل settlement.closed، وفيه
links. الأرقام تُحفظ لحظة الإغلاق: لو صُحّح رسم بعدها لا يتغيّر أسبوع أُغلق. Scope مطلوب cod:read.
GET /partner/v1/cod/settlements |
الأسابيع: الفترة، currency، gross_cod، fees، net، items_count، status (closed ← paid) |
GET /partner/v1/cod/settlements/{id} |
أسبوع واحد |
GET /partner/v1/cod/settlements/{id}/items |
سطر لكل طرد (per_page حتى 500، وreference=ORD-5001 لطرود طلب) |
GET /partner/v1/cod/settlements/{id}/sellers |
حصة كل بائع، ثم unattributed لطرودكم أنتم بلا بائع. و?seller_ref=SL-17 يضيف أسطر ذلك البائع |
السطر:
{"tracking_code": "TW-…", "reference": "ORD-5001/P1",
"store_order": {"external_id": "MP-5001", "external_number": "ORD-5001", "package_ref": "P1"},
"seller": {"ref": "SL-17", "name": "أحذية الشام"},
"cod_amount": 45000, "shipping_fee": 3500, "cod_fee": 1125, "net": 40375,
"collected_at": "2026-10-27T15:12:00+03:00"}
- ما يُخصم من التحصيل: رسم الشحن ورسم التحصيل لكل طرد. أما رسم الاستلام من باب البائع فيُفوتَر منفصلاً، ولا يُخصم هنا.
- مجموع الحصص = أرقام الأسبوع بالضبط، أي
sellersمعunattributedمعاً. لا داعي لأن تجمعوا شيئاً، لكن تستطيعون أن تتحققوا. - يدخل الأسبوعَ ما قُبض نقده فيه فقط. الطرد المدفوع مسبقاً أو المرتجع لا تحصيل له، فلا يظهر هنا.
١٠. المرتجع — الزبون يعيد قطعة
حين يطلب الزبون إعادة شيء استلمه:
POST /partner/v1/store-orders/MP-5001/returns
{"ref": "RET-9", "package_ref": "P1", "reason": "المقاس صغير",
"items": [{"name": "حذاء رياضي", "sku": "SH-42", "qty": 1}]}
- بعد التسليم فقط. ما دام الطرد في الطريق ألغوا الطلب (§٧). والطرد الذي تعذّر تسليمه يعود للبائع وحده
(
shipment.returned). - يذهب كابتن إلى باب الزبون ويتصل بالزبون نفسه، ثم يحمل القطعة إلى باب البائع الذي أرسلها.
- أعطوا الزبون
tracking_codeمن الرد: يقرؤه للكابتن عند الباب. أو اطبعوا لهlabel_urlليلصقه. - لا تحصيل على المرتجع. استرداد المبلغ للزبون بينكم وبينه. ورسوم المرتجع (الشحن والاستلام) تصل فاتورتكم.
refرقمكم للمرتجع: إعادة الإرسال بالرقم نفسه تعيد المرتجع نفسه. ومرتجع واحد مفتوح لكل طرد في كل مرة.- المتابعة:
store_order.updatedعند كل خطوة، والمرتجع فيpackages[].returnsبحالته (awaiting_pickup←picked_up← … ←deliveredحين يصل البائع). والطرد الأصلي يبقىdelivered. - الإلغاء:
POST /partner/v1/store-orders/MP-5001/returns/RET-9/cancelما دامت القطعة عند الزبون.
١١. رمز التسليم — منكم أنتم
الإثبات عندكم: تولّدون الرمز، وتوصلونه لزبونكم بقنواتكم، ونحن نفحصه عند الباب.
- أرسلوه مع الطلب:
"delivery_code": "4821"(4–6 أرقام) فيPOST /store-orders، فتأخذه كل طرود الطلب. وفيPOST /expressللطلب السريع.- لا نرسله لأحد، ولا نعرضه للكابتن. التطبيق يقول له «اطلب الرمز» فقط.
- بلا هذا الحقل يبقى كل شيء كما هو.
- عند الباب: يدخل الكابتن ما يقرؤه الزبون، ونفحصه نحن فوراً بلا اتصال بكم. بعد 5 أخطاء يُقفل التسليم.
- ما يصلكم:
delivery.code_failedعند كل رمز خطأ:{kind: express|parcel, code أو tracking_code، external_ref أو reference، attempts_left, captain, at}.delivery.code_lockedعند الحد.- وكل تسليم يقول كيف تمّ:
verification=codeأوoverrideأوnone، وverified_at. فيexpress.status_changedوفيshipment.delivered(مع الكابتن والموقع)، وفيpackages[]منstore_order.updated.
- استبدال الرمز (أعاد الزبون طلبه، أو قُفل):
POST /partner/v1/store-orders/{external_id}/delivery-code {"delivery_code": "8080"}لكل ما لم يُسلَّم من الطلب.- و
POST /partner/v1/express/{code}/delivery-codeللسريع. - يبدأ العدّ من الصفر.
override: سلّمه الموزّع أو الفرع بلا الرمز، بسبب مكتوب. السبب واسم الموظف فيoverride_reasonللسريع. راجعوه عندكم.
١٢. بيئة التجربة
تعطيكم شركة التوصيل مفتاح تجربة على نطاقها. مثال كامل لطلب من بائعين في Postman: مجلد «Stores».
ما ليس بعد (قيد البناء)
- تجميع طرود البائعين في الفرع لتصل الزبون طرداً واحداً بتحصيل واحد.
- الدفع للبائعين مباشرة من التسوية (اليوم تُسوّى مع السوق، وهو يدفع لبائعيه — §٩).