تخطَّ إلى المحتوى

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=… في سجل المسح إلى الأمام بمؤشر ثابت.