---
title: "Signatures"
description: "Check that each event is from Goomzo before you act on it."
url: "https://docs.goomzo.africa/docs/developers/webhooks/signatures"
docs_index: /llms.txt
lastUpdated: 2026-11-12
section: "Webhooks"
related:
  - /docs/developers/webhooks
  - /docs/developers/webhooks/events
---

> For an index of all documentation, see [/llms.txt](https://docs.goomzo.africa/llms.txt). Any page is also available as Markdown by adding .md to its URL.

# Signatures

> Check that each event is from Goomzo before you act on it.

Anyone can post to your URL, so check that each event is from Goomzo before you act on it. Every delivery is signed
with your endpoint's secret.

## Headers

Webhooks follow [Standard Webhooks](https://www.standardwebhooks.com), an open spec with libraries for most
languages. These headers come with each delivery:

| Header              | Value                                                           |
| ------------------- | --------------------------------------------------------------- |
| `webhook-id`        | The event's `id`. The same on every retry, so use it to dedupe. |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds                     |
| `webhook-signature` | `v1,` and the signature, base64                                 |

## Check the signature

The easiest way is a
Standard Webhooks library, with the endpoint's secret. It starts with `whsec_`; find it in the endpoint under
**Signing secret**.

```js title="Node.js"
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.GOOMZO_WEBHOOK_SECRET)
// Throws if the signature is wrong or the timestamp is more than 5 minutes off.
const event = wh.verify(rawBody, req.headers)
```

```python title="Python"
from standardwebhooks import Webhook

wh = Webhook(os.environ["GOOMZO_WEBHOOK_SECRET"])
event = wh.verify(raw_body, request.headers)
```

To check it yourself:

1. Base64-decode the secret after `whsec_` to get the key.
2. Compute HMAC-SHA256 of `webhook-id`, a dot, `webhook-timestamp`, a dot, and the body exactly as it arrived.
3. Base64-encode it and compare it, in constant time, with each `v1,` value in `webhook-signature`.
4. Reject it if `webhook-timestamp` is more than 5 minutes from now.

## Rotate the secret

Open the endpoint and select **Rotate secret**, or call `POST /v1/webhook-endpoints/{id}/rotate-secret`. You
get a new secret at once, and for the next 24 hours each delivery is signed with both: `webhook-signature` holds
two `v1,` values. Standard Webhooks libraries accept either, so update your receiver any time in that window.
Send `{"keepPreviousForHours": 0}` to stop the old secret at once, for example if it leaked.

## Next steps

- [Events](https://docs.goomzo.africa/docs/developers/webhooks/events?utm_source=llms&utm_medium=ai)
- [Webhooks](https://docs.goomzo.africa/docs/developers/webhooks?utm_source=llms&utm_medium=ai)

---

For an index of all available documentation, see [/llms.txt](https://docs.goomzo.africa/llms.txt)
