ข้ามไปยังเนื้อหา

Mosaqo webhooks

เรา POST เนื้อหา JSON ที่ลงลายเซ็นไปยัง endpoint ของคุณสำหรับทุกอีเวนต์ที่คุณสมัครรับ และส่งซ้ำโดยเว้นระยะถี่ห่างขึ้นเรื่อย ๆ จนกว่าคุณจะตอบรับ

การสมัครรับอีเวนต์

เพิ่ม endpoint ใน Bulk & API หรือลงทะเบียนจากโค้ดของคุณเองด้วย key ที่มีสโคป 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"]
  }'

response จะมี secret สำหรับลงลายเซ็น ระบบแสดงให้เห็นเพียงครั้งเดียว — เก็บไว้ หรือสร้างการสมัครรับใหม่แทนภายหลัง

อีเวนต์

อีเวนต์ทำงานเมื่อ
qr.createdมีการสร้างโค้ด ทั้งในแอปหรือผ่าน API
qr.publishedโค้ดขึ้นใช้งานจริงและการเปลี่ยนเส้นทางเริ่มทำงาน
qr.updatedเนื้อหา ดีไซน์ ชื่อ หรือสถานะเปลี่ยนแปลง
qr.archivedโค้ดถูกเก็บเข้าคลังและถอดออกจากโฮสต์เปลี่ยนเส้นทาง
qr.deletedโค้ดถูกลบอย่างถาวร
redirect.rule_changedปลายทางหรือกฎการกำหนดเป้าหมายเปลี่ยนแปลง
scan.aggregate_readyข้อมูลสแกนแบบรวมประมวลผลเสร็จแล้ว
export.readyการส่งออกแบบอะซิงโครนัสเสร็จสิ้น
approval.changedการตรวจทานได้รับอนุมัติหรือถูกปฏิเสธ
bulk.completedงานแบบ bulk ทำงานเสร็จ

Payload

เมื่ออีเวนต์เกี่ยวข้องกับ QR code ค่า 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"
    }
  }
}

การตรวจสอบข้อมูลที่ส่งมา

ทุกคำขอจะมี timestamp และลายเซ็นที่คำนวณจากทั้ง timestamp และเนื้อหา:

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

การปฏิเสธคำขอที่ timestamp ห่างจากนาฬิกาของคุณมากคือสิ่งที่ทำให้ข้อมูลที่ถูกดักจับไปนำมาส่งซ้ำไม่ได้ จึงอย่าข้ามการตรวจสอบนี้ เปรียบเทียบลายเซ็นด้วยเวลาคงที่ และใช้เนื้อหาคำขอแบบ raw — การนำ JSON ที่ parse แล้วมาแปลงกลับเป็นสตริงจะทำให้ไบต์เปลี่ยนไป และลายเซ็นจะไม่ตรงกัน

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 ใดก็ได้ทันทีที่บันทึกอีเวนต์เสร็จ แล้วค่อยไปประมวลผลต่อ เราส่งซ้ำได้สูงสุดหกครั้งแบบ exponential backoff ตัวจัดการที่ทำงานช้าจึงกลายเป็นการส่งซ้ำซ้อน — ให้ใช้ X-Mosaqo-Delivery เป็น idempotency key ฝั่งคุณ

endpoint ต้องเป็น HTTPS บนโฮสต์สาธารณะ ที่อยู่ภายในเครื่องและที่อยู่แบบส่วนตัวจะถูกปฏิเสธตั้งแต่ตอนสร้างการสมัครรับ

ลายเซ็นที่เลิกใช้แล้ว

X-Mosaqo-Signature-V0 เป็นลายเซ็นรุ่นเก่าที่คำนวณจากเนื้อหาเพียงอย่างเดียว จึงไม่สามารถป้องกันการส่งซ้ำได้ ยังคงส่งมาอีกหนึ่งรีลีสเพื่อให้ผู้รับเดิมย้ายระบบได้ทัน และต้องไม่นำไปใช้กับสิ่งใหม่ใด ๆ

ข้อมูลการสแกนไม่ถูกส่งออกไป

เราตั้งใจไม่ให้มี webhook รายการสแกนแต่ละครั้ง Mosaqo ไม่ส่ง analytics ระดับการสแกนไปยัง endpoint ของบุคคลที่สาม และไม่มีอีเวนต์ใดในตารางด้านบนที่พาข้อมูลนั้นไปด้วย

ประวัติการสแกนดูได้ด้วยการดึงเองด้วย key ของคุณเท่านั้น — GET /scans — และถึงอย่างนั้นก็ยังอยู่ภายใต้การตั้งค่าความเป็นส่วนตัวของ analytics ใน workspace คุณ: มิติที่คุณปิดไว้จะถูกตัดออกจากทุกแถว ตัวกรองบอททำงานเสมอ และค่า hash นิรนามของผู้เข้าชมที่อยู่เบื้องหลัง “การสแกนที่ไม่ซ้ำ” จะไม่ถูกเปิดเผยเลย response จะแสดงรายการ allowedDimensions ไว้ให้ทราบว่าคุณจะได้ข้อมูลอะไรบ้าง

scan.aggregate_ready บอกเพียงว่าข้อมูลแบบรวมพร้อมแล้ว ไม่ได้มีรายการสแกนที่เป็นต้นทางอยู่ในนั้น หากการเชื่อมต่อของคุณจำเป็นต้องได้รับรายการสแกนแบบเรียลไทม์จริง ๆ เรามีให้ตามคำขอ — ติดต่อเราเพื่อพูดคุย แทนการสันนิษฐานว่ามีอยู่แล้ว

หากคุณรับ webhook ไม่ได้

ให้ใช้การ poll แทน GET /qr?updatedSince=… จะให้โค้ดที่มีการเปลี่ยนแปลง ส่วน GET /scans?since=… จะไล่ประวัติการสแกนไปข้างหน้าด้วย cursor ที่คงที่