Skip to content

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.

  1. Tolinku serializes the JSON payload body.
  2. It computes an HMAC-SHA256 hash using your webhook’s signing secret.
  3. The hex-encoded hash is sent in the X-Webhook-Signature header.
  4. The event type is sent in the X-Webhook-Event header.

Your server should compute the same HMAC and compare it to the header value.

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');
});

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.

  • 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.