Webhooks في Mosaqo
نرسل طلب POST بجسم JSON موقّع إلى نقطة النهاية لديك عند كل حدث تشترك فيه، ونعيد المحاولة بفواصل متزايدة حتى تقبله.
الاشتراك
أضف نقطة نهاية من Bulk & API، أو سجّلها من شيفرتك الخاصة بمفتاح يحمل النطاق webhooks:write — وهكذا تُشغّل منصات الأتمتة المُطلِقات وتوقفها:
curl -X POST https://api.mosaqo.app/v1/public-api/webhooks \
-H "Authorization: Bearer $MOSAQO_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/mosaqo",
"eventTypes": ["qr.published", "qr.updated"]
}'تحتوي الاستجابة على سر التوقيع secret. وهو يُعرض مرة واحدة فقط — خزّنه، أو استبدل الاشتراك لاحقًا.
الأحداث
| الحدث | يُطلَق عندما |
|---|---|
qr.created | يُنشأ رمز، في التطبيق أو عبر الـ API |
qr.published | ينطلق الرمز وتبدأ إعادة توجيهه بالعمل |
qr.updated | يتغيّر المحتوى أو التصميم أو الاسم أو الحالة |
qr.archived | يُؤرشَف رمز ويُرفع عن مضيف إعادة التوجيه |
qr.deleted | يُحذف رمز نهائيًا |
redirect.rule_changed | تتغيّر وجهة أو قاعدة استهداف |
scan.aggregate_ready | يكتمل تجميع عمليات المسح |
export.ready | ينتهي تصدير غير متزامن |
approval.changed | تُعتمد مراجعة أو تُرفض |
bulk.completed | تنتهي مهمة دفعة |
الحمولة
حين يخص الحدث رمز QR، يحمل الحقل data.qr الملخّص نفسه الذي يُرجعه GET /qr/{id} — فلا يحتاج المُطلِق إلى استدعاء لاحق ليجلب السجل الذي أُبلغ عنه للتو.
{
"id": "0b0f…",
"type": "qr.published",
"workspaceId": "2f60…",
"occurredAt": "2026-08-05T10:20:30.456Z",
"data": {
"qrId": "e7b7…",
"source": "public_api",
"qr": {
"id": "e7b7…",
"name": "Autumn campaign",
"slug": "autumn-campaign-1a2b3c4d",
"mode": "dynamic",
"contentType": "url",
"status": "published",
"approvalState": "not_required",
"folderId": null,
"updatedAt": "2026-08-05T10:20:30.401Z",
"publicUrl": "https://mosaqo.link/aB3dEf"
}
}
}التحقق من التسليم
يحمل كل طلب طابعًا زمنيًا وتوقيعًا يغطي الطابع الزمني والجسم معًا:
X-Mosaqo-Timestamp: 1785942718
X-Mosaqo-Signature: t=1785942718,v1=<hex HMAC-SHA256 of "{timestamp}.{raw body}">
X-Mosaqo-Delivery: 0b0f…
X-Mosaqo-Event: qr.publishedرفض كل ما يبتعد طابعه الزمني كثيرًا عن ساعتك هو ما يجعل تسليمًا مُلتقطًا غير قابل لإعادة التشغيل، فلا تتخطَّ هذا الفحص. قارن التواقيع في زمن ثابت، واستخدم جسم الطلب الخام — فإعادة تسلسل JSON المُحلَّل تغيّر البايتات ولن يتطابق التوقيع.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyMosaqoWebhook(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map((piece) => piece.split('=').map((s) => s.trim())),
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest();
const received = Buffer.from(parts.v1 ?? '', 'hex');
return expected.length === received.length && timingSafeEqual(expected, received);
}PHP
<?php
function verify_mosaqo_webhook(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $piece) {
[$key, $value] = array_map('trim', explode('=', $piece, 2));
$parts[$key] = $value;
}
if (!isset($parts['t'], $parts['v1'])) return false;
if (abs(time() - (int) $parts['t']) > $tolerance) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}Python
import hmac, time
from hashlib import sha256
def verify_mosaqo_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(piece.strip().split("=", 1) for piece in header.split(","))
if "t" not in parts or "v1" not in parts:
return False
if abs(time.time() - int(parts["t"])) > tolerance:
return False
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])إعادة المحاولة والإخفاقات
أجب بأي رمز 2xx فور تخزين الحدث، ثم نفّذ العمل بعد ذلك. ونعيد المحاولة حتى ست مرات بتراجع أسّي، لذا يتحوّل أي معالج بطيء إلى عمليات تسليم مكررة — تعامل مع X-Mosaqo-Delivery كمفتاح تكرار آمن لديك.
يجب أن تكون نقاط النهاية على HTTPS وعلى مضيف عام. أما العناوين المحلية والخاصة فتُرفض عند إنشاء الاشتراك.
توقيع مهجور
يحمل الرأس X-Mosaqo-Signature-V0 توقيعًا أقدم محسوبًا على الجسم وحده. وهو عاجز عن التعبير عن الحماية من إعادة التشغيل، ويبقى إصدارًا واحدًا كي ينتقل المستقبِلون الحاليون، ويجب ألا يُستخدم في أي شيء جديد.
بيانات المسح لا تُدفَع إليك
لا يوجد عمدًا أي webhook لكل عملية مسح. لا ترسل Mosaqo تحليلات على مستوى المسح إلى نقاط نهاية خارجية، ولا يحمل أي حدث في الجدول أعلاه واحدة منها.
سجل عمليات المسح متاح فقط بجلبه بنفسك بمفتاحك عبر GET /scans — وحتى عندئذٍ تحكمه إعدادات خصوصية التحليلات في مساحة عملك: الأبعاد التي أوقفتها تُحذف من كل صف، ويُطبَّق مرشّح الروبوتات، ولا تُكشف أبدًا تجزئة الزائر المستعارة التي تقف خلف «عمليات المسح الفريدة». وتسرد الاستجابة الحقل allowedDimensions كي تعرف ما الذي تحصل عليه.
يخبرك الحدث scan.aggregate_ready بأن التجميع جاهز؛ وهو لا يتضمّن عمليات المسح الأساسية. وإذا كان تكاملك يحتاج فعلًا إلى تسليم عمليات المسح الفردية في الوقت الحقيقي، فذلك متاح عند الطلب — تحدّث إلينا بشأنه بدل افتراض وجوده.
إن تعذّر عليك استقبال webhooks
استطلع بدلًا من ذلك. يعطيك GET /qr?updatedSince=… الرموز المتغيّرة، ويمضي GET /scans?since=… في سجل المسح إلى الأمام بمؤشر ثابت.