# Notifuse Webhooks

[Notifuse](https://www.notifuse.com) is an open-source, self-hostable email marketing and transactional email platform. Its outgoing webhook subscriptions report contact, list, segment, email and custom-event changes. Every delivery is signed per **Standard Webhooks** with `webhook-id`, `webhook-timestamp` and `webhook-signature` headers.

## When to Use This Skill

* How do I receive Notifuse webhooks?
* How do I verify a Notifuse webhook signature?
* Where do I find my Notifuse webhook signing secret?
* How do I handle `contact.created`, `email.bounced` or `list.unsubscribed` events?
* Why is my Notifuse `webhook-signature` verification failing?
* Why do Notifuse webhooks fail verification when retried through Hookdeck?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Unique delivery ID |
| `webhook-timestamp` | Unix seconds of this delivery attempt |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>`, possibly several space-separated entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. The secret looks like `whsec_<base64>` and the key is the base64-**decoded** part after `whsec_`. Each subscription has its own secret. The examples use the `standardwebhooks` library (npm, PyPI), which compares in constant time against every `v1,` entry.

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body string, NOT re-serialised JSON
function verifyNotifuseWebhook(rawBody, get, secret = process.env.NOTIFUSE_WEBHOOK_SECRET) {
  // Notifuse sends webhook-*; fall back to svix-* when webhook-id is absent
  const p = get('webhook-id') ? 'webhook' : 'svix';
  return new Webhook(secret).verify(rawBody, {
    'webhook-id': get(`${p}-id`),
    'webhook-timestamp': get(`${p}-timestamp`),
    'webhook-signature': get(`${p}-signature`),
  }); // returns the parsed event; throws → respond 400
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), {...same mapping...})`.

> **Timestamp window.** The library always enforces a fixed 5-minute window. **Hookdeck retries and replays resend the original timestamp, so the library rejects late retries after 5 minutes.** If you receive through Hookdeck, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/notifuse-webhooks/references/verification.md) (HMAC-SHA256 over `{id}.{timestamp}.{raw body}`, every `v1,` entry compared in constant time) where the window can be dropped or widened.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/notifuse-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/notifuse-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/notifuse-webhooks/examples/fastapi/).

## Event Envelope

```json
{ "id": "delivery_abc123", "type": "contact.created", "timestamp": "2026-10-26T10:30:00Z",
  "workspace_id": "ws_1234567890",
  "data": { "id": "contact_12345", "email": "user@example.com", "first_name": "John" } }

```

`id` identifies the delivery and `timestamp` is ISO 8601. Dedupe on the `webhook-id` header: Notifuse says webhooks may rarely arrive twice.

## Common Event Types

The docs list 23 events (the list may not be exhaustive).

| Event | Fired when |
| --- | --- |
| `contact.created` / `contact.updated` / `contact.deleted` | A contact is added / changed / permanently deleted |
| `list.subscribed` / `list.unsubscribed` / `list.confirmed` / `list.resubscribed` | Subscription state changes (confirmed = double opt-in) |
| `list.bounced` / `list.complained` / `list.pending` / `list.removed` | Bounce, spam complaint, awaiting confirmation, removal |
| `segment.joined` / `segment.left` | A contact starts / stops matching a segment |
| `email.sent` / `email.delivered` / `email.opened` / `email.clicked` | Email lifecycle and engagement |
| `email.bounced` / `email.complained` / `email.unsubscribed` | Delivery failure, spam report, unsubscribe link |
| `custom_event.created` / `custom_event.updated` / `custom_event.deleted` | A custom event is recorded / updated / soft-deleted |

> **Full payloads:** [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/notifuse-webhooks/references/overview.md) and [Notifuse's webhook docs](https://docs.notifuse.com/features/webhooks).

## Delivery Semantics

* Return a `2xx` within **30 seconds**. Enqueue the event and process it asynchronously.
* Up to 10 retries over about 34 hours (immediate, 30 s, 1 m, 2 m, 5 m, 15 m, 30 m, 1 h, 2 h, 6 h, final 24 h).
* `429` never counts as a failure. `410 Gone` retires the subscription at once.
* 20 consecutive failures spanning more than 2 hours disable the subscription, and events while it is disabled are dropped, not replayed.
* The URL must be publicly reachable: private, loopback and metadata addresses are refused (self-hosted can set `WEBHOOK_DELIVERY_ALLOW_PRIVATE_HOSTS=true`).

## Environment Variables

```bash
NOTIFUSE_WEBHOOK_SECRET=whsec_xxxxx   # the full whsec_ value; handlers fail closed (500) without it

```

Notifuse generates the secret when you create the subscription; only the workspace owner can see it. Rotate it with `webhookSubscriptions.regenerateSecret` (the old one stops working at once).

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 notifuse --path /webhooks/notifuse

```

Use the printed URL as the subscription URL, then fire a sample with `webhookSubscriptions.test`.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/notifuse-webhooks/references/overview.md) - Event catalog, payload fields, retries and disabling
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/notifuse-webhooks/references/setup.md) - Create a subscription, filters, signing secret, rotation, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/notifuse-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging