Mosaqo Webhooks
Wir senden für jedes abonnierte Ereignis ein POST mit signiertem JSON an Ihren Endpunkt und wiederholen mit wachsendem Abstand, bis Sie es annehmen.
Abonnieren
Tragen Sie einen Endpunkt unter Bulk & API ein oder registrieren Sie ihn aus Ihrem eigenen Code mit einem webhooks:write-Schlüssel — genau so schalten Automatisierungsplattformen einen Trigger ein und aus:
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"]
}'Die Antwort enthält das secret zum Signieren. Es wird nur einmal gezeigt — speichern Sie es oder ersetzen Sie das Abonnement später.
Ereignisse
| Ereignis | Wird ausgelöst, wenn |
|---|---|
qr.created | Ein Code entsteht, in der App oder über die API |
qr.published | Ein Code geht live und seine Weiterleitung greift |
qr.updated | Inhalt, Design, Name oder Status ändern sich |
qr.archived | Ein Code wird archiviert und vom Weiterleitungs-Host genommen |
qr.deleted | Ein Code wird endgültig gelöscht |
redirect.rule_changed | Ein Ziel oder eine Targeting-Regel ändert sich |
scan.aggregate_ready | Eine Scan-Aggregation ist fertig |
export.ready | Ein asynchroner Export ist abgeschlossen |
approval.changed | Eine Prüfung wird freigegeben oder abgelehnt |
bulk.completed | Ein Bulk-Job endet |
Payload
Betrifft ein Ereignis einen QR-Code, enthält data.qr dieselbe Zusammenfassung wie GET /qr/{id} — ein Trigger muss den gerade gemeldeten Datensatz also nicht erst zurückfragen.
{
"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"
}
}
}Eine Zustellung prüfen
Jede Anfrage trägt einen Zeitstempel und eine Signatur über Zeitstempel und Body zugleich:
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.publishedAlles abzulehnen, dessen Zeitstempel weit von Ihrer eigenen Uhr entfernt liegt, macht eine mitgeschnittene Zustellung unwiederholbar — überspringen Sie diese Prüfung nicht. Vergleichen Sie Signaturen in konstanter Zeit und verwenden Sie den rohen Request-Body: erneutes Serialisieren des geparsten JSON ändert die Bytes, und die Signatur passt nicht mehr.
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"])Wiederholungen und Fehler
Antworten Sie mit irgendeinem 2xx, sobald Sie das Ereignis gespeichert haben; die Arbeit folgt danach. Wir wiederholen bis zu sechsmal mit exponentiell wachsendem Abstand, ein langsamer Handler wird also zu doppelten Zustellungen — behandeln Sie X-Mosaqo-Delivery auf Ihrer Seite als Idempotenzschlüssel.
Endpunkte müssen HTTPS auf einem öffentlichen Host sein. Lokale und private Adressen werden schon beim Anlegen des Abonnements abgelehnt.
Veraltete Signatur
X-Mosaqo-Signature-V0 trägt eine ältere Signatur allein über den Body. Sie kann keinen Wiederholungsschutz ausdrücken, läuft noch ein Release mit, damit bestehende Empfänger migrieren können, und darf für nichts Neues verwendet werden.
Scan-Daten werden nicht gepusht
Es gibt bewusst keinen Webhook pro Scan. Mosaqo sendet keine Analytics auf Scan-Ebene an fremde Endpunkte, und kein Ereignis in der Tabelle oben trägt sie.
Die Scan-Historie erhalten Sie nur, indem Sie sie selbst mit Ihrem Schlüssel abholen — GET /scans — und auch dann gelten die Analytics-Datenschutzeinstellungen Ihres Workspace: abgeschaltete Dimensionen fehlen in jeder Zeile, der Bot-Filter greift, und der pseudonyme Besucher-Hash hinter „einzigartigen Scans“ wird nie herausgegeben. Die Antwort listet allowedDimensions, damit Sie wissen, was Sie bekommen.
scan.aggregate_ready meldet nur, dass eine Aggregation fertig ist; die zugrunde liegenden Scans enthält es nicht. Braucht Ihre Integration einzelne Scans wirklich in Echtzeit, ist das auf Anfrage möglich — sprechen Sie uns an, statt es vorauszusetzen.
Wenn Sie keine Webhooks empfangen können
Dann pollen Sie. GET /qr?updatedSince=… liefert geänderte Codes, und GET /scans?since=… läuft mit stabilem Cursor durch die Scan-Historie.