# Rotate secrets

> Give a webhook a new signing secret without missing a delivery. For 24 hours BlueBot signs with both the old and the new one, so you can deploy the change at your own pace.

Each webhook has its own signing secret, `whsec_…`, shown once when you make it. Rotate it when it may have leaked (pasted into a chat, committed to a repository, known to somebody who left), when you've lost it, or on a schedule your security policy sets.

## What rotating does

- BlueBot makes a new secret and shows it only once. The API returns it in its answer, and the dashboard shows it on screen.
- For the next **24 hours**, every delivery is signed with both secrets. `webhook-signature` carries two values, the new secret's first: `v1,<new> v1,<old>`.
- The webhook's `previous_secret_expires_at` says when the old secret stops signing. After that, only the new one does.
- Nothing else changes: the address, the events and the deliveries waiting are the same.

Because each delivery carries a signature from each secret during those 24 hours, a handler that knows only the old secret and a handler that knows only the new one both accept it. That's what makes the switch safe: you never need both secrets in your code.

## Rotate without missing a delivery

### Rotate

In **Developers → Webhooks**, open the webhook and press **Rotate secret**, or call the API:

```bash
curl -X POST "https://usebluebot.com/api/v1/webhooks/wh_1Hq2LxW9cVbN3mKpRtYs0a/rotate-secret" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Idempotency-Key: rotate-wh_1Hq2LxW9cVbN3mKpRtYs0a-2026-11"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const webhook = await client.webhooks.rotateSecret('wh_1Hq2LxW9cVbN3mKpRtYs0a', { idempotencyKey: 'rotate-wh_1Hq2LxW9cVbN3mKpRtYs0a-2026-11' });
console.log(webhook);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

webhook = client.webhooks.rotate_secret(
    "wh_1Hq2LxW9cVbN3mKpRtYs0a",
    idempotency_key="rotate-wh_1Hq2LxW9cVbN3mKpRtYs0a-2026-11",
)
print(webhook)
```

The answer, `200`:

```json
{
  "id": "wh_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "webhook",
  "url": "https://bookings.example.com/bluebot/webhooks",
  "status": "active",
  "previous_secret_expires_at": "2026-11-14T19:41:00.000Z",
  "secret": "whsec_Q2nVb8mLx0RkT4cW9yHj3PdZs6EuAa1F"
}
```

Copy `secret`. It isn't shown again.

### Put the new secret in your environment

Set `BLUEBOT_WEBHOOK_SECRET` to the new value wherever your handler runs: your host's environment settings, your secrets manager, every region and every worker that verifies.

### Deploy

Deploy as you normally would. Servers still on the old secret and servers already on the new one both accept every delivery while the window is open, so a rolling deploy is fine.

### Check it

Press **Send a test event** on the webhook, or call `POST /v1/webhooks/{id}/test`. Your handler answers `2xx` with the new secret in place.

| You got                                                | It means                                                                                                        | Do next                                                                                                      |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `last_status: 204` (or any `2xx`) on the test delivery | Your handler verifies with a secret BlueBot signs with.                                                         | Make sure every server has the new secret before `previous_secret_expires_at`.                               |
| `last_status: 400`                                     | Your handler rejected the signature. The new value didn't reach it, or it was pasted with a space or cut short. | Copy the secret again from where you saved it. If it's lost, rotate again, but read the warning below first. |

### Let the old secret go

When `previous_secret_expires_at` passes, BlueBot signs with the new secret only. Delete the old value from anywhere you kept it.

> **Rotate once, then wait.** Rotating again inside the 24 hours ends the original secret at once: the new pair is the second and the third secret, and the first stops signing straight away. A server still on the first secret then rejects every delivery. If you need to rotate twice, finish the first switch before you start the second. Sending an `Idempotency-Key` stops a retried request from rotating twice by accident.

## If the secret leaked

Anybody with the old secret can sign posts your handler accepts for as long as your handler trusts that secret. BlueBot's 24-hour window only decides which secrets BlueBot signs with; your handler decides which it accepts. So after a leak, put the new secret in place as soon as you can. Once your handler verifies with the new secret only, a post signed with the leaked one fails your check, even though BlueBot still adds its own old-secret signature for the rest of the day.

Then look for posts that arrived during the leak and don't match an event BlueBot sent: every genuine event is in the [event feed](https://usebluebot.com/docs/api/events/list-events) under its `id`.

## Lost the secret?

BlueBot shows a secret once and can't show it again. Rotate to get a new one. If nothing verifies with the old secret any more, the 24-hour window doesn't matter to you, and you can deploy the new secret whenever it's ready; deliveries that fail meanwhile are [retried for 72 hours](https://usebluebot.com/docs/webhooks/retries).

## Next

- [Verify signatures](https://usebluebot.com/docs/webhooks/verify): How the signature is made and checked.
- [Test webhooks](https://usebluebot.com/docs/webhooks/testing): Send a signed test event to your handler.
