Webhooks
Webhooks
Cu ajutorul webhook-urilor primești notificări în timp real atunci când apar evenimente în workspace-ul tău — de exemplu, când un cod QR este scanat.
Creare webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Răspuns
{ "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"}Tipuri de evenimente
| Eveniment | Descriere |
|---|---|
* | Toate evenimentele |
qr.created | Cod QR creat |
qr.updated | Cod QR actualizat (URL, status, etichete) |
qr.deleted | Cod QR șters |
qr.scanned | Cod QR scanat |
qr.flagged | Cod QR marcat ca nesigur |
webhook.ping | Ping de test (prin endpoint-ul /ping) |
Formatul payload-ului
Toate payload-urile webhook-urilor au acest 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" }}Evenimente de cod: qr.created și qr.updated
În cazul acestor două evenimente, data este codul exact așa cum îl returnează GET /v1/codes/:id. Aceasta include appearance, culorile salvate pe cod (null reprezintă negrul, respectiv albul standard):
{ "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" }}Exemplul este prescurtat. Toate câmpurile sunt disponibile în API-ul Codes.
Verificarea semnăturii
qr3.app semnează fiecare cerere de webhook cu HMAC-SHA256:
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Implementarea verificării
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 de reîncercare
În caz de erori (HTTP >= 300 sau timeout), qr3.app reîncearcă livrarea:
| Încercare | Întârziere |
|---|---|
| 1 | Imediat |
| 2 | 1 minut |
| 3 | 5 minute |
După 10 erori consecutive, webhook-ul este dezactivat automat.
Garanția livrării
qr3.app livrează cel puțin o dată, nu exact o dată. Același eveniment poate ajunge, prin urmare, de mai multe ori la endpoint-ul tău: ca reîncercare după o eroare sau ca repetare a unei încercări deja trimise.
- Elimină duplicatele cu ajutorul
id-ului din payload (evt_…). Aparține evenimentului și rămâne același la fiecare încercare și la fiecare repetare.X-QR3-Deliveryindică încercarea individuală: o repetare a acelei încercări poartă aceeași valoare, o încercare nouă una nouă. Confirmă cu 2xx unidpe care îl cunoști deja, nu cu 4xx: un 4xx se numără ca încercare eșuată, iar după 10 erori consecutive webhook-ul este dezactivat automat. - O încercare care este încă
pendingla o oră după crearea ei (de exemplu, pentru că mesajul ei din coadă s-a pierdut) este setată pefailedde o curățare zilnică;status_codeșiresponse_time_msrămânnull. Aceasta nu este o eroare a endpoint-ului tău și nu se numără printre cele 10 erori consecutive. Jurnalul stochează pentru ea textulDelivery was not completed in time — closed by cleanup; endpoint-ul jurnalelor de livrare nu îl returnează. Închiderea nu este definitivă: dacă mesajul ajunge totuși, este livrat, iar rezultatul său real înlocuiește acest text.
Jurnale de livrare
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" } ]}Încercările de livrare se păstrează 90 de zile (ziua UTC curentă și cele 89 de zile anterioare), apoi se șterg automat. În afara câmpurilor afișate, jurnalul stochează payload-ul evenimentului și primul kilobyte (1 KB) din răspunsul endpoint-ului; acest endpoint nu returnează niciunul dintre ele.
Ping de test
Testează un webhook imediat:
POST /v1/webhooks/:id/pingAceasta trimite un eveniment webhook.ping către URL-ul endpoint-ului tău.