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

# Signature verification

> Check that a webhook delivery really came from Bullionwale and was not changed or replayed.

> [!WARNING]
>
> **Not available yet.** Webhooks are being built and are not part of the API today: there is no way to subscribe yet, and no events are sent. This page describes the planned design so you can prepare; details may change before release.

Every delivery will be signed with your endpoint's **signing secret** using HMAC-SHA256. Verify the signature before you trust anything in the body.

## The signature header

```text theme={null}
X-Bullionwale-Signature: t=1792432356,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` is the time the delivery was signed, in Unix seconds.
* `v1` is the hex-encoded HMAC-SHA256 of the **signed payload**, keyed with your signing secret.

The signed payload is the timestamp, a dot, and the **raw request body**, byte for byte:

```text theme={null}
<t>.<raw body>
```

## Verify it

1. Read the raw body **before** parsing it. Re-serialising parsed JSON changes the bytes and breaks the signature.
2. Split the header into `t` and `v1`.
3. Compute HMAC-SHA256 over `t + "." + raw body` with your signing secret, and hex-encode it.
4. Compare it with `v1` using a **constant-time** comparison.
5. Reject the delivery if `t` is too far from your clock (for example more than 5 minutes), so an old delivery cannot be replayed.

```python theme={null}
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(",") if "=" in item)
    t, sig = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

Answer `401` or `400` to a delivery that fails verification, and do not act on it.

## Rotating the signing secret

You will be able to ask for a new signing secret for an endpoint. Like an API key, it is shown once. Switch your verifier to the new secret as soon as you receive it.


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