Browse help topics

Verify the X-TMV-Signature header

In this article: Confirm a webhook really came from us before you act on it.

Your endpoint URL is a public address. Anyone who learns it can POST whatever they like at it, so verify the signature before you act on a delivery.

The header

X-TMV-Signature: t=1756908311,sha256=a3f1…

Two parts: t is the Unix timestamp we signed at, sha256 is the digest.

How it's computed

HMAC-SHA256 over the timestamp, a full stop, and the raw request body, keyed with your endpoint's signing secret:

HMAC_SHA256(secret, "<t>.<raw body>")

Ruby

t, sig = request.headers["X-TMV-Signature"]
           .split(",").map { |p| p.split("=", 2).last }

expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{t}.#{request.raw_post}")

# Constant-time compare — a plain == leaks the answer a byte at a time.
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)

# Reject anything older than five minutes, so a captured delivery
# can't be replayed at you next week.
head :unauthorized if Time.now.to_i - t.to_i > 300

Node

const [t, sig] = req.get("X-TMV-Signature")
  .split(",").map(p => p.split("=")[1]);

const expected = crypto.createHmac("sha256", SECRET)
  .update(`${t}.${rawBody}`).digest("hex");

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
  return res.sendStatus(401);
}

Careful

Sign the raw body, exactly as received. Parsing the JSON and re-serialising it changes whitespace and key order, and the digest will never match — which is the single most common reason verification "doesn't work".

Two more things

  • Compare in constant time. A plain == returns early on the first differing byte, which is enough to guess a signature given patience.
  • Check the timestamp. A valid signature is valid forever without it, so reject deliveries older than a few minutes.

Lost the secret?

It's shown once and never again. If it's gone, delete the endpoint and create a new one — that issues a fresh secret. Rotate the same way if you think it's leaked.

Related articles

Didn't find what you were looking for?

Send us the question — a person answers, usually the same working day.

Contact support