للمطوّرين

دليل التكامل لسوق متعدد البائعين

لمطوّري سوقٍ يشحن طلبات بائعيه عبر شركة توصيل تعمل على منصة توصيل. العقد الكامل في 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».

ما ليس بعد (قيد البناء)

  • تجميع طرود البائعين في الفرع لتصل الزبون طرداً واحداً بتحصيل واحد.
  • الدفع للبائعين مباشرة من التسوية (اليوم تُسوّى مع السوق، وهو يدفع لبائعيه — §٩).