Ověření podpisu webhooku
Jak z hlavičky Tiklify-Signature ověřit, že požadavek poslal Tiklify a nikdo ho cestou nezměnil. S ukázkou v Node.js a PHP.
Aktualizováno 16. 10. 2026
Každý webhook nese hlavičku Tiklify-Signature. Než událost zpracujete, ověřte ji: potvrzuje, že požadavek poslal Tiklify a že tělo cestou nikdo nezměnil.
Tiklify-Signature: t=1760607000,v1=0b1e5ab4c9d0f3b6c0f2d9b5c1a4e7f8d2c3b4a5968778695a4b3c2d1e0f9a8b
t je čas podpisu v sekundách (Unix), v1 je HMAC-SHA256 v šestnáctkovém zápisu. Klíčem je celý „Podpisový klíč“ webhooku včetně předpony whsec_. Podepisuje se hodnota t, tečka a tělo požadavku přesně tak, jak přišlo:
1760607000.{"schemaVersion":2,"eventId":"3ed902c6-ce55-42d9-84f6-39c5de0e7435","eventType":"submission.approved",...}
Postup
- Přečtěte surové tělo požadavku, ještě než ho převedete z JSON.
- Z hlavičky vezměte
tav1. Požadavek se starýmtodmítněte, například když se od vašeho času liší o víc než 5 minut. Každý pokus o doručení podepisujeme znovu s aktuálním časem, takže kontrolou projde i opakované doručení. - Spočítejte HMAC-SHA256 a porovnejte ho s
v1funkcí, která porovnává v konstantním čase. - Událost si uložte a hned odpovězte kódem 2xx: na odpověď čekáme 10 sekund.
- Zpracujte ji až potom. Pořadí doručení není zaručené:
campaign.endeda poslednícampaign.status_changedmají stejnýoccurredAta mohou přijít v libovolném pořadí. Události se známýmeventIdpřeskočte a pořadí určujte podleoccurredAta vlastního uloženého stavu.
Kód 2xx vracejte jen pro platný podpis. Každá jiná odpověď se počítá jako neúspěšné doručení.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.TIKLIFY_WEBHOOK_SECRET; // celý klíč včetně whsec_
const TOLERANCE_SECONDS = 300; // 5 minut
const app = express();
// express.raw nechá tělo jako Buffer, přesně jak přišlo
app.post('/webhooks/tiklify', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Tiklify-Signature') ?? '';
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=', 2)));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!/^\d+$/.test(parts.t ?? '') || age > TOLERANCE_SECONDS) {
return res.sendStatus(400);
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${parts.t}.`)
.update(req.body)
.digest();
const received = Buffer.from(parts.v1 ?? '', 'hex');
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
enqueue(event); // vaše fronta: zpracujte později, duplicity podle event.eventId
res.sendStatus(204);
});
PHP
<?php
$secret = getenv('TIKLIFY_WEBHOOK_SECRET'); // celý klíč včetně whsec_
$tolerance = 300; // 5 minut
$body = file_get_contents('php://input'); // surové tělo, přesně jak přišlo
$parts = [];
foreach (explode(',', $_SERVER['HTTP_TIKLIFY_SIGNATURE'] ?? '') as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, '');
$parts[$key] = $value;
}
$t = $parts['t'] ?? '';
if (!ctype_digit($t) || abs(time() - (int) $t) > $tolerance) {
http_response_code(400);
exit;
}
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
enqueue($event); // vaše fronta: zpracujte později, duplicity podle $event['eventId']
http_response_code(204);
Nepomohlo? Napište nám na hello@tiklify.com.
Související
- API klíče a webhookyKde vytvoříte API klíč a webhook, které události webhook posílá a proč se po chybách sám vypne.
- Události webhookůKdy přijde která z deseti událostí, která pole nese tělo a jak zpracovat opakované doručení.
- Veřejné APIPřihlášení klíčem, pět endpointů pro kampaně a videa, stránkování, pole odpovědí, chyby a limit požadavků.