跳至主要內容

Mosaqo Webhooks

您訂閱的每個事件,我們都會用 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在 App 中或透過 API 建立了一個碼
qr.published一個碼上線,它的轉址開始生效
qr.updated內容、設計、名稱或狀態有變動
qr.archived一個碼被封存,並從轉址主機上撤下
qr.deleted一個碼被永久刪除
redirect.rule_changed目的地或目標鎖定規則有變動
scan.aggregate_ready掃描彙總資料已彙整完成
export.ready一項非同步匯出已完成
approval.changed一次審核被核准或退回
bulk.completed一項批次工作結束

Payload

當事件與某個 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 只是通知您彙總資料已經備妥,並不包含底層的掃描記錄。如果您的整合確實需要即時取得個別掃描,這可以另外申請——請直接與我們聯絡,不要假設它已經存在。

如果您無法接收 webhook

那就改用輪詢。GET /qr?updatedSince=… 會回傳有變動的碼,GET /scans?since=… 則以穩定的游標往前走過掃描歷史。