Přejít na obsah

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

  1. Přečtěte surové tělo požadavku, ještě než ho převedete z JSON.
  2. Z hlavičky vezměte t a v1. Požadavek se starým t odmí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í.
  3. Spočítejte HMAC-SHA256 a porovnejte ho s v1 funkcí, která porovnává v konstantním čase.
  4. Událost si uložte a hned odpovězte kódem 2xx: na odpověď čekáme 10 sekund.
  5. Zpracujte ji až potom. Pořadí doručení není zaručené: campaign.ended a poslední campaign.status_changed mají stejný occurredAt a mohou přijít v libovolném pořadí. Události se známým eventId přeskočte a pořadí určujte podle occurredAt a 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.

Ověření podpisu webhooku — Nápověda Tiklify