Mosaqo-webhooks
We sturen voor elke gebeurtenis waarop je bent geabonneerd een POST met ondertekende JSON naar je endpoint, en proberen het met groeiende tussenpozen opnieuw tot je hem accepteert.
Abonneren
Voeg een endpoint toe in Bulk & API, of registreer er een vanuit je eigen code met een webhooks:write-sleutel — zo zet een automatiseringsplatform een trigger aan en uit:
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"]
}'Het antwoord bevat het secret voor ondertekening. Het wordt maar één keer getoond — bewaar het, of vervang het abonnement later.
Gebeurtenissen
| Gebeurtenis | Wordt geactiveerd wanneer |
|---|---|
qr.created | Een code wordt gemaakt, in de app of via de API |
qr.published | Een code gaat live en de omleiding begint te werken |
qr.updated | Inhoud, ontwerp, naam of status verandert |
qr.archived | Een code wordt gearchiveerd en van de omleidingshost gehaald |
qr.deleted | Een code wordt definitief verwijderd |
redirect.rule_changed | Een bestemming of targetingregel verandert |
scan.aggregate_ready | Een scanaggregatie is klaar |
export.ready | Een asynchrone export is voltooid |
approval.changed | Een beoordeling wordt goedgekeurd of afgewezen |
bulk.completed | Een bulkopdracht is afgerond |
Payload
Gaat een gebeurtenis over een QR-code, dan draagt data.qr dezelfde samenvatting die GET /qr/{id} teruggeeft — een trigger hoeft het record waarover hij zojuist is ingelicht dus niet nog eens op te vragen.
{
"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"
}
}
}Een levering verifiëren
Elk verzoek draagt een tijdstempel en een handtekening over zowel het tijdstempel als de body:
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 weigeren waarvan het tijdstempel ver van je eigen klok ligt, is precies wat een onderschepte levering onherhaalbaar maakt — sla die controle niet over. Vergelijk handtekeningen in constante tijd en gebruik de ruwe request-body: de geparste JSON opnieuw serialiseren verandert de bytes, en dan klopt de handtekening niet meer.
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"])Nieuwe pogingen en fouten
Antwoord met een willekeurige 2xx zodra je de gebeurtenis hebt opgeslagen; doe het werk daarna. We proberen het tot zes keer opnieuw met exponentieel groeiende tussenpozen, dus een trage handler leidt tot dubbele leveringen — behandel X-Mosaqo-Delivery aan jouw kant als idempotentiesleutel.
Endpoints moeten HTTPS zijn op een publieke host. Lokale en private adressen worden al bij het aanmaken van het abonnement geweigerd.
Verouderde handtekening
X-Mosaqo-Signature-V0 draagt een oudere handtekening die alleen over de body is berekend. Die kan geen bescherming tegen herhaling uitdrukken, blijft één release meelopen zodat bestaande ontvangers kunnen migreren, en mag voor niets nieuws worden gebruikt.
Scangegevens worden niet gepusht
Er is bewust geen webhook per scan. Mosaqo stuurt geen analytics op scanniveau naar endpoints van derden, en geen enkele gebeurtenis in de tabel hierboven draagt die.
De scangeschiedenis is alleen beschikbaar door haar zelf op te halen met je eigen sleutel — GET /scans — en zelfs dan wordt ze bepaald door de analytics-privacyinstellingen van je werkruimte: dimensies die je hebt uitgezet ontbreken in elke rij, het botfilter geldt, en de pseudonieme bezoekershash achter “unieke scans” wordt nooit prijsgegeven. Het antwoord toont allowedDimensions zodat je weet wat je krijgt.
scan.aggregate_ready meldt dat een aggregatie klaar is; de onderliggende scans zitten er niet in. Heeft je integratie echt losse scans in realtime nodig, dan kan dat op aanvraag — bespreek het met ons in plaats van ervan uit te gaan dat het er al is.
Als je geen webhooks kunt ontvangen
Poll dan. GET /qr?updatedSince=… geeft gewijzigde codes, en GET /scans?since=… loopt met een stabiele cursor vooruit door de scangeschiedenis.