Mosaqo-webhooks
Vi skickar en POST med signerad JSON till din endpoint för varje händelse du prenumererar på, och försöker om med växande mellanrum tills du tar emot den.
Prenumerera
Lägg antingen till en endpoint under Bulk & API, eller registrera en från din egen kod med en webhooks:write-nyckel — det är så en automationsplattform slår på och av 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 innehåller signeringens secret. Den visas bara en gång — spara den, eller byt ut prenumerationen senare.
Händelser
| Händelse | Utlöses när |
|---|---|
qr.created | En kod skapas, i appen eller via API:t |
qr.published | En kod går live och dess omdirigering börjar svara |
qr.updated | Innehåll, design, namn eller status ändras |
qr.archived | En kod arkiveras och tas bort från omdirigeringsvärden |
qr.deleted | En kod raderas permanent |
redirect.rule_changed | Ett mål eller en målgruppsregel ändras |
scan.aggregate_ready | En skanningsaggregering är klar |
export.ready | En asynkron export är färdig |
approval.changed | En granskning godkänns eller avslås |
bulk.completed | Ett massjobb blir klart |
Payload
När en händelse gäller en QR-kod bär data.qr samma sammanfattning som GET /qr/{id} returnerar — en trigger behöver alltså inte fråga tillbaka efter posten den just fick höra 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"
}
}
}Verifiera en leverans
Varje anrop bär en tidsstämpel och en signatur över både tidsstämpeln och 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.publishedAtt avvisa allt vars tidsstämpel ligger långt från din egen klocka är precis det som gör en avlyssnad leverans omöjlig att spela upp igen — hoppa inte över den kontrollen. Jämför signaturer i konstant tid och använd anropets råa kropp: att serialisera om den tolkade JSON-strukturen ändrar byten och då stämmer signaturen inte längre.
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"])Omförsök och fel
Svara med vilken 2xx som helst så snart du sparat händelsen; gör arbetet efteråt. Vi försöker om upp till sex gånger med exponentiellt växande mellanrum, så en långsam hanterare blir till dubbletter — behandla X-Mosaqo-Delivery som en idempotensnyckel hos dig.
Endpoints måste vara HTTPS på en publik värd. Lokala och privata adresser avvisas redan när prenumerationen skapas.
Utfasad signatur
X-Mosaqo-Signature-V0 bär en äldre signatur beräknad enbart på kroppen. Den kan inte uttrycka skydd mot uppspelning, följer med en release så att befintliga mottagare hinner byta, och ska inte användas till något nytt.
Skanningsdata skickas inte ut
Det finns medvetet ingen webhook per skanning. Mosaqo skickar inte analys på skanningsnivå till tredjeparts-endpoints, och ingen händelse i tabellen ovan bär sådan.
Skanningshistoriken är bara tillgänglig genom att du hämtar den själv med din egen nyckel — GET /scans — och även då styrs den av arbetsytans integritetsinställningar för analys: dimensioner du stängt av saknas i varje rad, botfiltret gäller, och den pseudonyma besökarhashen bakom ”unika skanningar” lämnas aldrig ut. Svaret listar allowedDimensions så att du vet vad du får.
scan.aggregate_ready säger att en aggregering är klar; den innehåller inte de underliggande skanningarna. Behöver din integration verkligen enskilda skanningar i realtid går det att ordna på begäran — prata med oss i stället för att utgå från att det finns.
Om du inte kan ta emot webhooks
Fråga i stället. GET /qr?updatedSince=… ger ändrade koder, och GET /scans?since=… går framåt genom skanningshistoriken med en stabil markör.