Webhook Mosaqo
Kami mengirim POST dengan badan JSON bertanda tangan ke endpoint Anda untuk setiap peristiwa yang Anda langgan, dan mencoba lagi dengan jeda yang membesar sampai Anda menerimanya.
Berlangganan
Tambahkan endpoint di Bulk & API, atau daftarkan dari kode Anda sendiri dengan kunci webhooks:write — begitulah platform otomatisasi menyalakan dan mematikan sebuah pemicu:
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"]
}'Respons berisi secret untuk penandatanganan. Ini hanya ditampilkan sekali — simpan, atau ganti langganannya nanti.
Peristiwa
| Peristiwa | Terpicu saat |
|---|---|
qr.created | Sebuah kode dibuat, di aplikasi atau lewat API |
qr.published | Sebuah kode tayang dan pengalihannya mulai merespons |
qr.updated | Konten, desain, nama, atau status berubah |
qr.archived | Sebuah kode diarsipkan dan ditarik dari host pengalihan |
qr.deleted | Sebuah kode dihapus permanen |
redirect.rule_changed | Tujuan atau aturan penargetan berubah |
scan.aggregate_ready | Agregat pemindaian sudah siap |
export.ready | Ekspor asinkron selesai |
approval.changed | Tinjauan disetujui atau ditolak |
bulk.completed | Pekerjaan massal selesai |
Payload
Saat sebuah peristiwa menyangkut kode QR, data.qr membawa ringkasan yang sama dengan yang dikembalikan GET /qr/{id} — jadi pemicu tidak perlu meminta ulang catatan yang baru saja diberitahukan kepadanya.
{
"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"
}
}
}Memverifikasi pengiriman
Setiap permintaan membawa stempel waktu dan tanda tangan atas stempel sekaligus badan permintaan:
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.publishedMenolak apa pun yang stempel waktunya jauh dari jam Anda sendiri adalah hal yang membuat pengiriman yang tersadap tidak bisa diputar ulang — jangan lewati pemeriksaan itu. Bandingkan tanda tangan dalam waktu konstan, dan gunakan badan permintaan mentah: menserialisasi ulang JSON yang sudah diurai mengubah byte-nya, dan tanda tangannya tidak akan cocok lagi.
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"])Percobaan ulang dan kegagalan
Balas dengan 2xx apa pun begitu Anda menyimpan peristiwanya; kerjakan sisanya setelah itu. Kami mencoba ulang hingga enam kali dengan jeda eksponensial, jadi penangan yang lambat berubah menjadi pengiriman ganda — perlakukan X-Mosaqo-Delivery sebagai kunci idempotensi di sisi Anda.
Endpoint harus HTTPS di host publik. Alamat lokal dan privat ditolak sejak langganan dibuat.
Tanda tangan usang
X-Mosaqo-Signature-V0 membawa tanda tangan lama yang dihitung hanya dari badan permintaan. Ia tidak bisa menyatakan perlindungan terhadap pengulangan, ikut satu rilis lagi agar penerima yang ada sempat bermigrasi, dan tidak boleh dipakai untuk apa pun yang baru.
Data pemindaian tidak dikirim keluar
Secara sengaja tidak ada webhook per pemindaian. Mosaqo tidak mengirim analitik tingkat pemindaian ke endpoint pihak ketiga, dan tidak ada peristiwa di tabel di atas yang membawanya.
Riwayat pemindaian hanya tersedia jika Anda mengambilnya sendiri dengan kunci Anda — GET /scans — dan bahkan saat itu ia diatur oleh pengaturan privasi analitik ruang kerja Anda: dimensi yang Anda matikan tidak muncul di baris mana pun, filter bot berlaku, dan hash pengunjung samaran di balik “pemindaian unik” tidak pernah diungkap. Respons memuat allowedDimensions agar Anda tahu apa yang Anda terima.
scan.aggregate_ready memberi tahu bahwa agregat sudah siap; ia tidak berisi pemindaian itu sendiri. Jika integrasi Anda memang memerlukan pemindaian individual secara langsung, hal itu tersedia atas permintaan — bicarakan dengan kami alih-alih menganggapnya sudah ada.
Jika Anda tidak bisa menerima webhook
Lakukan penarikan berkala. GET /qr?updatedSince=… memberi kode yang berubah, dan GET /scans?since=… menyusuri riwayat pemindaian maju dengan kursor yang stabil.