Verify signatures¶
Each request is signed with your signing secret (whsec_…) from
Settings → API and webhooks:
- Sign the timestamp,
., and the raw body bytes. Don't re-encode the JSON. - Compare with
X-Kothok-Signaturein constant time. - Reject timestamps more than five minutes from your clock. Retries are signed again with a fresh timestamp.
Code samples¶
import crypto from "node:crypto";
function verify(rawBody, headers, secret) {
const timestamp = headers["x-kothok-timestamp"];
const ts = Number(timestamp);
if (!timestamp || !Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const given = headers["x-kothok-signature"] || "";
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
With Express, read the raw body with express.raw({ type: "application/json" })
on the webhook route, not express.json().
import hashlib
import hmac
import time
def verify(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("X-Kothok-Timestamp", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, headers.get("X-Kothok-Signature", ""))
With Flask use request.get_data(); with FastAPI or Starlette,
await request.body(); with Django, request.body.
function verify(string $secret, string $ts, string $signature, string $rawBody): bool {
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
$ok = verify(
$secret,
$_SERVER['HTTP_X_KOTHOK_TIMESTAMP'] ?? '',
$_SERVER['HTTP_X_KOTHOK_SIGNATURE'] ?? '',
file_get_contents('php://input')
);
If the check fails, answer 401 and ignore the request.
Rotate signing secret in the dashboard stops the old secret at once. If your tool can't check an HMAC, use an auth header instead.