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

HeaderValue
webhook-idUnique delivery ID
webhook-timestampUnix seconds of this delivery attempt
webhook-signaturev1,<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}, every v1, 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).

EventFired when
contact.created / contact.updated / contact.deletedA contact is added / changed / permanently deleted
list.subscribed / list.unsubscribed / list.confirmed / list.resubscribedSubscription state changes (confirmed = double opt-in)
list.bounced / list.complained / list.pending / list.removedBounce, spam complaint, awaiting confirmation, removal
segment.joined / segment.leftA contact starts / stops matching a segment
email.sent / email.delivered / email.opened / email.clickedEmail lifecycle and engagement
email.bounced / email.complained / email.unsubscribedDelivery failure, spam report, unsubscribe link
custom_event.created / custom_event.updated / custom_event.deletedA custom event is recorded / updated / soft-deleted

Full payloads: references/overview.md and Notifuse's webhook docs.

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

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →