Verify webhook signatures

Authenticate ThriveDesk webhook requests with X-TD-SIGNATURE and constant-time comparison.

Every webhook includes X-TD-SIGNATURE. ThriveDesk computes a base64-encoded HMAC-SHA1 using the webhook secret and the JSON-encoded data value from the request body.

Verification contract

  1. Read the request body and X-TD-SIGNATURE header.
  2. JSON-encode the body’s data value without changing its key order or character escaping.
  3. Compute binary HMAC-SHA1 with the webhook secret.
  4. Base64-encode the result.
  5. Compare the supplied and expected signatures in constant time.
  6. Reject a missing or mismatched signature before processing the event.

JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(body, suppliedSignature, secret) {
  const expected = createHmac('sha1', secret)
    .update(JSON.stringify(body.data))
    .digest('base64');

  const supplied = Buffer.from(suppliedSignature ?? '', 'utf8');
  const calculated = Buffer.from(expected, 'utf8');

  return supplied.length === calculated.length &&
    timingSafeEqual(supplied, calculated);
}

PHP

<?php
function verifyThriveDeskWebhook(array $body, string $signature, string $secret): bool
{
    $data = json_encode($body['data']);
    $expected = base64_encode(hash_hmac('sha1', $data, $secret, true));

    return hash_equals($expected, $signature);
}

Python

import base64
import hashlib
import hmac
import json

def verify_webhook(body: dict, supplied: str, secret: str) -> bool:
    data = json.dumps(body["data"], separators=(",", ":"), ensure_ascii=False)
    digest = hmac.new(secret.encode(), data.encode(), hashlib.sha1).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(expected, supplied)

Preserve serialization

JSON whitespace, key order, slash escaping, and non-ASCII escaping can change the signed bytes. Test your runtime against a real delivery before launch and retain the original request for debugging without logging the secret.

Errors

Return 401 for a missing signature and 403 for a mismatch. Do not enqueue or log sensitive payload fields until verification succeeds.