واجهة برمجة وصّلني
REST API + Webhooks لدمج خدمات الشحن والتوصيل في تطبيقك أو نظامك.
https://wasally.budoorzahera.cloud/api/v11. البداية السريعة
للاستخدام تحتاج مفتاح API. الخطوات:
- سجّل دخول كـ عميل على /client
- اضغط 🔌 API ثم + إنشاء مفتاح جديد
- انسخ المفتاح الكامل (يُعرض مرة واحدة فقط)
- انتظر موافقة الأدمين — ستصلك رسالة على Telegram عند التفعيل
- ابدأ في إرسال الطلبات!
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.
order.created, order.accepted, إلخ).
السلوك التلقائي
عند POST /orders في test mode:
- الطلب يتعمل بحالة
acceptedفوراً (مش pending) delivery_idيُعيَّن للـ Test Driveris_test: trueفي الـ response- Webhooks تنطلق بالترتيب:
order.created→driver.assigned→order.accepted - الطلب يفضل في
acceptedلحد ما تنقّله يدوياً
تقديم الطلب يدوياً
/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 |
|---|---|---|
| +0s | accepted | order.created + driver.assigned + order.accepted |
| +15s | picked_up | order.picked_up |
| +30s | in_transit | order.in_transit |
| +60s | delivered | order.delivered + payment.completed |
حدود Test Mode
- 100 طلب test في اليوم كحد أقصى (الأدمين يقدر يعدله)
- POST /orders مقيد بـ 10/دقيقة (بدل 30 في Starter العادي)
- عند تجاوز الحد اليومي:
429 test_daily_cap_exceeded
الترقية لـ Production
لما تخلص الاختبار وتبقى جاهز، ارفع الوثائق المطلوبة من dashboard وسيب الأدمين يراجعها. بعد الموافقة، الـ mode يتحول لـ production والحدود اليومية تختفي، والطلبات تروح للسائقين الحقيقيين.
3. حدود الاستخدام (Rate Limits)
الحدود تُحسب لكل مفتاح، لكل endpoint. ثلاثة planes:
| Endpoint | Starter | Business | Enterprise |
|---|---|---|---|
POST /orders | 30/min | 100/min | 500/min |
GET /orders/* | 600/min | 1200/min | 3000/min |
POST /price-estimate | 20/min | 50/min | 200/min |
PATCH/cancel | 10/min | 30/min | 100/min |
| Default | 60/min | 200/min | 1000/min |
Burst control
إضافة للحد الدقيقي، يوجد حماية burst (لحظية):
- Starter: 10 req/sec (burst 50)
- Business: 20 req/sec (burst 100)
- Enterprise: 50 req/sec (burst 250)
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}}
4. Endpoints
/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": [...]
}
/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"
/api/v1/orders/{id}
تفاصيل طلب واحد بكامل البيانات (locations، contacts، delivery_man عند التعيين).
/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
}
order.location_updated بدلاً من polling.
/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": "تغيير: اتصل قبل الوصول"}'
/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": "إلغاء من العميل"}'
/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.
/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 |
/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
/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)، نعيد المحاولة:
- المحاولة 1: فوراً
- المحاولة 2: بعد 30 ثانية
- المحاولة 3: بعد 5 دقائق
- المحاولة 4: بعد 30 دقيقة
بعد فشل المحاولة الرابعة، يُسجّل التسليم كـ 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 | المعنى |
|---|---|---|
| 400 | field_not_allowed | حقل غير مسموح في PATCH |
| 401 | missing_api_key / invalid_key | المفتاح ناقص أو خاطئ |
| 403 | key_not_active | المفتاح pending أو disabled |
| 404 | not_found | طلب غير موجود أو لا يخص هذا المفتاح |
| 409 | cannot_cancel / order_finalized | الحالة لا تسمح بالعملية |
| 429 | rate_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()
8. الروابط
- Swagger UI تفاعلي: /api/v1/docs
- OpenAPI JSON: /api/v1/openapi.json
- Event catalog: /api/v1/events
- Health check: /api/v1/health
- لوحة العميل: /client