Ověření a zpracování webhooků
Kdykoli nastane vybraná událost, QRflex pošle HTTP POST na vaši adresu. Každý požadavek je podepsaný — ověříte si tak, že přišel opravdu od nás a cestou se nezměnil. Webhooky zapnete v Nastavení → Webhooky.
Obálka a hlavičky
Tělo každého požadavku má stejnou obálku; metadata pro ověření jsou v hlavičkách.
Obálka (tělo POST)
{
"id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"event": "scan.created",
"createdAt": "2026-10-12T10:09:50+02:00",
"data": {
"qrCodeId": "0f8e...",
"deviceType": "mobile",
"countryCode": "CZ"
}
}
Hlavičky požadavku
webhook-id | Jednoznačné ID doručení. Použijte ho pro deduplikaci (idempotenci). |
webhook-timestamp | Čas odeslání (unixové sekundy). Použijte ho pro ochranu proti replay útokům. |
webhook-signature | Podpis ve formátu v1,<base64>. Může obsahovat víc podpisů oddělených mezerou. |
Content-Type | Vždy application/json. |
Jaké události přijdou
Vyberte, které události chcete odebírat. Obsah pole data se liší podle typu události.
event | Popis | data |
|---|---|---|
scan.created | QR kód byl naskenován. | qrCodeId, deviceType, countryCode |
conversion.created | Návštěvník provedl konverzi (kliknutí na CTA nebo odeslání formuláře). | qrCodeId, eventType, sessionToken, pageUrl, occurredAt |
qr.created | Byl vytvořen nový QR kód. | id, name, type |
qr.limit_warning | QR kód se blíží limitu skenů. | used, limit, percent |
campaign.published | Kampaň byla publikována. | id, name |
bot.detected | Sken byl vyhodnocen jako bot. | qrCodeId, botName, botCategory |
webhook.test | Testovací událost z tlačítka Test v nastavení. | message |
Jak ověřit podpis
Z hlaviček složte řetězec {webhook-id}.{webhook-timestamp}.{tělo} a spočtěte jeho HMAC-SHA256 se svým secretem — ze secretu nejdřív odřízněte prefix whsec_ a zbytek dekódujte z base64. Výsledek pak v konstantním čase porovnejte s hodnotou v hlavičce webhook-signature (bez prefixu v1,).
function verify(string $secret, array $headers, string $rawBody): bool {
$id = $headers['webhook-id'] ?? '';
$ts = $headers['webhook-timestamp'] ?? '';
$hdr = $headers['webhook-signature'] ?? '';
if (abs(time() - (int) $ts) > 300) return false; // replay
$key = base64_decode(substr($secret, strlen('whsec_')));
$signed = $id . '.' . $ts . '.' . $rawBody;
$expected = base64_encode(hash_hmac('sha256', $signed, $key, true));
foreach (explode(' ', $hdr) as $part) { // "v1,xxx v1,yyy"
[$ver, $sig] = array_pad(explode(',', $part, 2), 2, '');
if ($ver === 'v1' && hash_equals($expected, $sig)) return true;
}
return false;
}
const crypto = require('crypto');
function verify(secret, headers, rawBody) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
const hdr = headers['webhook-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const signed = `${id}.${ts}.${rawBody}`;
const expected = crypto.createHmac('sha256', key).update(signed).digest('base64');
return hdr.split(' ').some((part) => {
const [ver, sig] = part.split(',');
if (ver !== 'v1' || !sig) return false;
const a = Buffer.from(sig), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id", "")
ts = headers.get("webhook-timestamp", "")
hdr = headers.get("webhook-signature", "")
if abs(time.time() - int(ts)) > 300: # replay
return False
key = base64.b64decode(secret[len("whsec_"):])
signed = f"{msg_id}.{ts}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for part in hdr.split(" "): # "v1,xxx v1,yyy"
ver, _, sig = part.partition(",")
if ver == "v1" and hmac.compare_digest(sig, expected):
return True
return False
Ověřujte vždy proti hodnotám z hlaviček, ne z těla. Požadavky starší než pár minut odmítejte (ochrana proti replay útoku) a případné duplicity poznáte podle webhook-id.
Opakování a vypnutí
Pokud váš server nevrátí 2xx, doručení opakujeme.
Až 5 pokusů s prodlevami 5 minut, 1 hodina, 6 hodin a 12 hodin.
Po 10 selháních v řadě webhook automaticky vypneme (status disabled).
Historii doručení uchováváme 30 dní.
Doručení si vyzkoušejte tlačítkem Test v nastavení; pro rychlou kontrolu bez vlastního serveru použijte webhook.site.