للمطورين
ربط نظام الكاشير بدوري
بدل أن يضغط الكاشير «جاهز» في شاشة دوري، يستدعي برنامجك الواجهة بنفسه: ينشئ رقم الطلب عند إصدار الفاتورة، ويرسل «جاهز» عند انتهاء التحضير، فيرنّ جوال الزبون. وإن أردتَ أن يعرف نظامك بكل ما يحدث في دوري، يستقبل أحداثًا موقّعة على رابط يخصّك (Webhook).
١. الفكرة في ثلاث خطوات
- الفاتورة في نظامك: يرسل برنامجك
POST /api/v1/ordersفيرجع رقم الطلبnumberورابطclaim_url. - الزبون يمسح الرمز: يعرض نظامك (أو شاشة الآيباد) رمز QR لهذا الرابط، فيمسحه الزبون وترتبط الصفحة بجواله وبهذا الرقم.
- الجاهزية: عند الانتهاء يرسل برنامجك
POST /api/v1/orders/{id}/readyفيرنّ جوال الزبون.
لا حاجة لأي تطبيق على جوال الزبون. ولا يتغيّر شيء على بقية المحل: الكاشير ما زال يستطيع العمل من شاشة دوري إذا تعطّل الربط.
٢. كيف أنشئ مفتاح الربط من لوحة التحكم
المفتاح هو «كلمة السر» التي يستخدمها برنامج الكاشير ليثبت أنه مصرَّح له بالتحدث باسم محلك. تنشئه بنفسك دون مبرمج.
- سجّل الدخول إلى لوحة التحكم بحساب صاحب المحل. (الموظفون لا يرون هذا التبويب.)
- افتح تبويب «الربط» من الشريط العلوي.
- في بطاقة «مفاتيح API لنظام الكاشير (POS)» اكتب في خانة الاسم ما يدلّك عليه، مثل «الكاشير الرئيسي».
- اضغط «إنشاء مفتاح».
- يظهر مربع أخضر فيه المفتاح، يبدأ بـ
dwr_. اضغط «نسخ» وضعه في إعدادات برنامج الكاشير. لا يظهر مرة ثانية. دوري تحفظ بصمة مشفّرة فقط ولا تستطيع استرجاعه. إن ضاع، ألغِه وأنشئ غيره. - يمكنك إنشاء حتى 5 مفاتيح (مثلًا مفتاح لكل فرع أو لكل نظام). ويظهر لكل مفتاح اسمه وأول حروفه وآخر استخدام، وزر «إلغاء المفتاح» يوقفه فورًا.
رابط الـ Webhook (اختياري)
- في تبويب «الربط» نفسه، بطاقة «Webhook: إشعار نظامك بالأحداث».
- اكتب في «رابط الاستقبال (https فقط)» عنوان نقطة في نظامك، مثل
https://pos.example.com/dawri-webhook، ثم «حفظ الرابط». - اضغط «إظهار السر» لتنسخ السر
whsec_…الذي تتحقق به من التوقيع. («تدوير السر» يغيّره ويوقف السر القديم.) - اضغط «إرسال اختبار»: يصل حدث
pingإلى نظامك، وتظهر النتيجة في «آخر المحاولات».
لتشغيل «رقم الفاتورة» اليدوي بدون API: تبويب «الكاشير والشاشة» ← «طريقة عمل الكاشير» ← «رقم الكاشير (فاتورة نظام نقاط البيع)».
٣. الأسماء كاملة (انسخها كما هي)
| العنصر | القيمة |
|---|---|
العنوان الأساسي BASE_URL | https://اسم-نطاقك (نفس عنوان موقع دوري الذي تفتحه، بلا شرطة في آخره) |
| ترويسة التوثيق | Authorization: Bearer dwr_XXXXXXXXXXXXXXXXXXXXXXXX |
| نوع المحتوى | Content-Type: application/json |
| متغير مقترح في برنامجك للمفتاح | DAWRI_API_KEY |
| متغير مقترح للعنوان | DAWRI_BASE_URL |
| متغير مقترح لسر الـ Webhook | DAWRI_WEBHOOK_SECRET (قيمته whsec_…) |
| حقول إنشاء طلب | ref · name · show_on_tablet |
| حقول الرد | order · claim_url · claim_expires_at · on_tablet · duplicate |
| أسماء الإجراءات (آخر المسار) | accept · preparing · ready · collected · cancel · reject |
| ترويسات الـ Webhook | X-Dawri-Event · X-Dawri-Signature |
| أحداث الـ Webhook | order.created · order.accepted · order.ready · order.collected · order.cancelled · order.rejected · order.reopened · ping |
٤. التدفق الكامل (مثال يمكنك نسخه)
أ. التأكد من أن المفتاح يعمل:
curl https://YOUR-DOMAIN/api/v1/me \
-H "Authorization: Bearer dwr_XXXX"
# → { "shop": "my-cafe", "name": "قهوة الصباح", "mode": "cafe" }ب. إنشاء الطلب عند إصدار الفاتورة:
curl -X POST https://YOUR-DOMAIN/api/v1/orders \
-H "Authorization: Bearer dwr_XXXX" \
-H "Content-Type: application/json" \
-d '{"ref":"POS-1001","name":"سعد"}'
# 201 →
{
"order": { "id": 57, "number": 24, "status": "preparing", "name": "سعد", "ref": "POS-1001",
"source": "api", "cancel_reason": null, "created_at": 1789000000000,
"ready_at": null, "collected_at": null, "phone_connected": false },
"on_tablet": true,
"claim_url": "https://YOUR-DOMAIN/c/my-cafe?claim=Kq3xT9...",
"claim_expires_at": 1789007200000
}احتفظ بـ order.id (هنا 57) فهو الذي تستخدمه في بقية الاستدعاءات، وأظهر order.number للزبون على الفاتورة. وclaim_url هو ما يمسحه الزبون (انظر القسم ١١).
ج. عند انتهاء التحضير:
curl -X POST https://YOUR-DOMAIN/api/v1/orders/57/ready \
-H "Authorization: Bearer dwr_XXXX"
# 200 → { "order": { "id": 57, "number": 24, "status": "ready", ... } }د. عند تسليم الطلب (اختياري): POST /api/v1/orders/57/collected.
٥. مسارات API
كل المسارات تحت /api/v1 وتتطلب ترويسة Authorization. الطلبات والردود بصيغة JSON.
| المسار | الغرض | الجسم / الرد |
|---|---|---|
GET /api/v1/me | اختبار المفتاح ومعرفة المحل | الرد: {shop, name, mode} |
POST /api/v1/orders | إنشاء طلب جديد برقم جديد | الجسم (كله اختياري):ref نص حتى 60 حرفًا، رقم فاتورتكname حتى 30 حرفًا، اسم الزبونshow_on_tablet false لمنع ظهوره على آيباد الزبونالرد 201: order · claim_url · claim_expires_at · on_tablet |
GET /api/v1/orders | قائمة الطلبات الحالية (المستلمة حديثًا) | اختياري ?status=ready. الرد: {orders:[{id, number, status, name, ref, source}]} |
GET /api/v1/orders/{id} | تفاصيل طلب | الرد: {order} |
POST /api/v1/orders/{id}/accept | قبول طلب معلّق (يصير «قيد التحضير») | الرد: {order} |
POST /api/v1/orders/{id}/preparing | إعادة طلب جاهز إلى «قيد التحضير» | الرد: {order} |
POST /api/v1/orders/{id}/ready | الطلب جاهز: يرنّ جوال الزبون | الرد: {order} |
POST /api/v1/orders/{id}/collected | الزبون استلم طلبه | الرد: {order} |
POST /api/v1/orders/{id}/cancel | إلغاء الطلب | الجسم اختياري: {"reason":"fake"} أو "left" أو "other" |
POST /api/v1/orders/{id}/reject | رفض طلب معلّق | الرد: {order} |
تكرار الإرسال آمن: إذا أرسل نظامك الطلب نفسه مرتين بنفس ref في اليوم نفسه وهو ما زال فعّالًا، يعيد دوري الطلب نفسه مع "duplicate": true ولا يصدر رقمًا جديدًا. استخدم رقم فاتورتك دائمًا في ref حتى لا تتكرر الأرقام عند إعادة المحاولة بعد انقطاع الشبكة.
٦. كائن الطلب (order)
| الحقل | النوع | المعنى |
|---|---|---|
id | رقم | معرّف الطلب داخل دوري. استخدمه في المسارات. |
number | رقم 1–999 | الرقم الذي يراه الزبون ويُنادى به. يبدأ من 1 كل يوم (أو متسلسلًا حسب إعداد المحل). |
status | نص | pending · preparing · ready · collected · cancelled · rejected |
name | نص أو null | اسم الزبون إن أُرسل. |
ref | نص أو null | رقم فاتورتك الذي أرسلته. |
source | نص | api أو manual (من الكاشير) أو qr (مسح الزبون بنفسه). |
cancel_reason | نص أو null | fake · left · other · rejected |
created_at · ready_at · collected_at | رقم أو null | وقت الحدث بالميلي ثانية (Unix epoch). |
phone_connected | true/false | هل صفحة الزبون مفتوحة الآن أو سمح بالإشعارات. تصير true بعد أن يمسح الزبون ويبقي الصفحة مفتوحة، ولا تعني أنه مسح فقط. |
٧. الحالات والانتقالات المسموحة
| من | إلى (الإجراء) |
|---|---|
pending | preparing (accept) · rejected (reject) · cancelled (cancel) |
preparing | ready (ready) · cancelled (cancel) |
ready | collected (collected) · preparing (preparing) · cancelled (cancel) |
collected | ready (ready: لإعادة النداء) |
الطلبات التي تنشئها عبر API تبدأ مباشرة بحالة preparing. وأي انتقال غير مسموح يرجع 409.
٨. الأخطاء
| الرمز | error | السبب والحل |
|---|---|---|
| 401 | invalid_api_key | المفتاح ناقص أو خاطئ أو أُلغي، أو الترويسة ليست بصيغة Bearer dwr_…. |
| 403 | inactive | اشتراك المحل منتهٍ أو موقوف. جدّد الاشتراك من لوحة التحكم. |
| 404 | not_found · unknown_action | الطلب ليس لمحلك، أو اسم الإجراء غير صحيح. |
| 409 | invalid_transition | انتقال غير مسموح (مثلًا «جاهز» لطلب ملغى). يحتوي الرد على الحالة الحالية في status. |
| 429 | rate_limited | تجاوزتَ 120 طلبًا في الدقيقة لهذا المفتاح. انتظر دقيقة. |
شكل الخطأ: {"error":"invalid_transition","status":"collected"}. بعض الحالات تُرجع حقولًا إضافية مثل status.
٩. Webhook: دوري تخبر نظامك
إذا حفظتَ رابط الاستقبال، ترسل دوري POST بصيغة JSON إلى رابطك عند كل حدث. مفيد إذا ضغط الكاشير «جاهز» من شاشة دوري ويريد نظامك أن يعرف، أو لتسجيل الطلبات التي أنشأها الزبائن بأنفسهم.
POST https://pos.example.com/dawri-webhook
Content-Type: application/json
X-Dawri-Event: order.ready
X-Dawri-Signature: t=1789000300,v1=9f2c...e7a1
{
"id": "evt_3fa91c0b7d2e4a11",
"type": "order.ready",
"created": 1789000300,
"shop": "my-cafe",
"data": { "id": 57, "number": 24, "status": "ready", "name": "سعد", "ref": "POS-1001",
"source": "api", "cancel_reason": null, "created_at": 1789000000000,
"ready_at": 1789000300000, "collected_at": null, "phone_connected": true }
}الحدث type | متى يصل |
|---|---|
order.created | أُنشئ طلب من شاشة الكاشير أو بمسح الزبون. (الطلبات التي أنشأها نظامك عبر API لا ترسل هذا الحدث، لأنك تعرفها.) |
order.accepted | انتقل الطلب إلى «قيد التحضير». |
order.ready | الطلب جاهز (رنّ جوال الزبون). |
order.collected | تم تسليم الطلب. |
order.cancelled | أُلغي الطلب. السبب في cancel_reason. |
order.rejected | رُفض طلب معلّق. |
order.reopened | أُعيد طلب جاهز إلى «قيد التحضير». |
ping | زر «إرسال اختبار». الحقل data فيه {"message":"Hello from Dawri"}. |
- ردّ بأي رمز
2xxخلال 8 ثوانٍ. وإلا تعيد دوري المحاولة حتى 3 مرات (بعد ثانية ثم 4 ثوانٍ). - قد يصل الحدث نفسه أكثر من مرة. استخدم
id(مثلevt_…) لتتجاهل المكرر. - الرابط يجب أن يكون
httpsوعلى نطاق عام. العناوين الداخلية (مثل192.168.x.xأوlocalhost) تُرفض لحمايتك.
١٠. التحقق من التوقيع
تأكد دائمًا أن الطلب من دوري قبل أن تصدّقه. الترويسة X-Dawri-Signature بصيغة t=<unix>,v1=<hex>، والتوقيع هو HMAC-SHA256 بالسر whsec_… على النص: t + نقطة + الجسم الخام كما وصل (قبل أي تحويل JSON).
Node.js:
const crypto = require('node:crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // ارفض الأقدم من 5 دقائق
return ok && fresh;
}
// Express: استخدم express.raw({ type: 'application/json' }) لتحصل على rawBody
app.post('/dawri-webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body.toString('utf8'), req.get('X-Dawri-Signature') || '', process.env.DAWRI_WEBHOOK_SECRET))
return res.sendStatus(401);
const evt = JSON.parse(req.body);
// evt.type, evt.data.number, evt.data.ref ...
res.sendStatus(200);
});Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300PHP:
function verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p); // t=...&v1=...
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
return hash_equals($expected, $p['v1']) && abs(time() - (int)$p['t']) < 300;
}
$raw = file_get_contents('php://input');
if (!verify($raw, $_SERVER['HTTP_X_DAWRI_SIGNATURE'] ?? '', getenv('DAWRI_WEBHOOK_SECRET'))) { http_response_code(401); exit; }١١. كيف يظهر رمز QR للزبون
لديك ثلاث طرق، وكلها تستخدم claim_url الذي رجع من إنشاء الطلب:
- على آيباد الزبون (دون أي برمجة إضافية): إذا فعّل المحل «شاشة العميل» بالوضع الافتراضي، يظهر رمز الطلب الجديد عليها تلقائيًا، وفي
on_tabletترىtrue. أرسل"show_on_tablet": falseإن لم تردّه عليها. - صورة جاهزة من دوري: ضع هذا الرابط في
<img>على شاشتك أو اطبعه على الفاتورة (بدون مفتاح):https://YOUR-DOMAIN/api/qr.svg?d=ENCODED_CLAIM_URL // JavaScript const img = BASE_URL + '/api/qr.svg?d=' + encodeURIComponent(claim_url); - مكتبة QR في برنامجك: ولّد الرمز من النص
claim_urlنفسه.
الرابط صالح ساعتين (claim_expires_at) ولجوال واحد فقط. من يصوّره ويرسله لغيره لن يعمل عنده، ولا يتكرر.
١٢. الحدود والملاحظات
- المعدل: 120 طلبًا في الدقيقة لكل مفتاح، وحتى 5 مفاتيح لكل محل.
- الاشتراك: عند انتهاء الاشتراك ترجع كل الاستدعاءات
403 inactiveحتى التجديد. - العزل: المفتاح لا يرى ولا يغيّر إلا طلبات محله.
- الوقت: الأوقات في JSON الطلب بالميلي ثانية، وأما
createdفي الـ Webhook فبالثواني. - الاختبار: الواجهة والـ Webhook والتوقيع مغطّاة باختبارات آلية. لم تُجرَّب مع نظام كاشير تجاري بعينه، لأن لكل نظام طريقة ربط خاصة به. جرّب أولًا على محل تجريبي.