> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usertick.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify signatures

> Check that a delivery came from Usertick before acting on it.

Usertick signs every delivery the [Standard Webhooks](https://www.standardwebhooks.com/) way. Three headers carry the signature data:

| Header | Contents |
| - | - |
| `webhook-id` | The message id, the same on every retry and resend |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds |
| `webhook-signature` | `v1,` and the base64 signature. During a [secret rotation](https://docs.usertick.com/developers/webhooks/rotation.md) it holds two, separated by a space. |

The signature is an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is your secret without its `whsec_` prefix, base64 decoded.

To verify a delivery:

1. To block replayed requests, refuse a timestamp more than 5 minutes away from your server's clock.
2. Compute the signature over the id, the timestamp and the **raw** body.
3. Accept the delivery when any `v1` signature in the header matches the computed one, comparing in constant time.

> [!WARNING]
>
> Sign the raw bytes of the body exactly as they arrived, before any JSON parsing, because parsed and re-serialized JSON rarely matches byte for byte. If your framework parses the body for you, keep a raw copy.

```js Node.js (Express) theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.USERTICK_WEBHOOK_SECRET; // whsec_...
const KEY = Buffer.from(SECRET.slice("whsec_".length), "base64");
const TOLERANCE_SECONDS = 5 * 60;

// express.raw keeps the body as a Buffer, byte for byte.
app.post("/hooks/usertick", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id");
  const timestamp = req.header("webhook-timestamp");
  const signatures = req.header("webhook-signature") ?? "";
  if (!id || !timestamp) return res.status(400).end();

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!(age <= TOLERANCE_SECONDS)) return res.status(401).end();

  const expected = crypto
    .createHmac("sha256", KEY)
    .update(`${id}.${timestamp}.`)
    .update(req.body)
    .digest("base64");

  const valid = signatures.split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    return (
      version === "v1" &&
      signature?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
  });
  if (!valid) return res.status(401).end();

  res.status(200).end(); // answer first, then do the work
  const event = JSON.parse(req.body.toString("utf8"));
  console.log(req.header("X-Usertick-Event"), event.id);
});
```

```python Python (Flask) theme={null}
import base64, hashlib, hmac, os, time
from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["USERTICK_WEBHOOK_SECRET"]  # whsec_...
KEY = base64.b64decode(SECRET.removeprefix("whsec_"))
TOLERANCE_SECONDS = 5 * 60

@app.post("/hooks/usertick")
def usertick_hook():
    msg_id = request.headers.get("webhook-id", "")
    timestamp = request.headers.get("webhook-timestamp", "")
    signatures = request.headers.get("webhook-signature", "")
    if not msg_id or not timestamp.isdigit():
        return "", 400
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return "", 401

    signed = f"{msg_id}.{timestamp}.".encode() + request.get_data()  # raw bytes
    expected = base64.b64encode(hmac.new(KEY, signed, hashlib.sha256).digest()).decode()

    valid = any(
        version == "v1" and hmac.compare_digest(signature, expected)
        for version, _, signature in (entry.partition(",") for entry in signatures.split(" "))
    )
    if not valid:
        return "", 401

    event = request.get_json()
    print(request.headers.get("X-Usertick-Event"), event["id"])
    return "", 200
```

> [!TIP]
>
> The Standard Webhooks project publishes verification libraries for Node, Python, Go and other languages. They take the `whsec_` secret and the three headers as they are, for example `new Webhook(secret).verify(rawBody, headers)` in the `standardwebhooks` npm package.

## Verification rules

* **Compare in constant time** (`timingSafeEqual`, `compare_digest`), never with `==`.
* **Check the timestamp.** Without the check, a captured delivery could be sent again later and still verify.
* **Keep one secret per webhook on your server.** The dashboard shows the secret when a webhook is created or its secret is rotated, and the API returns it in the [subscribe](https://docs.usertick.com/developers/api/reference/subscribe-webhook.md) response.
* **Refuse a mismatch with 401** and don't process the body. A mismatch points to a bug in raw body handling, or to someone else calling your endpoint.
* **Drop duplicates by `webhook-id`.** It stays the same on every retry and resend of a delivery.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.