Skip to main content

Overview

Notifuse handles two types of webhooks:
  1. Incoming webhooks - Callbacks from email providers (Amazon SES, Mailgun, Postmark, etc.) that notify Notifuse about email events like bounces, complaints, and deliveries. These are configured automatically when you set up an email integration.
  2. Outgoing webhooks - HTTP callbacks that Notifuse sends to your server when events occur. This is what this feature page covers.
Outgoing webhooks allow you to receive HTTP POST requests to your server when specific events occur in Notifuse. This enables you to:
  • Sync contact data to your CRM or other systems
  • Trigger workflows in external automation tools
  • Build custom integrations and dashboards
  • React to email engagement events in real-time
Outgoing webhooks screenshot

Creating a Webhook Subscription

Create a webhook subscription via the API or the Notifuse console:
curl -X POST "https://your-instance.com/api/webhookSubscriptions.create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "your_workspace",
    "name": "Production Webhook",
    "url": "https://api.example.com/webhooks/notifuse",
    "event_types": ["contact.created", "contact.updated", "email.sent"]
  }'

Available Event Types

Contact Events

EventDescription
contact.createdA new contact was added to the workspace
contact.updatedContact data was modified
contact.deletedA contact was permanently deleted

List Events

EventDescription
list.subscribedContact subscribed to a list
list.unsubscribedContact unsubscribed from a list
list.confirmedContact confirmed double opt-in subscription
list.resubscribedPreviously unsubscribed contact resubscribed
list.bouncedEmail to contact bounced
list.complainedContact marked email as spam
list.pendingContact subscription is awaiting confirmation
list.removedContact was removed from list

Segment Events

EventDescription
segment.joinedContact entered a segment (matched segment criteria)
segment.leftContact left a segment (no longer matches criteria)

Email Events

EventDescription
email.sentEmail was sent to contact
email.deliveredEmail delivery was confirmed by the recipient’s server
email.openedContact opened the email
email.clickedContact clicked a link in the email
email.bouncedEmail delivery failed
email.complainedContact marked email as spam
email.unsubscribedContact unsubscribed via email link

Custom Events

EventDescription
custom_event.createdA new custom event was recorded
custom_event.updatedA custom event was updated
custom_event.deletedA custom event was soft-deleted

Custom Event Filters

For custom events, you can optionally filter by goal_types and event_names:
{
  "event_types": ["custom_event.created"],
  "custom_event_filters": {
    "goal_types": ["purchase", "subscription"],
    "event_names": ["orders/fulfilled", "payment.succeeded"]
  }
}

Payload Structure

All webhook payloads follow this structure:
{
  "id": "delivery_abc123",
  "type": "contact.created",
  "timestamp": "2024-01-15T10:30:00Z",
  "workspace_id": "ws_1234567890",
  "data": {
    // Event-specific data
  }
}
FieldDescription
idUnique identifier for the webhook delivery
typeThe event type that triggered the webhook
timestampISO 8601 timestamp when the webhook was sent
workspace_idThe workspace where the event occurred
dataEvent-specific payload data

Example: Contact Created

{
  "id": "delivery_abc123",
  "type": "contact.created",
  "timestamp": "2024-01-15T10:30:00Z",
  "workspace_id": "ws_1234567890",
  "data": {
    "email": "[email protected]",
    "id": "contact_12345",
    "external_id": "ext_67890",
    "first_name": "John",
    "last_name": "Doe",
    "tags": ["newsletter", "customer"],
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Example: Email Sent

{
  "id": "delivery_def456",
  "type": "email.sent",
  "timestamp": "2024-01-15T10:30:00Z",
  "workspace_id": "ws_1234567890",
  "data": {
    "message_id": "msg_xyz789",
    "email": "[email protected]",
    "subject": "Welcome to our newsletter",
    "broadcast_id": "bc_abc123",
    "template_id": "welcome_email",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Example: Email Opened

{
  "id": "delivery_ghi789",
  "type": "email.opened",
  "timestamp": "2024-01-15T11:45:00Z",
  "workspace_id": "ws_1234567890",
  "data": {
    "message_id": "msg_xyz789",
    "email": "[email protected]",
    "subject": "Welcome to our newsletter",
    "broadcast_id": "bc_abc123",
    "template_id": "welcome_email",
    "created_at": "2024-01-15T11:45:00Z"
  }
}

Example: Segment Joined

{
  "id": "delivery_jkl012",
  "type": "segment.joined",
  "timestamp": "2024-01-15T10:30:00Z",
  "workspace_id": "ws_1234567890",
  "data": {
    "email": "[email protected]",
    "contact_id": "contact_12345",
    "segment_id": "seg_vip_customers",
    "segment_name": "VIP Customers",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Verifying Webhook Signatures

Notifuse signs all webhook payloads using the Standard Webhooks specification with HMAC-SHA256.

Headers

Each webhook request includes these headers:
HeaderDescription
webhook-idUnique identifier for the webhook delivery
webhook-timestampUnix timestamp when the webhook was sent
webhook-signatureHMAC-SHA256 signature of the payload

Verifying the Signature

  1. Extract the timestamp and signature from the headers
  2. Construct the signed payload: {webhook-id}.{webhook-timestamp}.{body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare with the provided signature

Code Examples

const crypto = require('crypto');

function verifyWebhook(payload, headers, secret) {
  const msgId = headers['webhook-id'];
  const msgTimestamp = headers['webhook-timestamp'];
  const msgSignature = headers['webhook-signature'];

  // Check timestamp is within 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(msgTimestamp)) > 300) {
    throw new Error('Timestamp too old');
  }

  // Construct signed payload
  const signedPayload = `${msgId}.${msgTimestamp}.${payload}`;

  // Compute signature
  const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64');
  const expectedSignature = crypto
    .createHmac('sha256', secretBytes)
    .update(signedPayload)
    .digest('base64');

  // Extract v1 signature
  const signatures = msgSignature.split(' ');
  for (const sig of signatures) {
    const [version, signature] = sig.split(',');
    if (version === 'v1' && signature === expectedSignature) {
      return true;
    }
  }

  throw new Error('Invalid signature');
}

Retry Behavior

Notifuse automatically retries failed webhook deliveries with an exponential backoff strategy.

Retry Schedule

Webhooks are retried up to 10 times over approximately 34 hours:
AttemptDelay After Previous
1Immediate
230 seconds
31 minute
42 minutes
55 minutes
615 minutes
730 minutes
81 hour
92 hours
106 hours
Final24 hours

Success Criteria

A webhook delivery is considered successful if your endpoint returns HTTP status code 2xx (200-299).

Failure Conditions

A delivery attempt fails if:
  • Your endpoint returns a non-2xx status code
  • The request times out (30 second timeout)
  • The endpoint is unreachable (DNS error, connection refused, etc.)

Best Practices

Return 2xx Quickly

Return a successful response as quickly as possible. Process the webhook payload asynchronously if needed:
// Good: Acknowledge quickly, process later
app.post('/webhooks/notifuse', async (req, res) => {
  // Quickly validate signature
  verifyWebhook(req.body, req.headers, SECRET);

  // Queue for async processing
  await queue.add('process-webhook', req.body);

  // Respond immediately
  res.status(200).json({ received: true });
});

Handle Duplicate Deliveries

Webhooks may be delivered more than once in rare cases. Use the webhook-id header to implement idempotency:
app.post('/webhooks/notifuse', async (req, res) => {
  const webhookId = req.headers['webhook-id'];

  // Check if we've already processed this webhook
  if (await hasProcessed(webhookId)) {
    return res.status(200).json({ received: true, duplicate: true });
  }

  // Mark as processed before handling
  await markProcessed(webhookId);

  // Process the webhook
  await processWebhook(req.body);

  res.status(200).json({ received: true });
});

Verify Signatures

Always verify webhook signatures in production to ensure requests are from Notifuse.

Monitor Delivery Failures

Use the Notifuse console or API to monitor webhook delivery status:
curl "https://your-instance.com/api/webhookSubscriptions.deliveries?workspace_id=ws_123&subscription_id=whsub_456" \
  -H "Authorization: Bearer YOUR_API_KEY"

Testing Webhooks

Send a Test Webhook

Use the test endpoint to verify your webhook configuration:
curl -X POST "https://your-instance.com/api/webhookSubscriptions.test" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "your_workspace",
    "id": "whsub_a1b2c3d4e5f6",
    "event_type": "contact.created"
  }'
The event_type parameter is optional. If provided, the test webhook will include a realistic sample payload for that event type. If omitted, a generic test payload is sent.

Local Development

For local development, use tools like ngrok or localtunnel to expose your local server:
# Start ngrok
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/notifuse

Notes

  • Webhooks require HTTPS endpoints in production
  • The signing secret is generated automatically when you create a subscription
  • Use webhookSubscriptions.regenerateSecret to rotate the signing secret
  • See API Reference for complete endpoint documentation