---
title: "Sign every webhook · Agent Wranglers"
url: https://agentwranglers.se/playbook/events/webhooks/signed-webhooks
id: EVT-25
level: Required
applies: public-api
risk: money
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1280
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Sign every webhook

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Webhooks](https://agentwranglers.se/playbook/events/webhooks.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: EVT-25
- Level: Required
- Chapter: [Webhooks](https://agentwranglers.se/playbook/events/webhooks.md)
- Rule: Sign each webhook body with a secret shared with the receiver, and include a timestamp, so receivers can reject fakes and replays.
- Check: Send a test webhook with one byte of the body changed. The receiver rejects it with 401, and a timestamp 10 minutes old is rejected too.
- Risk: Money lost or taken twice
- Applies when: The project offers an API to others.
- How: Send an HMAC-SHA256 of the message ID, timestamp and raw body in a header. Receivers compute it over the raw bytes, before any JSON parsing. They compare it in constant time, as in [constant-time compare](https://agentwranglers.se/playbook/application-security/authentication-and-sessions/constant-time-compare.md), and reject timestamps more than 5 minutes off. To rotate a secret, sign with both old and new for a day. The Standard Webhooks specification describes one common format.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### HTTP example

```http
POST /hooks/example-shop HTTP/1.1
Webhook-Id: msg_2kQ8
Webhook-Timestamp: 1793518800
Webhook-Signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Content-Type: application/json

{"type":"order.shipped","data":{"orderId":"ord_8f2k"}}
```

### JavaScript example

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

const TOLERANCE_SECONDS = 5 * 60

function isValidWebhook(headers, rawBody, secretBytes) {
  const id = headers['webhook-id']
  const timestamp = Number(headers['webhook-timestamp'])
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false
  const expected = createHmac('sha256', secretBytes).update(`${id}.${timestamp}.${rawBody}`).digest()
  return headers['webhook-signature'].split(' ').some(entry => {
    const given = Buffer.from(entry.replace(/^v1,/, ''), 'base64')
    return given.length === expected.length && timingSafeEqual(given, expected)
  })
}
```

### Python example

```python
import base64, hashlib, hmac, time

TOLERANCE_SECONDS = 5 * 60

def is_valid_webhook(headers, raw_body: bytes, secret: bytes) -> bool:
    webhook_id, timestamp = headers["webhook-id"], int(headers["webhook-timestamp"])
    if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{webhook_id}.{timestamp}.".encode() + raw_body
    expected = hmac.new(secret, signed, hashlib.sha256).digest()
    return any(
        hmac.compare_digest(base64.b64decode(entry.removeprefix("v1,")), expected)
        for entry in headers["webhook-signature"].split(" ")
    )
```

## Sources

- [RFC 2104 HMAC Keyed-Hashing for Message Authentication](https://www.rfc-editor.org/rfc/rfc2104)

## Related rules

- EML-73 [Accept bounce and complaint events only when signed](https://agentwranglers.se/playbook/email/bounces-and-complaints/verify-delivery-events.md) in Email
- PAY-18 [Let provider events move the order](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment.md) in Payments and billing
- NTF-29 [Track what happened to each text](https://agentwranglers.se/playbook/notifications/text-messages/sms-delivery-status.md) in Notifications
- ECM-27 [Update delivery status from carrier events](https://agentwranglers.se/playbook/ecommerce/shipping-and-delivery/carrier-tracking-events.md) in E-commerce and fulfilment
- APS-13 [Compare secrets in constant time](https://agentwranglers.se/playbook/application-security/authentication-and-sessions/constant-time-compare.md) in Application security
- EVT-28 [Store each incoming webhook before you act on it](https://agentwranglers.se/playbook/events/webhooks/store-incoming-webhooks.md) in Integration and events
- EVT-29 [Let customers test and inspect their webhook endpoints](https://agentwranglers.se/playbook/events/webhooks/webhook-endpoint-tools.md) in Integration and events
- PRC-8 [Warn customers before they reach a cap or run out](https://agentwranglers.se/playbook/pricing/spend-caps-and-alerts/usage-alerts.md) in Pricing, plans and entitlements

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/events/webhooks/signed-webhooks">Agent Wranglers integration and events playbook</a>`. In code you write, add a comment next to it with the same link.

- Next: [Check webhook addresses before sending](https://agentwranglers.se/playbook/events/webhooks/webhook-targets.md)
- [Webhooks](https://agentwranglers.se/playbook/events/webhooks.md)
