LOUAJI EXPRESS API · v1

دليل الربط البرمجي

لربط متجركم أو نظام شركتكم بلواجي إكسبريس: تُنشئون الشحنة من نظامكم، وتتابعون حالتها حتى التسليم، دون إدخال يدوي. الدليل نفسه للتجار وللشركات الشريكة — الفرق في القسم الأخير.

Base: https://api.raseel.expressHeader: X-API-KeyJSON · UTF-8

بداية سريعة

الربط كله أربع خطوات، وأول مرة فقط:

  1. احصل على مفتاح من لوحتك: التكامل ← «مفتاح جديد».
  2. اسحب قائمة الوجهات مرة واحدة واربط كل مدينة عندك بـ destinationId عندنا.
  3. أنشئ الشحنة عند كل طلب، وأرسل معها رقم طلبك في reference.
  4. تابع الحالة عبر الويبهوك (نُخبرك فور التغيّر) أو بسحب التغييرات كل بضع دقائق.

كل الطلبات JSON على العنوان https://api.raseel.express، والمفتاح يُرسَل في ترويسة X-API-Key مع كل طلب.

المفتاح والمصادقة

المفتاح يبدأ بـ pk_live_ ويُنشأ من لوحتك. يظهر مرة واحدة عند إنشائه، فاحفظه في إعدادات نظامك ولا تضعه في كود يصل إليه المتصفح — أي طلب يُرسَل من خادمك لا من صفحة الزبون.

X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • يمكنك إنشاء أكثر من مفتاح (للتجربة وللإنتاج) وإيقاف أيٍّ منها لحظياً من اللوحة.
  • إيقاف المفتاح يوقف الطلبات فوراً ولا يمسّ الشحنات المُنشأة به.
  • الحد الأقصى 60 طلباً في الدقيقة. تجاوزه يردّ 429 — استخدم الدفعات بدل طلب لكل شحنة.
  • إذا تسرّب المفتاح: أوقفه من اللوحة وأنشئ غيره. لا حاجة لمراجعتنا.

الوجهات — تُربط مرة واحدة

لا ترسل اسم مدينة مكتوباً بخط يدك في كل طلب. اسحب القائمة مرة واحدة، واربط كل منطقة عندك بمعرّفها عندنا، ثم أرسل destinationId دائماً. هذا وحده هو الفرق بين ربط ينتهي في ساعة وربط يرفض نصف الطلبات بسبب اختلاف في كتابة الاسم.

GET/api/v1/integration/destinations— قائمة المناطق التي نوصل إليها
{
  "count": 2,
  "destinations": [
    { "id": 42, "name": "جرابلس", "city": "جرابلس", "governorate": "حلب", "type": "CITY_CENTER", "branch": "فرع حلب" },
    { "id": 43, "name": "الأشرفية", "city": "حلب", "governorate": "حلب", "type": "CITY_CENTER", "branch": "فرع حلب" }
  ]
}

governorate هي المحافظة التي تخدم المنطقة، وليست دائماً مدينتها: منبج بلدة قائمة بذاتها ضمن محافظة حلب. استخدمها إن كنت تعرض قائمتنا مجمّعة في نظامك.

إن أرسلت city باسم لا نعرفه، يردّ الخادم 422 ومعه suggestions بأقرب المناطق، لتصحّح الربط دون مراسلتنا.

إنشاء شحنة

POST/api/v1/integration/parcels— الرد 201

الحقول:

الحقلالنوعإلزامي؟الشرح
referenceنصإلزاميرقم الطلب في نظامكم. يمنع التكرار، وبه تستعلمون لاحقاً بدل رقم التتبع.
receiverNameنصإلزامياسم المستلم، حرفان على الأقل.
receiverPhoneنصإلزاميهاتف المستلم، 6 خانات على الأقل.
receiverAddressنصإلزاميالعنوان التفصيلي داخل المدينة.
destinationIdرقمإلزامي أو cityمعرّف المنطقة من قائمة الوجهات. الأدقّ، ولا يتأثر بطريقة كتابة الاسم.
cityنصبديل عن destinationIdاسم المنطقة أو المدينة كما تعرفونه. يُطابَق مع مناطقنا بالاسم ثم بالمدينة.
codAmountرقماختياريالمبلغ المطلوب تحصيله من المستلم. صفر أو تركه فارغاً يعني شحنة بلا تحصيل.
codCurrencySYP · USD · TRYاختياريعملة التحصيل. الافتراضي SYP.
shippingPaidBycustomer · included_in_cod · senderاختياريمن يدفع أجرة التوصيل. الافتراضي included_in_cod. التفاصيل في قسم «من يدفع الأجرة».
weightرقماختياريالوزن بالكيلوغرام.
descriptionنصاختياريوصف المحتوى، مثل «ملابس».
notesنصاختياريملاحظة للسائق أو للفرع، مثل «يُسلَّم بعد العصر».
barcodeنصاختياريباركود ملصقكم إن وُجد — يُمسح كما هو في فروعنا.
senderCompanyنصاختيارياسم المتجر داخل نظامكم. للشركات الشريكة التي تشحن لعدة متاجر.

مثال الطلب:

{
  "reference": "ORD-88213",
  "receiverName": "إبراهيم الناقو",
  "receiverPhone": "0981563850",
  "receiverAddress": "شارع الفرات - بناء 5",
  "destinationId": 42,
  "codAmount": 150000,
  "codCurrency": "SYP",
  "shippingPaidBy": "included_in_cod",
  "weight": 1.5,
  "description": "ملابس",
  "notes": "يُسلَّم بعد العصر"
}

مثال الرد:

{
  "trackingNumber": "RSL-20260923-K91B",
  "reference": "ORD-88213",
  "senderCompany": null,
  "status": "PENDING",
  "subStatus": null,
  "receiver": {
    "name": "إبراهيم الناقو",
    "phone": "0981563850",
    "city": "جرابلس",
    "address": "شارع الفرات - بناء 5"
  },
  "cod": { "amount": "150000", "currency": "SYP" },
  "shipping": { "fee": "470", "currency": "SYP", "paidBy": "included_in_cod" },
  "createdAt": "2026-09-23T08:14:11.000Z",
  "deliveredAt": null,
  "returnedAt": null,
  "destination": { "id": 42, "name": "جرابلس", "branch": "فرع حلب", "matchedBy": "id" }
}
الإرسال المكرر آمن. إذا انقطع الاتصال ولم تعرف هل وصل الطلب، أعد الإرسال بنفس reference. لن تُنشأ شحنة ثانية، وسيعود الرد نفسه ومعه duplicate: true. لهذا السبب reference إلزامي.

الأجرة تُحسب من تسعيرتك المتفق عليها وتعود في shipping.fee بعملة التحصيل. لا ترسل سعراً من عندك — السعر يُقرَّر عندنا حسب عقدك.

دفعة شحنات

حتى 200 شحنة في طلب واحد. النتيجة لكل شحنة على حدة: خطأ في واحدة لا يُسقط الباقي، والرد يقول أيّها فشلت ولماذا.

POST/api/v1/integration/parcels/batch— الرد 207
POST https://api.raseel.express/api/v1/integration/parcels/batch

{ "parcels": [ { ... }, { ... } ] }      // حتى 200 طرد

// الرد 207 — نتيجة لكل طرد على حدة
{
  "accepted": 1,
  "failed": 1,
  "results": [
    { "index": 0, "result": "ok", "trackingNumber": "RSL-…", "reference": "ORD-1" },
    { "index": 1, "result": "failed", "reference": "ORD-2", "error": "هاتف المستلم مطلوب" }
  ]
}

الاستعلام والمتابعة

شحنة واحدة — برقم طلبك أو برقم التتبع:

GET/api/v1/integration/parcels/{reference}

كل ما تغيّر منذ آخر مرة — الطريقة الموصى بها لمزامنة آلاف الشحنات بطلب واحد كل بضع دقائق. احفظ cursor من كل رد وأرسله في since في المرة التالية.

GET/api/v1/integration/parcels-changed?since=…&limit=200— الحد الأقصى 500
GET https://api.raseel.express/api/v1/integration/parcels-changed?since=2026-09-23T08:00:00Z&limit=200

{
  "count": 2,
  "cursor": "2026-09-23T09:41:02.883Z",   // احفظوه وأرسلوه في since المرة القادمة
  "more": false,
  "parcels": [ { "trackingNumber": "…", "status": "DELIVERED", "updatedAt": "…" } ]
}

وللزبون نفسه: صفحة تتبع عامة بلا مفتاح على raseel.express/track — ضَع رابطها في رسالة التأكيد التي ترسلها لزبونك.

التعديل والإلغاء

يمكن التعديل والإلغاء ما دامت الشحنة في حالة PENDING، أي قبل تأكيد استلامها في الفرع. بعد الاستلام تكون البضاعة في عهدتنا، فيتم التعديل عبر الفرع لا عبر الـ API، ويردّ الخادم 403 برسالة تقول ذلك.

PATCH/api/v1/integration/parcels/{reference}— اسم المستلم، الهاتف، العنوان، الوجهة، التحصيل
DELETE/api/v1/integration/parcels/{reference}— إلغاء الشحنة

الحالات

الحالة تصل في status، وحالات الإرجاع تُفصَّل في subStatus.

statusالمعنى
PENDINGأُنشئت الشحنة ولم تصل الفرع بعد.
IN_BRANCHاستُلمت في الفرع.
IN_TRANSITبين الفروع.
OUT_FOR_DELIVERYخرجت مع السائق للتوصيل.
DELIVEREDسُلّمت للمستلم.
DELIVERY_FAILEDتعذّر التسليم — راجعوا failureReason.
RETRY_SCHEDULEDمحاولة تسليم جديدة مجدولة.
RETURNEDقيد الإرجاع — راجعوا subStatus.
CANCELLEDأُلغيت قبل الاستلام.

حالات الإرجاع الفرعية:

subStatusالمعنى
RETURN_REQUESTEDطُلب الإرجاع.
RETURNED_TO_BRANCHعادت إلى الفرع.
RETURNED_TO_SENDERسُلّمت لكم — انتهت رحلة الشحنة.

عند DELIVERY_FAILED يصل معها failureReason، وهي اللحظة التي يُنتظر فيها قرارك: إعادة محاولة أم إرجاع.

من يدفع أجرة التوصيل

هذا الاختيار يقرّر مالاً، ولذلك لا نخمّنه عنك. القيمة الافتراضية included_in_cod.

القيمةالمستلم يدفعماذا يصلك
customerالتحصيل + الأجرةالتحصيل كاملاً؛ الأجرة يدفعها المستلم عند الباب.
included_in_codالتحصيل فقطالتحصيل ناقص الأجرة — الأجرة من داخل المبلغ.
senderالتحصيل فقطالتحصيل كاملاً، والأجرة تُقيَّد على حسابك وتُسوّى في كشفك.

إن كان التحصيل بعملة والأجرة بأخرى، لا تُخصم إحداهما من الأخرى: تُقيَّد الأجرة على حسابك وتظهر في الكشف صراحةً.

الويبهوك — نُخبرك فور تغيّر الحالة

بدل أن تسأل، نُرسل إليك. سجّل رابطاً واحداً من لوحتك، واختر الأحداث. نرسل POSTبصيغة JSON، ومعه توقيع في ترويسة X-Sham-Signature.

الحدثمتى يُرسَل
parcel.status_changedعند كل تغيّر في الحالة حتى التسليم أو الإرجاع.
parcel.createdعند إنشاء الشحنة.
parcel.deliveredعند التسليم فقط.
collection.availableعند إضافة مبلغ التحصيل إلى محفظتك.
*كل الأحداث.

شكل الرسالة:

{
  "id": 1842,
  "event": "parcel.status_changed",
  "timestamp": "2026-09-23T09:41:02.883Z",
  "data": { "trackingNumber": "RSL-…", "reference": "…", "status": "DELIVERED", "previousStatus": "OUT_FOR_DELIVERY", "…": "…" }
}

تحقّق من التوقيع دائماً — هو ما يمنع أي جهة من انتحال إشعاراتنا:

// Node.js — تحققوا من التوقيع قبل الوثوق بأي طلب
import crypto from "crypto";

app.post("/raseel/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = crypto
    .createHmac("sha256", process.env.RASEEL_WEBHOOK_SECRET)
    .update(req.body)              // النص الخام قبل أي تحويل
    .digest("hex");

  if (signature !== req.header("X-Sham-Signature")) return res.sendStatus(401);

  const { id, event, timestamp, data } = JSON.parse(req.body);
  // id: رقم الرسالة، نفسه عند كل إعادة محاولة — تجاهلوا ما عالجتموه سابقاً
  // event: "parcel.status_changed" مثلاً
  // data: الشحنة، وفيها trackingNumber و status (حالتها عند هذا التغيّر)
  res.sendStatus(200);            // ردّوا 200 بسرعة، وعالجوا لاحقاً
});
<?php // PHP
$raw = file_get_contents("php://input");
$expected = hash_hmac("sha256", $raw, getenv("RASEEL_WEBHOOK_SECRET"));
if (!hash_equals($expected, $_SERVER["HTTP_X_SHAM_SIGNATURE"] ?? "")) {
  http_response_code(401); exit;
}
$payload = json_decode($raw, true);
  • التوقيع = HMAC-SHA256 لنص الرسالة الخام بمفتاح السرّ الذي يظهر لك عند تسجيل الرابط.
  • ردّ بـ 200 خلال 10 ثوانٍ ثم عالج البيانات في الخلفية.
  • إن لم يردّ رابطك بـ 2xx نعيد المحاولة بعد دقيقة ثم 5 و15 دقيقة ثم ساعة و3 و6 و12 ساعة. رسائل الشحنة الواحدة تصل بترتيبها، وtimestamp وقت التغيّر نفسه.
  • سجل كل رسالة وردّ رابطك عليها في لوحتك، صفحة «تكامل API».
  • الويبهوك تنبيه وليس مصدر الحقيقة: إن تعطّل رابطك، أعِد المزامنة بـ parcels-changed.

الأخطاء

كل خطأ يعود بصيغة واحدة، ورسالته بالعربية جاهزة للعرض على موظفك:

{ "statusCode": 422, "message": "لا نوصل إلى «كفرنبل» أو أن الاسم غير مطابق. استخدم destinationId من GET /destinations", "code": "UNKNOWN_DESTINATION", "suggestions": [ … ] }
الرمزالمعنىما العمل
401المفتاح مفقود أو موقوفأرسلوا X-API-Key، وتأكدوا أن المفتاح فعّال في لوحتكم.
400حقل ناقص أو غير صالحالرد يحوي رسالة لكل حقل. صحّحوا وأعيدوا الإرسال.
422وجهة غير معروفة — UNKNOWN_DESTINATIONالرد يحوي suggestions بأقرب المناطق. استخدموا destinationId.
404لا توجد شحنة بهذا الرقم على حسابكمتأكدوا من reference أو رقم التتبع.
403الشحنة تجاوزت المرحلة التي يُسمح فيها بالتعديلبعد تأكيد الاستلام في الفرع لا تُعدَّل ولا تُلغى من الـ API — تواصلوا مع الفرع.
429تجاوزتم 60 طلباً في الدقيقةأعيدوا المحاولة بعد قليل، أو استخدموا الدفعات بدل طلب لكل شحنة.

أمثلة جاهزة

curl

curl -X POST https://api.raseel.express/api/v1/integration/parcels \
  -H "X-API-Key: pk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "ORD-88213",
    "receiverName": "إبراهيم الناقو",
    "receiverPhone": "0981563850",
    "receiverAddress": "شارع الفرات - بناء 5",
    "destinationId": 42,
    "codAmount": 150000,
    "codCurrency": "SYP"
  }'

Node.js

const res = await fetch("https://api.raseel.express/api/v1/integration/parcels", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.RASEEL_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    reference: order.id,
    receiverName: order.customer.name,
    receiverPhone: order.customer.phone,
    receiverAddress: order.customer.address,
    destinationId: order.zoneId,      // من قائمة الوجهات
    codAmount: order.total,
    codCurrency: "SYP",
  }),
});

const parcel = await res.json();
if (!res.ok) throw new Error(JSON.stringify(parcel));
// احفظوا رقم التتبع بجانب الطلب عندكم
await orders.update(order.id, { trackingNumber: parcel.trackingNumber });

PHP

<?php
$ch = curl_init("https://api.raseel.express/api/v1/integration/parcels");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "X-API-Key: " . getenv("RASEEL_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "reference"       => $order["id"],
    "receiverName"    => $order["name"],
    "receiverPhone"   => $order["phone"],
    "receiverAddress" => $order["address"],
    "destinationId"   => $order["zone_id"],
    "codAmount"       => $order["total"],
    "codCurrency"     => "SYP",
  ], JSON_UNESCAPED_UNICODE),
]);
$parcel = json_decode(curl_exec($ch), true);

Python

import os, requests

r = requests.post(
    "https://api.raseel.express/api/v1/integration/parcels",
    headers={"X-API-Key": os.environ["RASEEL_KEY"]},
    json={
        "reference": order["id"],
        "receiverName": order["name"],
        "receiverPhone": order["phone"],
        "receiverAddress": order["address"],
        "destinationId": order["zone_id"],
        "codAmount": order["total"],
        "codCurrency": "SYP",
    },
    timeout=20,
)
r.raise_for_status()
parcel = r.json()

الشركات الشريكة

الشركة الشريكة تستخدم نفس واجهة الشحنات أعلاه بنفس المفتاح، ولها فوق ذلك لوحة خاصة على raseel.express/partner.

انتبهوا للفرق: مسارات /api/v1/merchant/integration/… تعمل بـمفتاح الربط، أمّا مسارات /api/v1/partner/… فهي مسارات لوحتكم وتحتاج تسجيل دخول لا مفتاحاً — المفتاح عليها يُرجِع 401.

بمفتاح الربط:

GET/api/v1/merchant/integration/quote?destinationId=42&codCurrency=SYP— الأجرة قبل إنشاء الشحنة
GET/api/v1/merchant/integration/destinations— الوجهات ومعرّفاتها
GET/api/v1/merchant/integration/parcels-changed?since=— ما تغيّر من شحناتكم

بتسجيل دخول لوحة الشريك:

GET/api/v1/partner/parcels?status=&from=&to=— شحنات الشركة
GET/api/v1/partner/statement— كشف الحساب: لكم وعليكم والصافي
GET/api/v1/partner/wallet— الأرصدة والمتاح للسحب وسجل السحوبات
POST/api/v1/partner/withdrawals— طلب سحب من الرصيد المتاح
PATCH/api/v1/partner/parcels/{key}/failure-decision— بعد تعذّر التسليم: إعادة محاولة أو إرجاع
  • أرسلوا senderCompany مع كل شحنة باسم المتجر داخل نظامكم. تظهر في تقاريركم لأداء كل متجر، وتُطبع على البوليصة كمرسل.
  • التحصيل الذي نجمعه لكم دَين علينا يظهر في الكشف، ويُسحب من اللوحة أو عبر الواجهة أعلاه.
  • التسعيرة تُتفَق معكم وتُسجَّل على حسابكم؛ لا تُرسَل أسعار مع الشحنة.

قائمة تحقق قبل الإطلاق

  • المفتاح محفوظ في إعدادات الخادم، لا في كود الواجهة ولا في مستودع الكود.
  • كل مدينة عندكم مربوطة بـ destinationId، ولا تُرسل أسماء نصية.
  • reference هو رقم الطلب الحقيقي، وفريد، ويُعاد إرساله عند إعادة المحاولة.
  • رقم التتبع يُحفظ بجانب الطلب عندكم فور إنشاء الشحنة.
  • الويبهوك مسجَّل، والتوقيع يُتحقَّق منه، ومزامنة احتياطية بـ parcels-changed كل بضع دقائق.
  • shippingPaidBy مضبوط على ما اتفقتم عليه، ومُجرَّب على شحنة واحدة قبل الإطلاق.
  • جُرِّبت حالة الفشل: هاتف ناقص، ووجهة غير معروفة، ومفتاح موقوف.

تحتاج مساعدة في الربط؟

راسلنا من صفحة التواصل ومعك رقم الطلب الذي جرّبته ونصّ الخطأ الذي وصلك — هذان السطران يختصران المحادثة كلها.