Webhooks
Webhooks
Med webhooks tar du emot realtidsaviseringar när händelser inträffar i din workspace — t.ex. när en QR-kod skannas.
Skapa webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Svar
{ "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"}Händelsetyper
| Händelse | Beskrivning |
|---|---|
* | Alla händelser |
qr.created | QR-kod skapad |
qr.updated | QR-kod uppdaterad (URL, status, taggar) |
qr.deleted | QR-kod raderad |
qr.scanned | QR-kod skannad |
qr.flagged | QR-kod markerad som osäker |
webhook.ping | Test-ping (via /ping-endpoint) |
Payload-format
Alla webhook-payloads har detta 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" }}Kodhändelser: qr.created och qr.updated
Vid dessa två händelser är data koden exakt så som GET /v1/codes/:id returnerar den. Detta inkluderar appearance, de färger som sparats på koden (null står för standard-svart respektive -vitt):
{ "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" }}Exemplet är förkortat. Alla fält finns i API:et för koder.
Signaturverifiering
qr3.app signerar varje webhook-begäran med HMAC-SHA256:
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Implementera verifiering
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)Retry-logik
Vid fel (HTTP >= 300 eller timeout) försöker qr3.app leverera igen:
| Försök | Fördröjning |
|---|---|
| 1 | Omedelbart |
| 2 | 1 minut |
| 3 | 5 minuter |
Efter 10 misslyckade försök i rad inaktiveras webhooken automatiskt.
Leveransgaranti
qr3.app levererar minst en gång, inte exakt en gång. Samma händelse kan därför nå din endpoint mer än en gång: som nytt försök efter ett fel eller som upprepning av ett försök som redan har skickats.
- Avduplicera med
idi payloaden (evt_…). Det hör till händelsen och är detsamma för varje försök och varje upprepning.X-QR3-Deliveryanger det enskilda försöket: en upprepning av det försöket har samma värde, ett nytt försök ett nytt. Besvara ettidsom du redan känner till med 2xx, inte med 4xx: Ett 4xx räknas som ett misslyckat försök, och efter 10 misslyckade försök i rad inaktiveras webhooken automatiskt. - Ett försök som fortfarande är
pendingen timme efter att det skapades (till exempel för att dess meddelande i kön gick förlorat) sätts tillfailedav en daglig städning;status_codeochresponse_time_msförblirnull. Det är inget fel hos din endpoint och räknas inte med bland de 10 misslyckade försöken i rad. Loggen lagrar textenDelivery was not completed in time — closed by cleanupför det; endpointen för leveransloggar returnerar den inte. Stängningen är inte slutgiltig: kommer meddelandet ändå fram, levereras det, och dess verkliga resultat ersätter texten.
Leveransloggar
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" } ]}Leveransförsök sparas i 90 dagar (den aktuella UTC-dagen och de 89 föregående dagarna) och raderas därefter automatiskt. Utöver de visade fälten lagrar loggen händelsens payload och de första 1 KB av endpointens svar; denna endpoint returnerar ingetdera.
Test-ping
Testa en webhook direkt:
POST /v1/webhooks/:id/pingDetta skickar en webhook.ping-händelse till din endpoint-URL.