كيف تعمل
حين يحدث شيء يخصك، مثل طلب جديد أو ربط جديد، نرسل لك طلب POST على عنوان webhook الذي سجلته في لوحة المطورين.
- نرسل فور حدوث الحدث.
- إذا تراكمت أحداث، نجمع حتى 100 حدث في رسالة واحدة.
- كل رسالة موقعة بسر الـ webhook
whsec_xxx. - تستطيع تسجيل حتى 5 عناوين لكل بيئة، ولكل عنوان أنواع الأحداث التي يريدها.
- كل عنوان يستلم نسخته من كل حدث، بـ
delivery_idخاص به وإعادة محاولة خاصة به. تعطل عنوان لا يؤخر غيره.
قواعد العنوان
httpsفقط، على المنفذ 443، وعلى عنوان عام في الإنترنت.- نرفض العناوين الخاصة (
10.xو172.16.xإلى172.31.xو192.168.x)، والمحلية (127.0.0.1و::1)، وعناوين الربط المحلي (169.254.xوfe80::)، وعناوين IP المكتوبة مباشرة. - نتحقق عند التسجيل، ثم عند كل إرسال بعد حل الاسم (DNS).
- لا نتبع التحويلات (redirects).
- العنوان المرفوض عند التسجيل يرد
400 URL_NOT_ALLOWED.
شكل الرسالة
POST /aurmap/webhooks HTTP/1.1
Host: pos.example.com
Content-Type: application/json
X-AurMap-Delivery-Id: dlv_01JA9Y3K
X-AurMap-Timestamp: 1760445006
X-AurMap-Signature: v1=5f2c…e81a{
"delivery_id": "dlv_01JA9Y3K",
"mode": "live",
"sent_at": "2026-10-14T12:30:06Z",
"events": [
{
"event_id": "evt_01JA9Y3J8Q",
"type": "order.created",
"seq": 18233,
"occurred_at": "2026-10-14T12:30:05Z",
"link_id": "lnk_2h6j4n",
"place_id": "plc_9q3w7e",
"entity": { "kind": "order", "id": "ord_7m1x9c", "version": 1 },
"data": { "order": { "order_id": "ord_7m1x9c", "status": "received", "…": "…" } }
}
]
}| الحقل | الوصف |
|---|---|
event_id |
معرف فريد للحدث. استعمله لمنع التكرار |
type |
نوع الحدث |
seq |
رقم تسلسلي واحد لحسابك وبيئتك، مشترك بين كل عناوينك |
entity.version |
نسخة الكائن. يزيد مع كل تغيير عليه |
data |
الحالة الكاملة الحالية للكائن، لا الفرق فقط |
التحقق من التوقيع
التوقيع HMAC-SHA256 بسر الـ webhook للنص:
TIMESTAMP + "." + raw_body- اقرأ الجسم كما وصل، بايتات خام، قبل تحويله إلى JSON.
- ارفض الرسالة إذا كان
X-AurMap-Timestampأقدم من 5 دقائق. - احسب التوقيع وقارنه بمقارنة ثابتة الزمن.
- أثناء تدوير السر قد تحمل الترويسة قيمتين:
v1=aaa,v1=bbb. اقبل الرسالة إذا طابقت أي واحدة.
مثال للتحقق: السر whsec_example_secret_do_not_use، والوقت 1760443260، والجسم {"delivery_id":"dlv_01","events":[]}، والتوقيع 01b801fe5feb09fd7c745f38de67287ffba48a5e801ad2b058ba4bdd69934b32.
Node (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.AURMAP_WEBHOOK_SECRET;
function verified(req) {
const ts = req.get('X-AurMap-Timestamp') ?? '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto.createHmac('sha256', SECRET).update(ts + '.').update(req.body).digest();
return (req.get('X-AurMap-Signature') ?? '')
.split(',')
.map((p) => p.trim().replace(/^v1=/, ''))
.some((sig) => /^[0-9a-f]{64}$/.test(sig) && crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected));
}
app.post('/aurmap/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verified(req)) return res.status(401).end();
const { events } = JSON.parse(req.body.toString('utf8'));
for (const event of events) {
if (await alreadySeen(event.event_id)) continue; // منع التكرار
await queue.add(event); // عالج لاحقا، ورد بسرعة
}
res.status(200).end();
});PHP
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_AURMAP_TIMESTAMP'] ?? '';
$hdr = $_SERVER['HTTP_X_AURMAP_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $ts . '.' . $raw, getenv('AURMAP_WEBHOOK_SECRET'));
$ok = false;
foreach (explode(',', $hdr) as $part) {
$sig = preg_replace('/^v1=/', '', trim($part));
if (hash_equals($expected, $sig)) {
$ok = true;
}
}
if (!$ok) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
foreach ($payload['events'] as $event) {
if (already_seen($event['event_id'])) {
continue;
}
enqueue($event);
}
http_response_code(200);الترتيب ومنع التكرار
- قد يصلك الحدث أكثر من مرة. احفظ
event_idوتجاهل ما رأيته. - داخل الرسالة الواحدة الأحداث مرتبة تصاعديا حسب
seq. لكن رسالة أعيدت قد تصل بعد رسالة أحدث. seqلا فجوات فيه في سجل الأحداث. لكن العنوان الذي يستلم أنواعا محددة فقط يرى فجوات، وهذا طبيعي ولا يعني ضياع حدث.- لذلك قارن
entity.versionبما عندك. إذا كانت النسخة ليست أحدث، تجاهل الحدث. - كل حدث يحمل الحالة الكاملة للكائن، فآخر حدث يكفي وحده.
الرد وإعادة المحاولة
- رد بـ
2xxخلال 10 ثوان. هذا يعني أن الرسالة كلها وصلت. عالج الأحداث بعد الرد، لا قبله. - أي رد آخر، أو انتهاء الوقت، يعني إعادة الرسالة كلها.
| المحاولة | بعد |
|---|---|
| 2 | 30 ثانية |
| 3 | دقيقتين |
| 4 | 10 دقائق |
| 5 | 30 دقيقة |
| 6 | ساعة |
| 7 | ساعتين |
| 8 وما بعدها | كل 4 ساعات |
بعد 24 ساعة نتوقف عن الإرسال، ويبقى الحدث في سجل الأحداث. إذا فشل العنوان 72 ساعة متصلة نوقفه ونراسلك، وتعيد تفعيله من اللوحة.
مهلة قبول الطلب لا تنتظر إعادة المحاولة. إذا تعطل خادمك، اسأل عن الطلبات المنتظرة عند عودتك.
سجل الأحداث
إذا فاتك شيء، اقرأ الأحداث بعد آخر seq عالجته:
aurmap GET '/v1/events?after_seq=18200&limit=100&types=order.created,order.cancelled'{ "data": [ { "event_id": "evt_01JA9Y3J8Q", "type": "order.created", "seq": 18233, "…": "…" } ], "next_after_seq": 18300, "has_more": true }نحفظ الأحداث 30 يوما كما أرسلت، ثم نحذفها. لذلك يبقى رقم العميل فيها مخفيا، إلا إذا كان الربط يحمل customer:contact. وفي أحداث الطلبات القاعدة نفسها: phone_masked دائما، وphone_e164 فقط مع هذه الصلاحية.
بعد 7 أيام نخفي بيانات العميل داخل الأحداث المحفوظة، حتى مع customer:contact. احفظ ما تحتاجه من الطلب عند وصوله.
أنواع الأحداث
| النوع | متى |
|---|---|
link.created |
وافق التاجر على رمز ربط |
link.updated |
تغيرت الصلاحيات، أو انتقل مصدر القائمة |
link.removed |
ألغي الربط. فيه removed_by |
place.updated |
تغير الاسم أو الموقع أو أوقات الدوام |
place.ordering_changed |
إيقاف مؤقت، أو تغير مهلة القبول أو طرق الدفع |
menu.changed |
ظهرت نسخة جديدة من القائمة للعملاء: نشر كامل أو تعديل بنية أو تحديث حالة، منك أو من التاجر. فيه version وprev_version وkind وhash |
menu.removed |
أزيلت القائمة من المكان. فيه reason |
order.created |
طلب جديد |
order.status_changed |
تغيرت حالة الطلب، ومنها تغييراتك أنت |
order.cancelled |
ألغي الطلب. السبب في cancel.reason، وعدم حضور العميل في cancel.detail بقيمة customer_no_show |
order.payment_updated |
استرجع المبلغ كاملا (refunded) أو جزئيا (partially_refunded) مع refunded_minor، أو سجل الدفع النقدي |
endpoint.test |
زر «إرسال تجربة» في اللوحة |
catalog.products.processed |
انتهت معالجة دفعة منتجات قريبا |
offer.expired |
انتهى عرض قريبا |
accounting.day_closed |
أغلق يوم محاسبي قريبا |
payout.created، payout.paid |
تحويل للتاجر قريبا |
قد نضيف أنواعا جديدة. تجاهل أي نوع لا تعرفه، ورد عليه بـ 200.
مثال: menu.changed
{
"event_id": "evt_01JA7Z0STAT",
"type": "menu.changed",
"seq": 18240,
"occurred_at": "2026-10-14T18:00:00Z",
"link_id": "lnk_2h6j4n",
"place_id": "plc_9q3w7e",
"entity": { "kind": "menu", "id": "plc_9q3w7e", "version": 43 },
"data": { "place_id": "plc_9q3w7e", "prev_version": 42, "version": 43, "kind": "state", "hash": "a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1" }
}kind:fullأوstructureأوstate.hashبصمة القائمة بعد التغيير. احفظها معversionلتستعملها في المطابقة الليلية.- إذا كان
prev_versionلا يساوي آخر نسخة عندك، فقد فاتك تغيير. اقرأ القائمة بـGET /places/{place_id}/menu. menu.removedيحملplace_idوversionوreason:partnerأوlink_removedأوsource_replacedأوaurmap.