Webhooks and signatures
How every message from Tocler is signed and how to check it in Node.js, Python and PHP.
Tocler signs every message with HMAC-SHA256 over <timestamp>.<raw body>, using a secret only you and Tocler know. Check it before doing anything else.
| API delivery | Tocler Pay webhooks | |
|---|---|---|
| Signature header | x-tocler-signature: v1=<hex> | Tocler-Pay-Signature: t=<seconds>,v1=<hex> |
| Timestamp | x-tocler-timestamp: <unix seconds> | t= inside the signature header |
| Message id | x-tocler-delivery (and body id) | Tocler-Pay-Event-Id (and body id) |
| Event | x-tocler-event: order.paid or test | Tocler-Pay-Event-Type |
| Secret | whsec_… shown once when you add the endpoint | shown once when the webhook is set |
const crypto = require('crypto');
// rawBody = the request body exactly as received (a string / Buffer), NOT re-serialised JSON.
function verifyTocler(rawBody, headers, secret) {
const ts = headers['x-tocler-timestamp'];
const sig = String(headers['x-tocler-signature'] || '');
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('too old');
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error('bad signature');
return JSON.parse(rawBody);
}import hmac, hashlib, json, time
def verify_tocler(raw_body: bytes, headers, secret: str):
ts = headers.get('x-tocler-timestamp', '')
sig = headers.get('x-tocler-signature', '')
if not ts or abs(time.time() - int(ts)) > 300:
raise ValueError('too old')
mac = hmac.new(secret.encode(), f'{ts}.'.encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest('v1=' + mac, sig):
raise ValueError('bad signature')
return json.loads(raw_body)<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TOCLER_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_TOCLER_SIGNATURE'] ?? '';
if ($ts === '' || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, getenv('TOCLER_ENDPOINT_SECRET'));
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
$body = json_decode($raw, true); Secrets
A secret is shown once. If one may have leaked, make a new one: for API delivery press New secret on the endpoint (the old one stops at once).
Answer fast, safely twice
Answer within 10 seconds. If Tocler gets no 2xx answer it tries again later with the same id — so keep the ids you handled and don’t deliver twice.
Was this page helpful?