حمل التطبيق الخريطة (تفتح في نافذة جديدة)سجل منشأتك

المطورون

الأحداث

webhooks

كيف نرسل لك الأحداث مجمعة وموقعة، وكيف تتحقق من التوقيع، والترقيم ومنع التكرار، وجدول إعادة المحاولة، وسجل الأحداث للاسترجاع.

قريباواجهة الشركاء لم تفتح بعد. هذه الوثائق لتخطط لربطك من الآن، والعناوين والمفاتيح تصدر عند الإطلاق.سجل كمطور ←

في هذه الصفحة

كيف تعمل

حين يحدث شيء يخصك، مثل طلب جديد أو ربط جديد، نرسل لك طلب 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.

شكل الرسالة

bash
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
json
{
  "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
  1. اقرأ الجسم كما وصل، بايتات خام، قبل تحويله إلى JSON.
  2. ارفض الرسالة إذا كان X-AurMap-Timestamp أقدم من 5 دقائق.
  3. احسب التوقيع وقارنه بمقارنة ثابتة الزمن.
  4. أثناء تدوير السر قد تحمل الترويسة قيمتين: v1=aaa,v1=bbb. اقبل الرسالة إذا طابقت أي واحدة.

مثال للتحقق: السر whsec_example_secret_do_not_use، والوقت 1760443260، والجسم {"delivery_id":"dlv_01","events":[]}، والتوقيع 01b801fe5feb09fd7c745f38de67287ffba48a5e801ad2b058ba4bdd69934b32.

Node (Express)

js
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
<?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 عالجته:

bash
aurmap GET '/v1/events?after_seq=18200&limit=100&types=order.created,order.cancelled'
json
{ "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

json
{
  "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.