Webhooks
Webhooks
Con i webhook ricevi notifiche in tempo reale quando si verificano eventi nel tuo workspace, ad esempio quando viene scansionato un codice QR.
Creare un webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Risposta
{ "id": "wh_abc123", "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "is_active": true, "secret": "my-secret-key-min-16-chars", "secret_hint": "my-s…", "created_at": "2026-03-15T10:00:00.000Z"}Tipi di evento
| Evento | Descrizione |
|---|---|
* | Tutti gli eventi |
qr.created | Codice QR creato |
qr.updated | Codice QR aggiornato (URL, stato, tag) |
qr.deleted | Codice QR eliminato |
qr.scanned | Codice QR scansionato |
qr.flagged | Codice QR contrassegnato come non sicuro |
webhook.ping | Ping di test (tramite l’endpoint /ping) |
Formato del payload
Tutti i payload dei webhook hanno questo formato:
{ "id": "evt_4f0c2a91d7e84b3c9a1f", "type": "qr.scanned", "created": "2026-03-15T10:00:00.000Z", "data": { "code_id": "qr_abc123", "short_code": "r7f3Kx", "scanned_at": "2026-03-15T10:00:00.000Z", "country": "AT", "device_type": "mobile", "os": "iOS", "browser": "Safari", "language": "de", "ab_variant": null, "redirected_to": "https://example.com/landing" }}Eventi codice: qr.created e qr.updated
In questi due eventi, data è il codice esattamente come viene restituito da GET /v1/codes/:id. Questo include appearance, ovvero i colori memorizzati sul codice (null rappresenta il nero o il bianco predefinito):
{ "id": "evt_9b2e41c07a5d4f18b3c6", "type": "qr.updated", "created": "2026-09-28T10:00:00.000Z", "data": { "id": "qr_abc123", "short_code": "r7f3Kx", "status": "live", "appearance": { "foreground_color": "#1F4E79", "background_color": null }, "updated_at": "2026-09-28T10:00:00.000Z" }}L’esempio è abbreviato. Tutti i campi sono disponibili nelle API dei codici.
Verifica della firma
qr3.app firma ogni richiesta webhook con HMAC-SHA256:
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Implementare la verifica
import crypto from "crypto";
function verifySignature( payload: string, signature: string | undefined, secret: string): boolean { const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); const expectedBytes = Buffer.from(`sha256=${expected}`); const receivedBytes = Buffer.from(signature ?? ""); return ( expectedBytes.length === receivedBytes.length && crypto.timingSafeEqual(expectedBytes, receivedBytes) );}
// In Express: verify the raw body — re-serialised JSON breaks the signature.app.post( "/webhooks/qr3", express.raw({ type: "application/json" }), (req, res) => { const raw = req.body.toString("utf8"); const signature = req.headers["x-qr3-signature"] as string | undefined; if (!verifySignature(raw, signature, process.env.QR3_WEBHOOK_SECRET!)) { return res.status(401).json({ error: "Invalid signature" }); } const event = JSON.parse(raw); console.log(event.type, event.data); res.json({ received: true }); },);import hmacimport hashlib
def verify_signature(payload: str, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() received = (signature or "").removeprefix("sha256=") return hmac.compare_digest(expected, received)Logica di retry
In caso di errori (HTTP >= 300 o timeout), qr3.app riprova a inviare la notifica:
| Tentativo | Ritardo |
|---|---|
| 1 | Immediatamente |
| 2 | 1 minuto |
| 3 | 5 minuti |
Dopo 10 errori consecutivi, il webhook viene disattivato automaticamente.
Garanzia di consegna
qr3.app consegna almeno una volta, non esattamente una. Lo stesso evento può quindi raggiungere il tuo endpoint più di una volta: come nuovo tentativo dopo un errore o come ripetizione di un tentativo già inviato.
- Deduplica con l’
iddel payload (evt_…). Appartiene all’evento e resta uguale per ogni tentativo e ogni ripetizione.X-QR3-Deliveryidentifica il singolo tentativo: una ripetizione di quel tentativo porta lo stesso valore, un nuovo tentativo un valore nuovo. Conferma con un 2xx unidche già conosci, non con un 4xx: un 4xx conta come tentativo fallito e, dopo 10 errori consecutivi, il webhook viene disattivato automaticamente. - Un tentativo ancora
pendingun’ora dopo la sua creazione (ad esempio perché il suo messaggio in coda è andato perso) viene impostato sufailedda una pulizia giornaliera;status_codeeresponse_time_msrestanonull. Non è un errore del tuo endpoint e non conta tra i 10 errori consecutivi. Il log memorizza per questo il testoDelivery was not completed in time — closed by cleanup; l’endpoint dei log di consegna non lo restituisce. La chiusura non è definitiva: se il messaggio arriva comunque, viene consegnato e il suo risultato reale sostituisce quel testo.
Log di consegna
GET /v1/webhooks/:id/deliveries{ "data": [ { "id": "wdl_abc123", "event_type": "qr.scanned", "status": "success", "status_code": 200, "response_time_ms": 145, "attempt": 1, "created_at": "2026-03-15T10:00:00.000Z" } ]}I tentativi di consegna vengono conservati per 90 giorni (il giorno UTC in corso e gli 89 precedenti) e poi eliminati automaticamente. Oltre ai campi mostrati, il log memorizza il payload dell’evento e il primo kilobyte (1 KB) della risposta dell’endpoint; questo endpoint non restituisce né l’uno né l’altro.
Ping di test
Testa immediatamente un webhook:
POST /v1/webhooks/:id/pingQuesto invia un evento webhook.ping all’URL del tuo endpoint.