واجهة برمجة وصّلني

REST API + Webhooks لدمج خدمات الشحن والتوصيل في تطبيقك أو نظامك.

📍 Base URL: https://wasally.budoorzahera.cloud/api/v1
🔐 Auth: Bearer token
📡 Webhooks: HMAC-SHA256

1. البداية السريعة

للاستخدام تحتاج مفتاح API. الخطوات:

  1. سجّل دخول كـ عميل على /client
  2. اضغط 🔌 API ثم + إنشاء مفتاح جديد
  3. انسخ المفتاح الكامل (يُعرض مرة واحدة فقط)
  4. انتظر موافقة الأدمين — ستصلك رسالة على Telegram عند التفعيل
  5. ابدأ في إرسال الطلبات!
⚠️ مهم: المفتاح الكامل يُعرض مرة واحدة عند الإنشاء. احفظه في متغير بيئة (WASSALNI_API_KEY) — لا تضعه في الكود مباشرةً ولا تشاركه.

2. المصادقة

كل طلب لـ /api/v1/* (عدا /health و/events) يحتاج مفتاح API في الهيدر:

Authorization: Bearer wsl_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

مثال أول طلب

curl https://wasally.budoorzahera.cloud/api/v1/health
const r = await fetch('https://wasally.budoorzahera.cloud/api/v1/health');
console.log(await r.json());
import os, requests
API = "https://wasally.budoorzahera.cloud/api/v1"
HEAD = {"Authorization": f"Bearer {os.environ['WASSALNI_API_KEY']}"}
print(requests.get(f"{API}/health").json())

🧪 Test Mode

كل مفتاح API جديد يبدأ في Test mode فوراً — بدون انتظار موافقة الأدمين. ده يخلّيك تتأكد من التكامل قبل ما تتقدّم لـ Production.

💡 الفرق: في Test mode، طلباتك يتم قبولها فوراً من Test Driver افتراضي ولا تظهر للسائقين الحقيقيين. كل webhooks بتشتغل عادي (order.created, order.accepted, إلخ).

السلوك التلقائي

عند POST /orders في test mode:

  1. الطلب يتعمل بحالة accepted فوراً (مش pending)
  2. delivery_id يُعيَّن للـ Test Driver
  3. is_test: true في الـ response
  4. Webhooks تنطلق بالترتيب: order.createddriver.assignedorder.accepted
  5. الطلب يفضل في accepted لحد ما تنقّله يدوياً

تقديم الطلب يدوياً

POST /api/v1/orders/{id}/test/advance

(test mode فقط) ينقل الطلب للحالة التالية: accepted → picked_up → in_transit → delivered. كل خطوة بتطلق الـ webhook المناسب.

curl -X POST https://wasally.budoorzahera.cloud/api/v1/orders/123/test/advance \
  -H "Authorization: Bearer $WASSALNI_API_KEY"

الردود:

  • 200: الطلب الجديد مع status المحدث
  • 409 terminal_status: الطلب وصل delivered ومش هينقل تاني
  • 409 not_a_test_order: مش طلب test

التقديم التلقائي بـ Timer

لو عايز تختبر فلو كامل بدون تدخل، أضف ?auto_progress=true على POST /orders:

curl -X POST "https://wasally.budoorzahera.cloud/api/v1/orders?auto_progress=true" \
  -H "Authorization: Bearer $WASSALNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{...}'

هتلاقي السيكونس ده تلقائياً:

الوقتالحالةWebhook
+0sacceptedorder.created + driver.assigned + order.accepted
+15spicked_uporder.picked_up
+30sin_transitorder.in_transit
+60sdeliveredorder.delivered + payment.completed

حدود Test Mode

الترقية لـ Production

لما تخلص الاختبار وتبقى جاهز، ارفع الوثائق المطلوبة من dashboard وسيب الأدمين يراجعها. بعد الموافقة، الـ mode يتحول لـ production والحدود اليومية تختفي، والطلبات تروح للسائقين الحقيقيين.

⚠️ مهم: طلبات Test مش بتأثر على الـ stats أو revenue في dashboard الأدمين. هي بياناتك للاختبار فقط.

3. حدود الاستخدام (Rate Limits)

الحدود تُحسب لكل مفتاح، لكل endpoint. ثلاثة planes:

EndpointStarterBusinessEnterprise
POST /orders30/min100/min500/min
GET /orders/*600/min1200/min3000/min
POST /price-estimate20/min50/min200/min
PATCH/cancel10/min30/min100/min
Default60/min200/min1000/min

Burst control

إضافة للحد الدقيقي، يوجد حماية burst (لحظية):

Headers في كل رد

X-RateLimit-Limit:     100
X-RateLimit-Remaining: 87
X-RateLimit-Reset:     1716800000   # unix timestamp

تجاوز الحد

الرد 429 Too Many Requests مع هيدر Retry-After: <seconds>:

HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/json

{"detail": {"error": "rate_limited", "scope": "minute", "retry_after": 23}}
💡 نصيحة: عند الوصول لـ 80% من الحد، يتم تنبيه الأدمين تلقائياً. للاستخدام الكثيف، اطلب ترقية الخطة.

4. Endpoints

POST /api/v1/orders

إنشاء طلب توصيل جديد. يدعم نقاط مصدر متعددة، وجهات متعددة، وفاتورة منتجات (line items) مع طريقة دفع.

💡 التسعير من النظام: لا داعي لإرسال expected_price. النظام يحسب shipping_fee تلقائياً من المسافة + AI. الـ final_price = shipping_fee + product_value.

Request body

{
  "description": "طلب من المتجر",
  "payment_mode": "cod",          // cod | prepaid | none
  "notes": "ملاحظات اختيارية",
  "dropoff_instructions": "اتصل عند الوصول",
  "items": [                       // line items للفاتورة (product_value يُحسب تلقائياً)
    {"name": "Phone case", "qty": 2, "unit_price": 50},
    {"name": "Charger",    "qty": 1, "unit_price": 80}
  ],
  "locations": [
    {"type": "source",      "lat": 30.0444, "lng": 31.2357, "address": "وسط القاهرة", "sequence": 0},
    {"type": "destination", "lat": 30.0626, "lng": 31.2497, "address": "الزمالك",     "sequence": 1}
  ],
  "contacts": [
    {"type": "destination", "name": "محمد", "phone": "01111111111", "sequence": 1}
  ]
}

Response (canonical breakdown)

{
  "id": 123,
  "shipping_fee": 35,              // محسوب من النظام (source of truth)
  "product_value": 180,            // مجموع items
  "commission_amount": 5.25,       // عمولة المنصة من الشحن
  "final_price": 215,              // shipping_fee + product_value
  "payment_mode": "cod",
  "settlement_mode": "B",          // كيف تتم تسوية الكاش
  "items": [...],
  "status": "pending"
}

أمثلة

curl -X POST https://wasally.budoorzahera.cloud/api/v1/orders \
  -H "Authorization: Bearer $WASSALNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "وثائق رسمية",
    "expected_price": 50,
    "locations": [
      {"type":"source","lat":30.0444,"lng":31.2357,"sequence":0},
      {"type":"destination","lat":30.0626,"lng":31.2497,"sequence":1}
    ],
    "contacts": [
      {"type":"destination","name":"محمد","phone":"01111111111","sequence":1}
    ]
  }'
const r = await fetch("https://wasally.budoorzahera.cloud/api/v1/orders", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.WASSALNI_API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    description: "وثائق رسمية",
    expected_price: 50,
    locations: [
      {type:"source",      lat:30.0444, lng:31.2357, sequence:0},
      {type:"destination", lat:30.0626, lng:31.2497, sequence:1}
    ],
    contacts: [
      {type:"destination", name:"محمد", phone:"01111111111", sequence:1}
    ]
  })
});
const order = await r.json();
console.log("Order ID:", order.id);
import os, requests
API = "https://wasally.budoorzahera.cloud/api/v1"
HEAD = {"Authorization": f"Bearer {os.environ['WASSALNI_API_KEY']}",
        "Content-Type": "application/json"}

r = requests.post(f"{API}/orders", headers=HEAD, json={
  "description": "وثائق رسمية",
  "expected_price": 50,
  "locations": [
    {"type":"source",      "lat":30.0444, "lng":31.2357, "sequence":0},
    {"type":"destination", "lat":30.0626, "lng":31.2497, "sequence":1}
  ],
  "contacts": [
    {"type":"destination", "name":"محمد", "phone":"01111111111", "sequence":1}
  ]
})
print("Order ID:", r.json()["id"])

Response (201)

{
  "id": 1234,
  "client_id": 42,
  "status": "pending",
  "description": "وثائق رسمية",
  "expected_price": 50.0,
  "final_price": null,
  "created_at": "2026-05-27T12:00:00",
  "locations": [...],
  "contacts": [...]
}
GET /api/v1/orders?status=&limit=50&offset=0

قائمة الطلبات التي أنشأها هذا المفتاح. يدعم الفلترة بـ status (pending, accepted, in_transit, delivered, cancelled).

curl "https://wasally.budoorzahera.cloud/api/v1/orders?status=delivered" \
  -H "Authorization: Bearer $WASSALNI_API_KEY"
GET /api/v1/orders/{id}

تفاصيل طلب واحد بكامل البيانات (locations، contacts، delivery_man عند التعيين).

GET /api/v1/orders/{id}/tracking

الحالة الحالية + موقع المندوب live على الخريطة.

{
  "order_id": 1234,
  "status": "in_transit",
  "delivery_lat": 30.052,
  "delivery_lng": 31.241,
  "accepted_at": "2026-05-27T12:05:00",
  "picked_up_at": "2026-05-27T12:20:00",
  "delivered_at": null,
  "delivery_photo": null
}
💡 للتتبع المستمر، استخدم Webhook order.location_updated بدلاً من polling.
PATCH /api/v1/orders/{id}

تعديل حقول محددة فقط: notes و dropoff_instructions. لا يمكن تغيير المسار أو السعر بعد الإنشاء.

curl -X PATCH https://wasally.budoorzahera.cloud/api/v1/orders/1234 \
  -H "Authorization: Bearer $WASSALNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes": "تغيير: اتصل قبل الوصول"}'
POST /api/v1/orders/{id}/cancel

إلغاء طلب (فقط إذا كان pending أو accepted). يرجع 409 لو فات الأوان.

curl -X POST https://wasally.budoorzahera.cloud/api/v1/orders/1234/cancel \
  -H "Authorization: Bearer $WASSALNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "إلغاء من العميل"}'
POST /api/v1/price-estimate

اعرف السعر قبل الإنشاء. النتائج مخزنة لـ 60 ثانية لنفس الإحداثيات (تجنب الإسراف).

curl -X POST https://wasally.budoorzahera.cloud/api/v1/price-estimate \
  -H "Authorization: Bearer $WASSALNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src_lat":30.0444,"src_lng":31.2357,"dst_lat":30.0626,"dst_lng":31.2497}'
{
  "suggested_price": 45.5,
  "mode": "simple",     // or "ml"
  "distance_km": 2.93
}

4.8 Account API

هذه الـ endpoints تستخدم Authorization: Bearer <user_jwt> (توكن المستخدم) وليس مفتاح API.

POST /auth/google بدون مصادقة

تسجيل دخول أو إنشاء حساب عبر Google Identity Services. يعيد توكن JWT جاهزاً للاستخدام.

needs_phone: true يعني الحساب جديد ويحتاج رقم هاتف — اتّبع بـ POST /me/complete-phone.

// credential = الـ id_token من google.accounts.id.initialize callback
POST /auth/google
{ "credential": "<google_id_token>" }

→ { "access_token": "...", "role": "client", "needs_phone": false, ... }
Endpointالوصف
GET /auth/google/configيعيد {"enabled": bool, "client_id": "..."} — أظهر زر جوجل فقط إذا enabled=true
POST /auth/google/linkربط حساب جوجل بمستخدم مسجّل (يحتاج Bearer)
POST /auth/google/unlinkفصل جوجل — محجوب إذا لم تكن للحساب كلمة مرور
POST /auth/google-reset-passwordتعيين كلمة مرور جديدة بإثبات هوية جوجل (بدون Bearer)
POST /me/complete-phoneتعيين رقم هاتف حقيقي لحساب needs_phone=true
GET PUT DEL /me/order-draft

يحفظ ويستعيد مسوّدة طلب واحدة لكل مستخدم (autosave). مفيد لاستعادة البيانات لو أغلق المستخدم التطبيق في المنتصف.

// حفظ
PUT /me/order-draft
Authorization: Bearer <token>
{ "payload": { "locations": [...], "desc": "شحنة", "step": 1 } }

// استعادة
GET /me/order-draft
→ { "payload": { ... }, "updated_at": "2026-05-30T..." }

// حذف بعد الإرسال
DELETE /me/order-draft
GET POST DEL /me/addresses

عناوين محفوظة لكل مستخدم لإعادة الاستخدام السريع في نموذج الطلب.

// إضافة عنوان (label اختياري — يُسمَّى تلقائياً من الإحداثيات)
POST /me/addresses
{ "lat": 30.0444, "lng": 31.2357, "label": "البيت", "address": "شارع التحرير" }

// قائمة
GET /me/addresses
→ [{ "id": 1, "label": "البيت", "lat": 30.0444, "lng": 31.2357, "address": "..." }]

// حذف
DELETE /me/addresses/{id}

5. Webhooks

5.1 الإعداد

من لوحة العميل → 🔌 API → اختر مفتاحاً → 🔔 Webhooks → أدخل URL واختر الأحداث.

عند الإنشاء يُعرض secret مرة واحدة — يُستخدم للتحقق من التوقيع.

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

كل طلب webhook يحمل هيدر X-Wassalni-Signature: sha256=<hex>. تحقق دائماً قبل المعالجة:

import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.raw({type: "application/json"}));   // raw body needed!

app.post("/wassalni-webhook", (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const got = req.headers["x-wassalni-signature"];
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
    return res.status(401).send("Invalid signature");
  }
  const event = JSON.parse(req.body);
  console.log(event.event, event.data);
  res.status(200).send("ok");
});
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()

@app.post("/wassalni-webhook")
def webhook():
    body = request.get_data()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    got = request.headers.get("X-Wassalni-Signature", "")
    if not hmac.compare_digest(expected, got):
        abort(401)
    event = request.get_json()
    print(event["event"], event["data"])
    return "ok", 200

5.3 قائمة الأحداث

Eventمتى يُطلق
order.createdعند إنشاء طلب عبر الـ API
order.acceptedقبول العرض من العميل / المندوب
driver.assignedتعيين المندوب على الطلب
order.picked_upالمندوب استلم الشحنة
order.in_transitالشحنة في الطريق
order.deliveredتم التسليم (مع delivery_photo لو متاح)
order.cancelledتم الإلغاء (مع reason)
order.location_updatedتحديث موقع المندوب (تردد عالي)
payment.completedتثبيت السعر النهائي بعد التسليم

5.4 إعادة المحاولة

عند فشل التسليم (status ≠ 2xx)، نعيد المحاولة:

بعد فشل المحاولة الرابعة، يُسجّل التسليم كـ failed ولا يُعاد. راجع /api-keys/{id}/deliveries من dashboard.

5.5 شكل الـ Payload

{
  "event": "order.delivered",
  "data": {
    "order": {
      "id": 1234,
      "status": "delivered",
      "final_price": 55.0,
      "delivery_photo": "/uploads/abc-123.jpg",
      ...
    },
    "delivery": {"id": 8, "name": "محمد المندوب"}
  },
  "delivered_at": "2026-05-27T12:45:00"
}

6. أكواد الخطأ

الكودerrorالمعنى
400field_not_allowedحقل غير مسموح في PATCH
401missing_api_key / invalid_keyالمفتاح ناقص أو خاطئ
403key_not_activeالمفتاح pending أو disabled
404not_foundطلب غير موجود أو لا يخص هذا المفتاح
409cannot_cancel / order_finalizedالحالة لا تسمح بالعملية
429rate_limitedتجاوزت الحد — راجع Retry-After

7. للوكلاء الذكية (For AI Agents) 🤖

هذا القسم يوفّر كل ما يحتاجه LLM agent لاستخدام النظام تلقائياً:

  • Base URL: https://wasally.budoorzahera.cloud/api/v1
  • Authentication: HTTP header Authorization: Bearer <wsl_live_*>
  • Machine-readable spec: /api/v1/openapi.json (OpenAPI 3)
  • Interactive playground: /api/v1/docs (Swagger UI)
  • Event catalog: GET /api/v1/events (no auth needed)

Capabilities (one-paragraph summary for tool descriptions)

Wassalni is a delivery dispatch API serving Egypt. Use it to create delivery
orders (parcel transport, food, documents, or passenger transport) between any
two coordinates, get fair price estimates, and track drivers in real-time.
Orders support multiple pickup/dropoff points, contacts, and optional notes.
Status flow: pending → accepted → picked_up → in_transit → delivered.
Cancellable while pending or accepted. Webhooks notify your system on every
state transition with HMAC-SHA256 signed payloads.

Minimal Python integration for an AI agent

import os, requests

API = "https://wasally.budoorzahera.cloud/api/v1"
HEAD = {"Authorization": f"Bearer {os.environ['WASSALNI_API_KEY']}"}

def estimate_price(src, dst):
    return requests.post(f"{API}/price-estimate", headers=HEAD, json={
        "src_lat": src[0], "src_lng": src[1],
        "dst_lat": dst[0], "dst_lng": dst[1]
    }).json()

def create_order(description, src, dst, price):
    return requests.post(f"{API}/orders", headers=HEAD, json={
        "description": description,
        "expected_price": price,
        "locations": [
            {"type":"source",      "lat":src[0], "lng":src[1], "sequence":0},
            {"type":"destination", "lat":dst[0], "lng":dst[1], "sequence":1}
        ],
        "contacts": []
    }).json()

def track(order_id):
    return requests.get(f"{API}/orders/{order_id}/tracking", headers=HEAD).json()