Agent skill
Notifuse Webhooks Skill
Receive and verify Notifuse webhooks (open-source email marketing and transactional email platform), signed per Standard Webhooks. Use when setting up a Notifuse webhook subscription endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling events like contact.created, list.subscribed, segment.joined, email.delivered, email.bounced and custom_event.created.
Install this skill
npx skills add hookdeck/webhook-skills --skill notifuse-webhooks
Notifuse 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.bouncedorlist.unsubscribedevents? - Why is my Notifuse
webhook-signatureverification 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.
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 (HMAC-SHA256 over
{id}.{timestamp}.{raw body}, everyv1,entry compared in constant time) where the window can be dropped or widened.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Envelope
{ "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 and Notifuse's webhook docs.
Delivery Semantics
- Return a
2xxwithin 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).
429never counts as a failure.410 Goneretires 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
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
# 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 - Event catalog, payload fields, retries and disabling
- references/setup.md - Create a subscription, filters, signing secret, rotation, testing
- references/verification.md - Library and manual verification, gotchas, debugging