Sari la conținut

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

Terminal window
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

EvenimentDescriere
*Toate evenimentele
qr.createdCod QR creat
qr.updatedCod QR actualizat (URL, status, etichete)
qr.deletedCod QR șters
qr.scannedCod QR scanat
qr.flaggedCod QR marcat ca nesigur
webhook.pingPing 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.scanned
X-QR3-Delivery: wdl_abc123
X-QR3-Attempt: 1
User-Agent: qr3-webhooks/1.0

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

Logica de reîncercare

În caz de erori (HTTP >= 300 sau timeout), qr3.app reîncearcă livrarea:

ÎncercareÎntârziere
1Imediat
21 minut
35 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-Delivery indică încercarea individuală: o repetare a acelei încercări poartă aceeași valoare, o încercare nouă una nouă. Confirmă cu 2xx un id pe 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ă pending la o oră după crearea ei (de exemplu, pentru că mesajul ei din coadă s-a pierdut) este setată pe failed de o curățare zilnică; status_code și response_time_ms rămân null. Aceasta nu este o eroare a endpoint-ului tău și nu se numără printre cele 10 erori consecutive. Jurnalul stochează pentru ea textul Delivery 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

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

Î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:

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

Aceasta trimite un eveniment webhook.ping către URL-ul endpoint-ului tău.