Ga naar inhoud

Webhooks

Webhooks

Met webhooks ontvang je realtime meldingen wanneer er gebeurtenissen in je workspace plaatsvinden — bijv. wanneer een QR-code wordt gescand.

Webhook maken

Terminal window
POST /v1/webhooks
{
"url": "https://example.com/webhooks/qr3",
"events": ["qr.scanned", "qr.created"],
"secret": "my-secret-key-min-16-chars"
}

Response

{
"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"
}

Event-typen

EventBeschrijving
*Alle events
qr.createdQR-code aangemaakt
qr.updatedQR-code bijgewerkt (URL, status, tags)
qr.deletedQR-code verwijderd
qr.scannedQR-code gescand
qr.flaggedQR-code als onveilig gemarkeerd
webhook.pingTest-ping (via /ping-endpoint)

Payload-indeling

Alle webhook-payloads hebben deze indeling:

{
"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"
}
}

Code-events: qr.created en qr.updated

Bij deze beide events is data de code precies zoals GET /v1/codes/:id deze levert. Hiertoe behoort ook appearance, de op de code opgeslagen kleuren (null staat voor het standaard zwart of wit):

{
"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"
}
}

Het voorbeeld is ingekort. Alle velden zijn te vinden in de Codes API.

Handtekeningverificatie

qr3.app ondertekent elk webhook-verzoek met 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

Verificatie implementeren

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

Retry-logica

Bij fouten (HTTP >= 300 of timeout) probeert qr3.app de aflevering opnieuw:

PogingVertraging
1Onmiddellijk
21 minuut
35 minuten

Na 10 opeenvolgende fouten wordt de webhook automatisch uitgeschakeld.

Leveringsgarantie

qr3.app levert minstens één keer af, niet precies één keer. Dezelfde gebeurtenis kan je endpoint daarom meer dan eens bereiken: als nieuwe poging na een fout of als herhaling van een poging die al is verzonden.

  • Ontdubbel met de id van de payload (evt_…). Die hoort bij de gebeurtenis en blijft gelijk bij elke poging en elke herhaling. X-QR3-Delivery benoemt de afzonderlijke poging: een herhaling van die poging draagt dezelfde waarde, een nieuwe poging een nieuwe. Bevestig een id die je al kent met een 2xx, niet met een 4xx: een 4xx telt als mislukte poging en na 10 opeenvolgende fouten wordt de webhook automatisch uitgeschakeld.
  • Een poging die een uur na het aanmaken nog pending is (bijvoorbeeld omdat het wachtrijbericht verloren is gegaan), wordt door een dagelijkse opschoning op failed gezet; status_code en response_time_ms blijven null. Dat is geen fout van je endpoint en telt niet mee voor de 10 opeenvolgende fouten. Het log slaat hiervoor de tekst Delivery was not completed in time — closed by cleanup op; het endpoint voor de delivery-logs geeft die niet terug. Het sluiten is niet definitief: komt het bericht alsnog binnen, dan wordt het afgeleverd en vervangt het echte resultaat die tekst.

Delivery-logs

Terminal window
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"
}
]
}

Leveringspogingen worden 90 dagen bewaard (de lopende UTC-dag plus de 89 voorgaande dagen) en daarna automatisch verwijderd. Naast de getoonde velden slaat het log de payload van de gebeurtenis en de eerste 1 KB van het antwoord van het endpoint op; dit endpoint geeft geen van beide terug.

Test-ping

Test een webhook direct:

Terminal window
POST /v1/webhooks/:id/ping

Dit verzendt een webhook.ping-event naar je endpoint-URL.