דילוג לתוכן

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=… מתקדמת בהיסטוריית הסריקות עם סמן יציב.