Webhook của Mosaqo
Chúng tôi gửi POST kèm thân JSON đã ký tới endpoint của bạn cho mỗi sự kiện bạn đăng ký, và thử lại với khoảng cách tăng dần cho đến khi bạn chấp nhận.
Đăng ký
Bạn có thể thêm endpoint trong mục Bulk & API, hoặc đăng ký từ chính mã của mình bằng khóa có phạm vi webhooks:write — đây chính là cách một nền tảng tự động hóa bật và tắt một trigger:
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"]
}'Phản hồi chứa secret dùng để ký. Nó chỉ hiện một lần — hãy lưu lại, hoặc thay đăng ký sau này.
Sự kiện
| Sự kiện | Kích hoạt khi |
|---|---|
qr.created | Một mã được tạo, trong ứng dụng hoặc qua API |
qr.published | Một mã lên sóng và chuyển hướng của nó bắt đầu hoạt động |
qr.updated | Nội dung, thiết kế, tên hoặc trạng thái thay đổi |
qr.archived | Một mã được lưu trữ và gỡ khỏi máy chủ chuyển hướng |
qr.deleted | Một mã bị xóa vĩnh viễn |
redirect.rule_changed | Đích đến hoặc quy tắc nhắm mục tiêu thay đổi |
scan.aggregate_ready | Một bản tổng hợp lượt quét đã sẵn sàng |
export.ready | Một lần xuất bất đồng bộ đã xong |
approval.changed | Một lượt duyệt được chấp thuận hoặc từ chối |
bulk.completed | Một tác vụ hàng loạt kết thúc |
Payload
Khi một sự kiện liên quan đến mã QR, data.qr mang chính bản tóm tắt mà GET /qr/{id} trả về — nên trigger không phải gọi ngược lại để lấy bản ghi vừa được báo.
{
"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ác minh một lần gửi
Mỗi yêu cầu mang một dấu thời gian và một chữ ký trên cả dấu thời gian lẫn phần thân:
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.publishedTừ chối mọi thứ có dấu thời gian cách xa đồng hồ của bạn chính là điều khiến một lần gửi bị chặn bắt không thể phát lại — đừng bỏ qua bước kiểm tra đó. Hãy so sánh chữ ký trong thời gian hằng số và dùng phần thân thô của yêu cầu: tuần tự hóa lại JSON đã phân tích sẽ đổi các byte, và chữ ký sẽ không còn khớp.
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"])Thử lại và lỗi
Hãy trả về bất kỳ mã 2xx nào ngay khi bạn đã lưu sự kiện; việc xử lý làm sau. Chúng tôi thử lại tối đa sáu lần với khoảng cách tăng theo cấp số nhân, nên một trình xử lý chậm sẽ biến thành các lần gửi trùng — hãy coi X-Mosaqo-Delivery là khóa idempotency ở phía bạn.
Endpoint phải là HTTPS trên một máy chủ công khai. Địa chỉ nội bộ và riêng tư bị từ chối ngay khi tạo đăng ký.
Chữ ký đã ngừng dùng
X-Mosaqo-Signature-V0 mang chữ ký cũ chỉ tính trên phần thân. Nó không thể diễn đạt việc chống phát lại, còn đi kèm thêm một bản phát hành để các bên nhận hiện có kịp chuyển đổi, và không nên dùng cho bất cứ thứ gì mới.
Dữ liệu lượt quét không được đẩy ra ngoài
Chúng tôi cố ý không có webhook cho từng lượt quét. Mosaqo không gửi số liệu ở mức lượt quét tới endpoint của bên thứ ba, và không sự kiện nào trong bảng trên mang theo dữ liệu đó.
Lịch sử lượt quét chỉ có được khi bạn tự lấy bằng khóa của mình — GET /scans — và ngay cả khi đó nó vẫn chịu sự chi phối của thiết lập riêng tư về số liệu trong không gian làm việc: các chiều bạn đã tắt sẽ không xuất hiện ở bất kỳ dòng nào, bộ lọc bot vẫn áp dụng, và mã băm ẩn danh của khách truy cập đứng sau “lượt quét duy nhất” không bao giờ được để lộ. Phản hồi liệt kê allowedDimensions để bạn biết mình đang nhận gì.
scan.aggregate_ready cho biết một bản tổng hợp đã sẵn sàng; nó không chứa các lượt quét bên dưới. Nếu tích hợp của bạn thực sự cần từng lượt quét theo thời gian thực, điều đó có thể thu xếp theo yêu cầu — hãy trao đổi với chúng tôi thay vì mặc định là đã có sẵn.
Nếu bạn không thể nhận webhook
Hãy chủ động hỏi. GET /qr?updatedSince=… trả về các mã đã thay đổi, còn GET /scans?since=… đi tới trong lịch sử lượt quét với một con trỏ ổn định.