---
title: "Verifying signatures"
description: "Verify every webhook with HMAC-SHA256 over the raw request body, compared in constant time, and rotate the signing secret without dropping events in flight."
canonical_url: "https://twominutebilling.com/help/developers/webhooks/signatures"
---
# Verifying signatures

> Verify every webhook with HMAC-SHA256 over the raw request body, compared in constant time, and rotate the signing secret without dropping events in flight.

Anyone can reach your webhook URL. The signature is how you tell an event from tmbilling apart
from a request someone else sent to the same address.

## Read the signature header

Every delivery carries:

```http
Tmbilling-Signature: sha256=<hex>
```

The value is `HMAC-SHA256(signing_secret, raw_request_body)`, hex-encoded and prefixed with
`sha256=`. Use the secret tmbilling returned when you created the endpoint.

## Verify the signature

<code-group>

```ts [Node]
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: Buffer, header: string | undefined, secret: string): boolean {
  if (!header) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  // Length must match before timingSafeEqual, which throws on a length mismatch.
  return a.length === b.length && timingSafeEqual(a, b);
}
```

```python [Python]
import hmac, hashlib

def verify(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header:
        return False
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)
```

```go [Go]
func Verify(rawBody []byte, header, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(header))
}
```

</code-group>

## Avoid two mistakes that make this pointless

<caution title="Hash the raw body, not the parsed object">

`JSON.stringify(req.body)` is not the bytes we signed. Key order, whitespace and number
formatting survive our serialisation but not a parse-and-restringify round trip. Verification
then fails for legitimate events, and the usual "fix" is to stop verifying.

Capture the raw body before your JSON middleware touches it. In Express, use
`express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })`. In Fastify, add a
content-type parser that keeps the buffer. In a Next.js route handler, call
`await request.text()` and parse it yourself.

</caution>

<caution title="Compare in constant time">

`expected === received` leaks how much of the signature was correct through how long the
comparison takes, which is enough to forge one given enough attempts. Use `timingSafeEqual`,
`hmac.compare_digest` or `hmac.Equal` — every language has one.

</caution>

## Reject a failed verification

Answer `401` and nothing else. Give no detail about why it failed, and do not process the body.

Don't log the body of an unverified request as if it were an event — it is a stranger's input.

## Rotate the secret

<endpoint method="POST" path="/v1/webhook_endpoints/{id}/rotate_secret">



</endpoint>

tmbilling returns the new secret once.

To rotate without dropping events in flight, accept either secret for a short window. Verify
against the new secret first, then against the old one on failure, until the old deliveries drain.

## Related

- [Delivery and retries](https://twominutebilling.com/help/developers/webhooks/delivery)
- [Security](https://twominutebilling.com/security)


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
