Przejdź do głównej zawartości

Webhooki

Webhooki

Dzięki webhookom otrzymujesz powiadomienia w czasie rzeczywistym, gdy w Twoim Workspace wystąpią zdarzenia — np. gdy kod QR zostanie zeskanowany.

Tworzenie webhooka

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

Odpowiedź

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

Typy zdarzeń

ZdarzenieOpis
*Wszystkie zdarzenia
qr.createdKod QR został utworzony
qr.updatedKod QR został zaktualizowany (URL, status, tagi)
qr.deletedKod QR został usunięty
qr.scannedKod QR został zeskanowany
qr.flaggedKod QR został oznaczony jako niebezpieczny
webhook.pingPing testowy (przez punkt końcowy /ping)

Format payloadu

Wszystkie payloady webhooków mają następujący 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"
}
}

Zdarzenia kodów: qr.created i qr.updated

W przypadku obu tych zdarzeń data to kod dokładnie w takiej postaci, w jakiej zwraca go GET /v1/codes/:id. Obejmuje to appearance, czyli kolory zapisane w kodzie (null oznacza domyślną czerń lub biel):

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

Przykład został skrócony. Wszystkie pola są dostępne w API kodów.

Weryfikacja sygnatury

qr3.app podpisuje każde żądanie webhooka za pomocą 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

Implementacja weryfikacji

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

Logika ponownych prób

W przypadku błędów (HTTP >= 300 lub limit czasu) qr3.app ponowi próbę dostarczenia:

PróbaOpóźnienie
1Natychmiast
21 minuta
35 minut

Po 10 kolejnych błędach webhook zostanie automatycznie wyłączony.

Gwarancja dostarczenia

qr3.app dostarcza co najmniej raz, a nie dokładnie raz. To samo zdarzenie może więc trafić do Twojego punktu końcowego więcej niż raz: jako ponowna próba po błędzie albo jako powtórzenie próby, która została już wysłana.

  • Usuwaj duplikaty za pomocą id z payloadu (evt_…). Należy ono do zdarzenia i pozostaje takie samo przy każdej próbie i każdym powtórzeniu. X-QR3-Delivery wskazuje pojedynczą próbę: powtórzenie tej próby ma tę samą wartość, nowa próba — nową. Na znane już id odpowiadaj kodem 2xx, a nie 4xx: 4xx liczy się jako nieudana próba, a po 10 kolejnych błędach webhook zostanie automatycznie wyłączony.
  • Próba, która godzinę po utworzeniu nadal ma status pending (na przykład dlatego, że jej wiadomość w kolejce zaginęła), jest ustawiana na failed przez codzienne czyszczenie; status_code i response_time_ms pozostają null. To nie jest błąd Twojego punktu końcowego i nie wlicza się do 10 kolejnych błędów. Log zapisuje dla niej tekst Delivery was not completed in time — closed by cleanup; endpoint logów dostarczeń go nie zwraca. Zamknięcie nie jest ostateczne: jeśli wiadomość mimo to dotrze, zostanie dostarczona, a jej rzeczywisty wynik zastąpi ten tekst.

Logi dostarczeń

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

Próby dostarczenia są przechowywane przez 90 dni (bieżący dzień UTC i 89 poprzednich dni), a następnie automatycznie usuwane. Poza pokazanymi polami log przechowuje payload zdarzenia i pierwszy kilobajt (1 KB) odpowiedzi punktu końcowego; ten endpoint nie zwraca żadnego z nich.

Ping testowy

Przetestuj webhook natychmiast:

Okno terminala
POST /v1/webhooks/:id/ping

Spowoduje to wysłanie zdarzenia webhook.ping na Twój adres URL punktu końcowego.