Skip to content

Verify signatures

Each request is signed with your signing secret (whsec_…) from Settings → API and webhooks:

sha256=hex( HMAC_SHA256( signing_secret, X-Kothok-Timestamp + "." + raw_body ) )
  1. Sign the timestamp, ., and the raw body bytes. Don't re-encode the JSON.
  2. Compare with X-Kothok-Signature in constant time.
  3. 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.

Still need help?

Tell us what you were trying to do and we will help you sort it out.

Contact support