Verify webhook signature
Every webhook call is signed with the secret of its webhook, so your endpoint can check that the call comes from Interankiety and was not changed on the way.
You get the secret in the secret field when you create or update the webhook. Calling PUT again on an existing webhook returns the same secret, so you can get it back if you lose it. GET endpoints never return it.
Signature header
Section titled “Signature header”Webhook-Signature: t=1760954400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd- t - time the call was sent, as a Unix timestamp (seconds)
- v1 - HMAC-SHA256 of the text
{t}.{body}, keyed with your secret, as lowercase hex.{body}is the raw request body, exactly as received.
How to verify
Section titled “How to verify”- Read the raw request body before parsing the JSON. Parsing and encoding it again can change the bytes.
- Split the header into
tandv1. - Calculate
HMAC-SHA256(secret, t + "." + body)and compare it withv1using a constant-time comparison. - Reject calls where
tis more than 5 minutes away from your server’s time, so a recorded call cannot be sent again later.
Answer with a 2xx code only once the signature is valid.
Example (PHP)
Section titled “Example (PHP)”$body = file_get_contents('php://input');$secret = getenv('WEBHOOK_SECRET'); // whsec_...
parse_str(str_replace(',', '&', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? ''), $signature);$timestamp = (int) ($signature['t'] ?? 0);$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! hash_equals($expected, $signature['v1'] ?? '') || abs(time() - $timestamp) > 300) { http_response_code(401); exit;}
$payload = json_decode($body, true);Example (Node.js)
Section titled “Example (Node.js)”import crypto from 'node:crypto';
// rawBody: the request body as a Buffer, e.g. express.raw({ type: 'application/json' })function isValidWebhook(rawBody, header, secret) { const parts = Object.fromEntries((header ?? '').split(',').map((part) => part.split('='))); const timestamp = Number(parts.t); const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return typeof parts.v1 === 'string' && parts.v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - timestamp) <= 300;}