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

# Verifying webhooks

> Check the signature header before you act on a delivery.

Your webhook URL is a public endpoint. Anything on the internet can POST to it, so
verify before you credit a balance or release goods.

## The `flick-signature` header

Every delivery carries your **webhook secret** in a header:

```http theme={null}
flick-signature: <your webhook secret>
```

Compare it against the secret stored for your account. Use a constant-time
comparison so a wrong value cannot be guessed a byte at a time.

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

const SECRET = process.env.FLICK_WEBHOOK_SECRET;

function verify(req) {
  const sent = String(req.headers["flick-signature"] || "");
  const a = Buffer.from(sent);
  const b = Buffer.from(SECRET);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/flick", express.json(), (req, res) => {
  if (!verify(req)) return res.sendStatus(401);
  // store req.body.data.Id, answer immediately, process after
  res.sendStatus(200);
});
```

```python Python theme={null}
import hmac, os
from flask import request, abort

SECRET = os.environ["FLICK_WEBHOOK_SECRET"]

def verify():
    sent = request.headers.get("flick-signature", "")
    if not hmac.compare_digest(sent, SECRET):
        abort(401)
```

> [!WARNING]
>
> `flick-signature` is a **shared secret presented verbatim**, not a signature
> computed over the request body. Two consequences you must design around:
>
> * It does not prove the body is unmodified, so trust the amount in the body only
>   after you have re-read the transaction with [Transaction status](https://docs.getflick.co/api-reference).
> * It contains no timestamp, so a captured delivery can be replayed indefinitely.
>   Deduplicate on `data.Id` and ignore an `Id` you have already settled.

## Also restrict by transport

Because the header alone is replayable, pair it with at least one of:

* a long, unguessable path on your webhook URL
* an allowlist of Flick's egress addresses, if your infrastructure supports it
* mutual TLS, if you operate your own ingress

## HMAC-signed events

Some newer event types are signed properly rather than carrying the secret:

```http theme={null}
x-flick-timestamp: 1762000000
x-flick-signature: <hex HMAC-SHA512>
```

The signature is `HMAC-SHA512(secret, timestamp + "." + rawBody)`, hex encoded.
Verify over the **raw request body**, before any JSON parsing or re-serialisation —
re-encoding changes the bytes and the signature will not match. Reject a timestamp
that is more than a few minutes old to stop replays.

```js theme={null}
const expected = crypto
  .createHmac("sha512", SECRET)
  .update(req.headers["x-flick-timestamp"] + ".")
  .update(rawBody)
  .digest("hex");
```

> [!NOTE]
>
> If a delivery carries `x-flick-signature`, prefer it — it is strictly stronger than
> `flick-signature`. Accept either, and record which one you verified.


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