SUQODocs
User GuideAPI ReferenceSDKs

Verify a webhook signature

Prove a delivery came from SUQO before acting on it.

Always verify before acting on a payload. Anyone can POST to your URL; the signature is what proves the request came from SUQO.

The signature

HMAC-SHA256( key = your signing secret,
             message = "<X-SUQO-Timestamp>" + "." + <raw request body> )

hex-encoded and prefixed with sha256=. It arrives in X-SUQO-Signature, alongside X-SUQO-Timestamp — see Request format for the full header set.

Two rules that matter:

  1. Use the raw request body bytes — exactly as received. Do not parse to JSON and re-serialize; key order and whitespace would change and the signature would not match.
  2. Compare in constant time (hmac.compare_digest, crypto.timingSafeEqual) — not ==.

The timestamp is part of the signed message, so a captured request cannot be replayed under a new time. Reject requests whose timestamp is more than ~5 minutes old.

Store the signing secret as a server-side secret — never in frontend code or a public repo.

Python

import hashlib
import hmac


def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node

const crypto = require("crypto")

function verify(rawBody, signature, timestamp, secret) {
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody) // Buffer — raw bytes
      .digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(signature)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Common signature problems

SymptomLikely cause
Signature never matchesBody was parsed and re-serialized before verifying — use the raw bytes. In Express, a JSON body parser mounted before your route is the usual culprit.
Matches locally, fails in productionWrong secret in that environment, or a proxy/WAF rewriting the body.
Requests rejected as too oldServer clock drift on your side. Sync via NTP.
Security: Treat the signing secret as a password. Never commit it to source control, expose it in client-side code, or paste it into screenshots or support tickets.

On this page