Mosaqo-webhooks
Vi sender en POST med signeret JSON til dit endpoint for hver hændelse, du abonnerer på, og prøver igen med voksende mellemrum, indtil du tager imod den.
Abonnér
Tilføj enten et endpoint under Bulk & API, eller registrér et fra din egen kode med en webhooks:write-nøgle — sådan tænder og slukker en automatiseringsplatform en trigger:
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"]
}'Svaret indeholder signeringens secret. Den vises kun én gang — gem den, eller udskift abonnementet senere.
Hændelser
| Hændelse | Udløses når |
|---|---|
qr.created | En kode oprettes, i appen eller via API’et |
qr.published | En kode går live, og dens omdirigering begynder at svare |
qr.updated | Indhold, design, navn eller status ændres |
qr.archived | En kode arkiveres og tages af omdirigeringsværten |
qr.deleted | En kode slettes permanent |
redirect.rule_changed | En destination eller en målretningsregel ændres |
scan.aggregate_ready | En scanningsaggregering er klar |
export.ready | En asynkron eksport er færdig |
approval.changed | En godkendelse gives eller afvises |
bulk.completed | En masseopgave afsluttes |
Payload
Når en hændelse handler om en QR-kode, bærer data.qr det samme resumé, som GET /qr/{id} returnerer — en trigger behøver altså ikke spørge tilbage efter den post, den lige har fået besked om.
{
"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"
}
}
}Verificér en levering
Hvert kald bærer et tidsstempel og en signatur over både tidsstemplet og kroppen:
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.publishedAt afvise alt, hvis tidsstempel ligger langt fra dit eget ur, er netop dét, der gør en opsnappet levering umulig at gentage — spring ikke det tjek over. Sammenlign signaturer i konstant tid, og brug kaldets rå krop: at serialisere den parsede JSON igen ændrer bytes, og så passer signaturen ikke længere.
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"])Gentagelser og fejl
Svar med et hvilket som helst 2xx, så snart du har gemt hændelsen; arbejdet gør du bagefter. Vi prøver op til seks gange med eksponentielt voksende mellemrum, så en langsom handler bliver til dobbelte leveringer — behandl X-Mosaqo-Delivery som en idempotensnøgle hos dig.
Endpoints skal være HTTPS på en offentlig vært. Lokale og private adresser afvises allerede, når abonnementet oprettes.
Udfaset signatur
X-Mosaqo-Signature-V0 bærer en ældre signatur beregnet alene på kroppen. Den kan ikke udtrykke beskyttelse mod gentagelse, følger med én udgivelse, så eksisterende modtagere kan skifte, og må ikke bruges til noget nyt.
Scanningsdata sendes ikke ud
Der findes bevidst ingen webhook pr. scanning. Mosaqo sender ikke analyse på scanningsniveau til tredjeparts-endpoints, og ingen hændelse i tabellen ovenfor bærer den.
Scanningshistorikken er kun tilgængelig ved, at du henter den selv med din egen nøgle — GET /scans — og selv da styres den af arbejdsområdets privatlivsindstillinger for analyse: dimensioner, du har slået fra, mangler i hver række, botfilteret gælder, og den pseudonyme besøgshash bag “unikke scanninger” udleveres aldrig. Svaret viser allowedDimensions, så du ved, hvad du får.
scan.aggregate_ready fortæller, at en aggregering er klar; den indeholder ikke de underliggende scanninger. Har din integration virkelig brug for enkelte scanninger i realtid, kan det lade sig gøre efter aftale — tal med os i stedet for at gå ud fra, at det findes.
Hvis du ikke kan modtage webhooks
Så spørg selv. GET /qr?updatedSince=… giver ændrede koder, og GET /scans?since=… går fremad gennem scanningshistorikken med en stabil markør.