---
title: "Accept bounce and complaint events only · Agent Wranglers"
url: https://agentwranglers.se/playbook/email/unsubscribe-and-bounces/verify-delivery-events
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Accept bounce and complaint events only when signed

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Unsubscribe and bounces](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces.md).

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

- ID: EML-73
- Level: Recommended
- Chapter: [Unsubscribe and bounces](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces)
- Rule: Verify the signature on every bounce, complaint and unsubscribe event from your sending service before you act on it. Reject unsigned, altered or replayed events.
- Applies when: The project sends email.
- How: These events suppress addresses and change consent. A forged event can stop password resets to every customer, or put back an address that complained. Use the method your service documents. Most sign the raw body with a shared secret, as [Sign every webhook](https://agentwranglers.se/playbook/api-design/webhooks-and-events/signed-webhooks) describes. Others sign with a certificate, so accept certificate addresses only on the service's own domain. Reject events whose timestamp falls outside the window in [Sign every webhook](https://agentwranglers.se/playbook/api-design/webhooks-and-events/signed-webhooks), and store event IDs to drop repeats.
- Check: Replay a recorded bounce event with one byte of the body changed. The endpoint answers 4xx and the address stays active. The original event, sent again, causes no second change.

## 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.

### JavaScript example

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

const MAX_EVENT_AGE_SECONDS = 300;

function isGenuineEvent(rawBody, timestamp, signature, secret) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_EVENT_AGE_SECONDS) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const given = Buffer.from(signature, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

### Python example

```python
import hashlib, hmac, time

MAX_EVENT_AGE_SECONDS = 300

def is_genuine_event(raw_body: bytes, timestamp: str, signature: str, secret: bytes) -> bool:
    if abs(time.time() - int(timestamp)) > MAX_EVENT_AGE_SECONDS:
        return False
    expected = hmac.new(secret, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

## Sources

- [Amazon SNS, Verifying the signature of a message](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message-verify-message-signature.html)

## Related rules

- [Sign every webhook](https://agentwranglers.se/playbook/api-design/webhooks-and-events/signed-webhooks) in API design

**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/email/unsubscribe-and-bounces/verify-delivery-events">Agent Wranglers email playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Stop at the first complaint](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces/suppress-complaints)
- Next: [Label list mail](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces/list-id)
- [Unsubscribe and bounces](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces)
