Zum Inhalt springen

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

EreignisWird ausgelöst, wenn
qr.createdEin Code entsteht, in der App oder über die API
qr.publishedEin Code geht live und seine Weiterleitung greift
qr.updatedInhalt, Design, Name oder Status ändern sich
qr.archivedEin Code wird archiviert und vom Weiterleitungs-Host genommen
qr.deletedEin Code wird endgültig gelöscht
redirect.rule_changedEin Ziel oder eine Targeting-Regel ändert sich
scan.aggregate_readyEine Scan-Aggregation ist fertig
export.readyEin asynchroner Export ist abgeschlossen
approval.changedEine Prüfung wird freigegeben oder abgelehnt
bulk.completedEin 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.published

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