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

# Signature verification

> Why the Boble-Signature header matters, and how to check it before trusting any event.

Anyone on the internet who knows your webhook URL could `POST` you a fake event pretending to be Boble. To protect against this, every Boble delivery is **signed** with a secret only you and Boble know.

Before you act on any event — granting access, sending a welcome email, removing a Discord role — make sure the signature is valid.

## Your signing secret

When you turn on webhooks for the first time, Boble generates a **signing secret** for your tenant. The same secret is used to sign deliveries for every promotion you connect a webhook to. You retrieve it from your dashboard, and you can rotate it at any time.

<Warning>
  Treat the signing secret like a password. Never expose it in client-side code, never commit it to a public repository, and rotate it immediately if it leaks.
</Warning>

## The `Boble-Signature` header

Every delivery carries a header that looks like this:

```http theme={null}
Boble-Signature: t=1717497600,v1=5257a869e7ec...
```

It has two parts, comma-separated:

* `t` — the Unix timestamp in **seconds** when Boble emitted the event. Same value as the `created` field in the body.
* `v1` — the signature: an HMAC-SHA256 in lowercase hex over the string `<t>.<rawBody>`, using your signing secret.

## How to verify

The verification is a three-step recipe:

<Steps>
  <Step title="Extract t and v1 from the header">
    Split the value on `,`, then split each part on `=`.
  </Step>

  <Step title="Recompute the signature on your side">
    Compute `HMAC-SHA256(secret, "<t>.<rawBody>")` and hex-encode it.
  </Step>

  <Step title="Compare in constant time">
    If your recomputed signature matches `v1`, the event is genuine. If not, drop it and reply `401`.
  </Step>
</Steps>

A few subtleties worth knowing:

* The signature is calculated over the **raw bytes** of the body, exactly as Boble sent them. If your framework parses the JSON and re-serializes it before you check the signature, you'll get a different string and verification will fail. Capture the raw body **before** any JSON middleware runs.
* The comparison must be **constant-time**. Don't use a regular string equality — most languages ship a dedicated function for this (`crypto.timingSafeEqual`, `hmac.compare_digest`, `hash_equals`, etc.).
* It's a good idea to also reject events whose `t` is more than a few minutes old (5 minutes is a reasonable default). This blocks replay attacks where someone captures a real signed body and replays it later.

## Rotating the signing secret

You can regenerate the signing secret from your dashboard at any time. The moment you do:

* The **old secret stops working immediately** for new events.
* Any event Boble was about to send is signed with the new secret.

The safe rollout order is:

1. Deploy the new secret to your receiver first.
2. Then trigger the rotation from your dashboard.

If you want a smoother transition, keep both secrets active on your side for a few minutes and accept whichever one matches.
