Webhook Mosaqo
Inviamo un POST con corpo JSON firmato al tuo endpoint per ogni evento a cui sei iscritto, riprovando con attesa crescente finché non lo accetti.
Sottoscrizione
Aggiungi un endpoint in Bulk & API, oppure registralo dal tuo codice con una chiave webhooks:write: è così che una piattaforma di automazione accende e spegne un 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"]
}'La risposta contiene il secret di firma. Viene mostrato una volta sola: conservalo, oppure sostituisci la sottoscrizione più avanti.
Eventi
| Evento | Scatta quando |
|---|---|
qr.created | Un codice viene creato, nell’app o tramite API |
qr.published | Un codice va online e il suo reindirizzamento inizia a rispondere |
qr.updated | Cambiano contenuto, design, nome o stato |
qr.archived | Un codice viene archiviato e tolto dall’host di reindirizzamento |
qr.deleted | Un codice viene eliminato definitivamente |
redirect.rule_changed | Cambia una destinazione o una regola di targeting |
scan.aggregate_ready | Un aggregato di scansioni è pronto |
export.ready | Un export asincrono è terminato |
approval.changed | Una revisione viene approvata o respinta |
bulk.completed | Un job in blocco si conclude |
Payload
Quando un evento riguarda un codice QR, data.qr porta lo stesso riepilogo restituito da GET /qr/{id}: un trigger non deve quindi richiamare il record di cui gli è appena stato parlato.
{
"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"
}
}
}Verificare una consegna
Ogni richiesta porta un timestamp e una firma calcolata sia sul timestamp sia sul corpo:
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.publishedRifiutare tutto ciò il cui timestamp è lontano dal tuo orologio è esattamente ciò che rende irripetibile una consegna intercettata: non saltare quel controllo. Confronta le firme a tempo costante e usa il corpo grezzo della richiesta: riserializzare il JSON analizzato cambia i byte e la firma non corrisponderà più.
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"])Ritentativi ed errori
Rispondi con un qualsiasi 2xx non appena hai salvato l’evento, e fai il lavoro dopo. Riproviamo fino a sei volte con attesa esponenziale, quindi un handler lento si traduce in consegne duplicate: tratta X-Mosaqo-Delivery come chiave di idempotenza dalla tua parte.
Gli endpoint devono essere HTTPS su un host pubblico. Gli indirizzi locali e privati vengono rifiutati già alla creazione della sottoscrizione.
Firma deprecata
X-Mosaqo-Signature-V0 porta una firma più vecchia calcolata sul solo corpo. Non può esprimere protezione dalla ripetizione, resta per una versione perché i ricevitori esistenti possano migrare e non va usata per nulla di nuovo.
I dati delle scansioni non vengono inviati
Non esiste deliberatamente alcun webhook per singola scansione. Mosaqo non invia analitiche a livello di scansione a endpoint di terze parti, e nessun evento della tabella sopra le trasporta.
Lo storico delle scansioni è disponibile solo recuperandolo tu stesso con la tua chiave — GET /scans — e anche allora è governato dalle impostazioni di privacy analitica del tuo spazio: le dimensioni che hai disattivato non compaiono in nessuna riga, il filtro bot si applica e l’hash pseudonimo del visitatore dietro le «scansioni uniche» non viene mai esposto. La risposta elenca allowedDimensions così sai cosa stai ricevendo.
scan.aggregate_ready dice che un aggregato è pronto; non contiene le scansioni sottostanti. Se la tua integrazione ha davvero bisogno delle singole scansioni in tempo reale, è disponibile su richiesta: parlane con noi invece di darlo per scontato.
Se non puoi ricevere webhook
Interroga l’API. GET /qr?updatedSince=… restituisce i codici modificati e GET /scans?since=… percorre lo storico delle scansioni in avanti con un cursore stabile.