Webhooks
Webhooks
С уебхукове получавате известия в реално време, когато възникнат събития във вашия workspace — например когато се сканира QR код.
Създаване на webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Отговор
{ "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"}Типове събития
| Събитие | Описание |
|---|---|
* | Всички събития |
qr.created | Създаден QR код |
qr.updated | Обновен QR код (URL, статус, тагове) |
qr.deleted | Изтрит QR код |
qr.scanned | Сканиран QR код |
qr.flagged | QR кодът е маркиран като небезопасен |
webhook.ping | Тестов пинг (чрез /ping крайна точка) |
Формат на payload
Всички payloads на уебхуковете имат следния формат:
{ "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" }}Събития за кодове: qr.created и qr.updated
При тези две събития data е кодът точно във вида, в който GET /v1/codes/:id го връща. Това включва appearance – цветовете, записани за кода (null означава стандартното черно или бяло):
{ "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" }}Примерът е съкратен. Всички полета са налични в API за кодове.
Верификация на сигнатура
qr3.app подписва всяка заявка към уебхук с HMAC-SHA256:
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Имплементиране на верификация
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)Логика за повторен опит
При грешки (HTTP >= 300 или изтичане на времето) qr3.app опитва повторно доставянето:
| Опит | Закъснение |
|---|---|
| 1 | Веднага |
| 2 | 1 минута |
| 3 | 5 минути |
След 10 последователни грешки уебхукът се деактивира автоматично.
Гаранция за доставка
qr3.app доставя поне веднъж, а не точно веднъж. Затова едно и също събитие може да достигне до вашата крайна точка повече от веднъж: като повторен опит след грешка или като повторение на опит, който вече е изпратен.
- Премахвайте дубликатите с помощта на идентификатора
idот payload (evt_…). Той принадлежи на събитието и остава един и същ при всеки опит и при всяко повторение.X-QR3-Deliveryобозначава отделния опит: повторение на този опит носи същата стойност, нов опит — нова. Потвърждавайте вече известенidс 2xx, а не с 4xx: 4xx се брои за неуспешен опит, а след 10 последователни грешки уебхукът се деактивира автоматично. - Опит, който един час след създаването си все още е
pending(например защото съобщението му в опашката е изгубено), се поставя наfailedот ежедневно почистване;status_codeиresponse_time_msоставатnull. Това не е грешка на вашата крайна точка и не се брои към 10-те последователни грешки. Дневникът записва за него текстаDelivery was not completed in time — closed by cleanup; крайната точка за логовете за доставка не го връща. Затварянето не е окончателно: ако съобщението все пак пристигне, то се доставя, а истинският му резултат заменя този текст.
Логове за доставка
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" } ]}Опитите за доставка се съхраняват 90 дни (текущият UTC ден плюс предходните 89 дни) и след това се изтриват автоматично. Освен показаните полета дневникът съхранява payload на събитието и първия килобайт (1 KB) от отговора на крайната точка; тази крайна точка не връща нито едното, нито другото.
Тестов пинг
Тествайте уебхук веднага:
POST /v1/webhooks/:id/pingТова изпраща събитие webhook.ping към URL адреса на вашата крайна точка.