Webhooks Guide

Webhooks let Broadcast push real-time event notifications to your application over HTTP. Instead of polling the API for changes, you register a URL and Broadcast sends a POST request to that URL whenever a subscribed event occurs.

This guide covers how webhooks work, what events are available, how to verify payloads, and how to handle deliveries reliably.

Common Use Cases

  • CRM synchronization – Keep subscriber data in sync with your CRM when people subscribe, unsubscribe, or update their profile.
  • Analytics pipelines – Stream email engagement data (opens, clicks, bounces) to your analytics warehouse.
  • Workflow automation – Trigger actions in tools like Zapier, Make, or n8n when broadcasts are sent or sequences complete.
  • Compliance and auditing – Log unsubscribe and complaint events for regulatory compliance.
  • Monitoring and alerting – Get notified immediately when deliveries fail or bounces spike.

Available Event Types

Email Events

Fired for individual email deliveries, whether from broadcasts, sequences, or transactional sends.

Event Description
email.sent Email handed off to the delivery provider
email.delivered Email confirmed delivered to the recipient’s mail server
email.delivery_delayed Delivery temporarily delayed by the provider
email.bounced Email bounced (hard or soft bounce)
email.complained Recipient marked the email as spam
email.opened Recipient opened the email (tracking pixel loaded)
email.clicked Recipient clicked a tracked link in the email
email.failed Email delivery permanently failed

Subscriber Events

Fired when subscriber records change.

Event Description
subscriber.created A new subscriber was added to the channel
subscriber.updated Subscriber data was modified (name, custom fields, tags)
subscriber.deleted Subscriber was permanently removed
subscriber.subscribed Subscriber opted in or was resubscribed
subscriber.unsubscribed Subscriber opted out
subscriber.bounced Subscriber was marked as bounced due to delivery failures
subscriber.complained Subscriber was flagged due to a spam complaint

Broadcast Events

Fired as broadcast campaigns progress through their lifecycle.

Event Description
broadcast.scheduled Broadcast scheduled for future delivery
broadcast.queueing Broadcast is being queued for sending
broadcast.sending Broadcast is actively being sent
broadcast.sent Broadcast finished sending to all recipients
broadcast.failed Broadcast sending failed entirely
broadcast.partial_failure Broadcast was sent to some recipients but not all
broadcast.aborted Broadcast was manually cancelled
broadcast.paused Broadcast sending was paused

Sequence Events

Fired as subscribers progress through automated email sequences.

Event Description
sequence.subscriber_added Subscriber was enrolled in a sequence
sequence.subscriber_completed Subscriber completed all steps in the sequence
sequence.subscriber_moved Subscriber was moved to a different step
sequence.subscriber_removed Subscriber was removed from the sequence
sequence.subscriber_paused Subscriber’s sequence progression was paused
sequence.subscriber_resumed Subscriber’s sequence progression was resumed
sequence.subscriber_error An error occurred while processing the subscriber

System Events

Event Description
message.attempt.exhausted All delivery retry attempts for a webhook have been exhausted
test.webhook A test webhook sent from the dashboard or API

Setting Up Webhooks

  1. Go to Settings > Webhook Endpoints in your Broadcast dashboard.
  2. Click Add Webhook Endpoint.
  3. Enter your endpoint URL (must be HTTPS in production).
  4. Select the event types you want to receive.
  5. Set the number of retries (0 to 20). The default retry schedule provides 6 attempts.
  6. Click Save.

Broadcast generates a signing secret for each endpoint automatically. Copy this secret and store it securely – you will need it to verify webhook signatures.

You can also create endpoints programmatically using the Webhook Endpoints API.

Webhook Payload Structure

Every webhook delivery is a POST request with a JSON body that follows this structure:

{
  "id": 12345,
  "type": "subscriber.created",
  "created_at": "2025-10-01T14:30:00Z",
  "data": {
    ...
  }
}
Field Type Description
id integer Unique ID of the webhook event
type string The event type identifier
created_at string ISO 8601 timestamp of when the event occurred
data object Event-specific payload (varies by event type)

Example Payloads

Subscriber Event

{
  "id": 12345,
  "type": "subscriber.created",
  "created_at": "2025-10-01T14:30:00Z",
  "data": {
    "subscriber_id": 4521,
    "email": "[email protected]",
    "first_name": "Jane",
    "last_name": "Smith",
    "subscribed_at": "2025-10-01T14:30:00Z",
    "unsubscribed_at": null,
    "is_active": true,
    "custom_data": {
      "source_ip": "192.168.1.42",
      "user_agent": "Mozilla/5.0",
      "referrer": "https://example.com/signup"
    },
    "source": "opt-in"
  }
}

Email Event

Email events include context about the source of the email – whether it came from a broadcast, a sequence, or a transactional send.

{
  "id": 12346,
  "type": "email.delivered",
  "created_at": "2025-10-01T15:00:00Z",
  "data": {
    "receipt_id": 8901,
    "email": "[email protected]",
    "identifier": "msg_a1b2c3d4e5f6g7h8",
    "receipted_type": "Broadcast",
    "receipted_id": 234,
    "delivered": true,
    "opened": false,
    "opened_at": null,
    "clicked": false,
    "clicked_at": null,
    "bounced": false,
    "bounced_reason": null,
    "complaint": false,
    "complaint_reason": null,
    "unsubscribed": false
  }
}

The receipted_type field tells you the email’s origin:

  • "Broadcast" – Sent as part of a broadcast campaign
  • "SequenceStep" – Sent from an automated sequence
  • "TransactionalEmail" – Sent via the transactional email API

Broadcast Event

{
  "id": 12347,
  "type": "broadcast.sent",
  "created_at": "2025-10-01T16:00:00Z",
  "data": {
    "broadcast_id": 234,
    "name": "October Newsletter",
    "subject": "Your October Update",
    "status": "sent",
    "total_recipients": 5200,
    "broadcast_recipients_count": 5200,
    "percentage_complete": 100.0,
    "sent_at": "2025-10-01T16:00:00Z",
    "scheduled_send_at": "2025-10-01T15:00:00Z",
    "created_at": "2025-09-28T10:00:00Z",
    "updated_at": "2025-10-01T16:00:00Z",
    "broadcast_channel_id": 1,
    "segment_ids": [3, 7],
    "tags": ["newsletter", "october"]
  }
}

Exhausted Delivery Event

Sent when all retry attempts for a webhook delivery have been used up.

{
  "id": 12348,
  "type": "message.attempt.exhausted",
  "created_at": "2025-10-01T22:00:00Z",
  "data": {
    "delivery_id": 5678,
    "webhook_endpoint_id": 7,
    "attempts": 6,
    "last_error": "Connection timeout after 30 seconds",
    "first_attempted_at": "2025-10-01T16:00:00Z",
    "last_attempted_at": "2025-10-01T22:00:00Z",
    "original_event_type": "email.delivered"
  }
}

Webhook Security

Every webhook request is signed with an HMAC-SHA256 signature so you can verify it came from Broadcast.

Signature Headers

Each delivery includes three custom headers:

Header Description
broadcast-webhook-id Unique ID of the webhook event
broadcast-webhook-timestamp Unix timestamp (seconds) of when the payload was signed
broadcast-webhook-signature Signature in the format v1,<base64-encoded-signature>

How Signing Works

  1. Broadcast constructs the signed payload: {timestamp}.{json_body}
  2. The HMAC-SHA256 digest is computed using your endpoint’s secret as the key.
  3. The digest is Base64-encoded and prefixed with v1,.

Verification Example (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(payload, headers, secret) {
  const timestamp = headers['broadcast-webhook-timestamp'];
  const signature = headers['broadcast-webhook-signature'];

  // Reject old timestamps to prevent replay attacks (5 minutes tolerance)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
    throw new Error('Webhook timestamp too old');
  }

  // Reconstruct the signed payload
  const signedPayload = `${timestamp}.${payload}`;

  // Compute the expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('base64');

  // Compare signatures (timing-safe)
  const expected = `v1,${expectedSignature}`;
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    throw new Error('Invalid webhook signature');
  }

  return true;
}

Verification Example (Ruby)

require 'openssl'
require 'base64'

def verify_webhook_signature(payload, headers, secret)
  timestamp = headers['broadcast-webhook-timestamp']
  signature = headers['broadcast-webhook-signature']

  # Check timestamp freshness (5 minute tolerance)
  if (Time.now.to_i - timestamp.to_i).abs > 300
    raise 'Webhook timestamp too old'
  end

  # Compute expected signature
  signed_payload = "#{timestamp}.#{payload}"
  expected = Base64.strict_encode64(
    OpenSSL::HMAC.digest('sha256', secret, signed_payload)
  )

  # Compare signatures
  unless ActiveSupport::SecurityUtils.secure_compare(signature, "v1,#{expected}")
    raise 'Invalid webhook signature'
  end

  true
end

Delivery and Retries

Delivery Behavior

  • Webhook payloads are sent as HTTP POST requests with Content-Type: application/json.
  • The User-Agent header is set to Broadcast-Webhooks/1.0.
  • Broadcast waits up to 30 seconds for a response before timing out.
  • A delivery is considered successful if your server responds with an HTTP 2xx status code.
  • Any non-2xx response or timeout triggers a retry (if retries are configured).

Retry Schedule

Failed deliveries are retried on the following schedule:

Attempt Delay After Failure
1st retry 5 seconds
2nd retry 5 minutes
3rd retry 30 minutes
4th retry 2 hours
5th retry 5 hours
6th retry 10 hours

After all configured retries are exhausted, Broadcast fires a message.attempt.exhausted event so you can take action (e.g., alert your team or disable the endpoint).

The message.attempt.exhausted event itself is not retried.

Delivery Status

Each webhook delivery has one of three statuses:

  • Successful – Your server responded with 2xx. The successfully_delivered_at timestamp is set.
  • Pending – The delivery failed but has retries remaining. The next_retry_at timestamp indicates when the next attempt will occur.
  • Failed – All retry attempts exhausted or the delivery was permanently abandoned.

Testing Webhooks

From the Dashboard

  1. Navigate to Settings > Webhook Endpoints.
  2. Open your endpoint.
  3. Click Test Webhook and select an event type to simulate.
  4. Check the delivery history to see the result.

From the API

curl -X POST "https://app.sendbroadcast.com/api/v1/webhook_endpoints/7/test" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "subscriber.created" }'

Using External Tools

  • webhook.site – Get a temporary URL to inspect incoming webhook payloads in real time.
  • ngrok – Tunnel webhooks to your local development server for end-to-end testing.

Express.js Handler Example

A complete webhook handler that verifies signatures and processes events:

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.BROADCAST_WEBHOOK_SECRET;

// Use raw body for signature verification
app.post('/webhooks/broadcast', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString();
  const timestamp = req.headers['broadcast-webhook-timestamp'];
  const signature = req.headers['broadcast-webhook-signature'];

  // Verify signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('base64');

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`v1,${expectedSignature}`)
  )) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  // Parse the event
  const event = JSON.parse(payload);

  // Process by event type
  switch (event.type) {
    case 'subscriber.created':
      console.log('New subscriber:', event.data.email);
      // Add to your CRM, trigger welcome flow, etc.
      break;

    case 'subscriber.unsubscribed':
      console.log('Unsubscribed:', event.data.email);
      // Update CRM, remove from active lists, etc.
      break;

    case 'email.bounced':
      console.log('Bounced:', event.data.email, event.data.bounced_reason);
      // Flag in your system, investigate deliverability, etc.
      break;

    case 'broadcast.sent':
      console.log('Broadcast complete:', event.data.name,
        `${event.data.broadcast_recipients_count}/${event.data.total_recipients} delivered`);
      break;

    default:
      console.log('Received event:', event.type);
  }

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

app.listen(3000, () => console.log('Webhook handler listening on port 3000'));

Best Practices

  • Respond quickly. Return a 2xx response as fast as possible. If processing takes time, accept the webhook and handle it asynchronously in a background job or queue.
  • Process events asynchronously. Queue incoming webhooks for background processing to avoid timeouts and ensure reliable handling.
  • Handle duplicate deliveries. Use the webhook event id to deduplicate. Retries may deliver the same event more than once.
  • Validate signatures. Always verify the HMAC-SHA256 signature before processing a webhook payload. This prevents spoofed requests.
  • Check timestamp freshness. Reject payloads with timestamps older than 5 minutes to prevent replay attacks.
  • Log everything. Store the raw payload, headers, and your processing result. This makes debugging delivery issues much easier.
  • Monitor your endpoint health. Watch the last_response_code and delivery history in the dashboard. Set up alerts for sustained failures.
  • Use HTTPS. Webhook URLs must use HTTPS in production to protect payload contents in transit.
  • Subscribe selectively. Only subscribe to the event types you actually need. This reduces unnecessary traffic and processing.

Troubleshooting

Not receiving webhooks

  • Verify the endpoint is active (active: true).
  • Confirm the endpoint subscribes to the event types you expect.
  • Check that your server is reachable from the public internet.
  • Review the delivery history via the dashboard or the Deliveries API for error details.
  • Make sure your firewall or CDN is not blocking POST requests from Broadcast.

Signature verification failing

  • Ensure you are using the raw request body (not a parsed/re-serialized version) for signature computation.
  • Confirm you are using the correct signing secret for the specific endpoint.
  • Check that you are constructing the signed payload as {timestamp}.{raw_body} with no extra whitespace.
  • Verify your HMAC implementation uses SHA-256 and Base64 encoding.

High failure rates

  • Check your server logs for errors or timeouts.
  • Ensure your endpoint responds within 30 seconds.
  • If processing is slow, return 200 immediately and process the event asynchronously.
  • Use the message.attempt.exhausted event to detect endpoints that are consistently failing.
  • Consider reducing the number of subscribed events if your server is overloaded.

For managing webhook endpoints programmatically, see the Webhook Endpoints API.