Signing & Verification
Every webhook delivery includes a signature so your server can verify that the request came from Tolinku and was not modified in transit.
How signing works
Section titled “How signing works”- Tolinku serializes the JSON payload body.
- It computes an HMAC-SHA256 hash using your webhook’s signing secret.
- The hex-encoded hash is sent in the
X-Webhook-Signatureheader. - The event type is sent in the
X-Webhook-Eventheader.
Your server should compute the same HMAC and compare it to the header value.
Verification examples
Section titled “Verification examples”import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// In your Express handler:app.post('/webhooks/tolinku', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const event = req.headers['x-webhook-event'];
if (!verifyWebhook(req.body, signature, process.env.TOLINKU_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
const payload = JSON.parse(req.body); console.log(`Received ${event}:`, payload.data);
res.status(200).send('OK');});import hmacimport hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)
# In your Flask handler:@app.route('/webhooks/tolinku', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') event = request.headers.get('X-Webhook-Event')
if not verify_webhook(request.data, signature, WEBHOOK_SECRET): return 'Invalid signature', 401
payload = request.get_json() print(f'Received {event}:', payload['data'])
return 'OK', 200Rejecting replays
Section titled “Rejecting replays”A valid signature proves a delivery came from Tolinku. It does not prove it is recent. Anyone who captures one delivery, from a proxy log or an error report, can send those exact bytes to your endpoint again and the signature still verifies, because the bytes have not changed.
Every payload carries the moment it was sent:
{ "event": "click.tracked", "timestamp": "2026-08-29T14:31:07.221Z", "data": { }}That field is inside the signed body, so it cannot be altered without breaking the signature. Check it after verifying, and refuse anything old:
const MAX_AGE_MS = 5 * 60 * 1000;
const age = Date.now() - new Date(payload.timestamp).getTime();if (!Number.isFinite(age) || age > MAX_AGE_MS || age < -MAX_AGE_MS) { return res.status(400).send('Stale or misdated webhook');}The negative bound matters as much as the positive one: a timestamp in the future is not a delivery that has not happened yet, it is one whose clock you cannot trust.
Important notes
Section titled “Important notes”- The signature is computed on the raw request body (the exact bytes sent), not on a parsed and re-serialized JSON object. Use the raw body for HMAC computation.
- If your web framework automatically parses JSON, you need to configure it to also preserve the raw body (as shown in the Express example above with
express.raw()). - The signing secret starts with
whsec_. Store it as an environment variable, not in your source code.