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
- Go to Settings > Webhook Endpoints in your Broadcast dashboard.
- Click Add Webhook Endpoint.
- Enter your endpoint URL (must be HTTPS in production).
- Select the event types you want to receive.
- Set the number of retries (0 to 20). The default retry schedule provides 6 attempts.
- 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
- Broadcast constructs the signed payload:
{timestamp}.{json_body} - The HMAC-SHA256 digest is computed using your endpoint’s secret as the key.
- 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-Agentheader is set toBroadcast-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_attimestamp is set. - Pending – The delivery failed but has retries remaining. The
next_retry_attimestamp indicates when the next attempt will occur. - Failed – All retry attempts exhausted or the delivery was permanently abandoned.
Testing Webhooks
From the Dashboard
- Navigate to Settings > Webhook Endpoints.
- Open your endpoint.
- Click Test Webhook and select an event type to simulate.
- 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
idto 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_codeand 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.exhaustedevent 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.