Aller au contenu

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

Fenêtre de terminal
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énementDescription
*Tous les événements
qr.createdCode QR créé
qr.updatedCode QR mis à jour (URL, statut, tags)
qr.deletedCode QR supprimé
qr.scannedCode QR scanné
qr.flaggedCode QR marqué comme suspect
webhook.pingPing 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.scanned
X-QR3-Delivery: wdl_abc123
X-QR3-Attempt: 1
User-Agent: qr3-webhooks/1.0

Implé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 });
},
);

Logique de tentative

En cas d’erreur (HTTP >= 300 ou timeout), qr3.app tente à nouveau la livraison :

TentativeDélai
1Immédiatement
21 minute
35 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’id du payload (evt_…). Il appartient à l’événement et reste identique pour chaque tentative et chaque répétition. X-QR3-Delivery désigne la tentative individuelle : une répétition de cette tentative porte la même valeur, une nouvelle tentative une nouvelle valeur. Acquittez un id dé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 pending une heure après sa création (par exemple parce que son message de file d’attente a été perdu) est passée à failed par un nettoyage quotidien ; status_code et response_time_ms restent null. 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 texte Delivery 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

Fenêtre de terminal
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 :

Fenêtre de terminal
POST /v1/webhooks/:id/ping

Cela envoie un événement webhook.ping à l’URL de votre endpoint.