Webhooks
Webhooks
Avec les webhooks, vous recevez des notifications en temps réel lorsque des événements se produisent dans votre espace de travail — par exemple, lorsqu’un code QR est scanné.
Créer un webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Réponse
{ "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"}Types d’événements
| Événement | Description |
|---|---|
* | Tous les événements |
qr.created | Code QR créé |
qr.updated | Code QR mis à jour (URL, statut, tags) |
qr.deleted | Code QR supprimé |
qr.scanned | Code QR scanné |
qr.flagged | Code QR marqué comme suspect |
webhook.ping | Ping de test (via l’endpoint /ping) |
Format du payload
Tous les payloads de webhook ont ce format :
{ "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" }}Événements de code : qr.created et qr.updated
Pour ces deux événements, data est le code exactement tel que GET /v1/codes/:id le renvoie. Cela inclut appearance, les couleurs enregistrées sur le code (null correspond au noir ou blanc par défaut) :
{ "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’exemple est raccourci. Tous les champs sont disponibles dans l’API Codes.
Vérification de la signature
qr3.app signe chaque requête de webhook avec HMAC-SHA256 :
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Implémenter la vérification
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)Logique de tentative
En cas d’erreur (HTTP >= 300 ou timeout), qr3.app tente à nouveau la livraison :
| Tentative | Délai |
|---|---|
| 1 | Immédiatement |
| 2 | 1 minute |
| 3 | 5 minutes |
Après 10 erreurs consécutives, le webhook est automatiquement désactivé.
Garantie de livraison
qr3.app livre au moins une fois, et non exactement une fois. Un même événement peut donc parvenir plusieurs fois à votre endpoint : comme nouvelle tentative après une erreur, ou comme répétition d’une tentative déjà envoyée.
- Dédoublonnez avec l’
iddu payload (evt_…). Il appartient à l’événement et reste identique pour chaque tentative et chaque répétition.X-QR3-Deliverydésigne la tentative individuelle : une répétition de cette tentative porte la même valeur, une nouvelle tentative une nouvelle valeur. Acquittez uniddéjà connu avec un code 2xx, et non 4xx : un 4xx compte comme une tentative échouée, et après 10 erreurs consécutives le webhook est automatiquement désactivé. - Une tentative encore
pendingune heure après sa création (par exemple parce que son message de file d’attente a été perdu) est passée àfailedpar un nettoyage quotidien ;status_codeetresponse_time_msrestentnull. Ce n’est pas une erreur de votre endpoint et cela ne compte pas parmi les 10 erreurs consécutives. Le journal enregistre alors le texteDelivery was not completed in time — closed by cleanup; le point de terminaison des journaux de livraison ne le renvoie pas. La clôture n’est pas définitive : si le message arrive finalement, il est livré et son véritable résultat remplace ce texte.
Journaux de livraison
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" } ]}Les tentatives de livraison sont conservées 90 jours (le jour UTC en cours et les 89 jours précédents), puis supprimées automatiquement. En plus des champs affichés, le journal stocke le payload de l’événement et le premier kilo-octet (1 Ko) de la réponse du point de terminaison ; ce point de terminaison ne renvoie ni l’un ni l’autre.
Ping de test
Testez un webhook immédiatement :
POST /v1/webhooks/:id/pingCela envoie un événement webhook.ping à l’URL de votre endpoint.