Lompat ke konten

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

PeristiwaTerpicu saat
qr.createdSebuah kode dibuat, di aplikasi atau lewat API
qr.publishedSebuah kode tayang dan pengalihannya mulai merespons
qr.updatedKonten, desain, nama, atau status berubah
qr.archivedSebuah kode diarsipkan dan ditarik dari host pengalihan
qr.deletedSebuah kode dihapus permanen
redirect.rule_changedTujuan atau aturan penargetan berubah
scan.aggregate_readyAgregat pemindaian sudah siap
export.readyEkspor asinkron selesai
approval.changedTinjauan disetujui atau ditolak
bulk.completedPekerjaan 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.published

Menolak 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.