Webhook Endpoints API
The Webhook Endpoints API provides full CRUD (Create, Read, Update, Delete) operations for managing webhook endpoint configurations. You can create, list, view, update, delete webhook endpoints, send test webhooks, and view delivery history programmatically.
Webhook endpoints allow you to receive real-time notifications about events in your Broadcast channel, such as email deliveries, subscriber changes, and broadcast status updates.
Required Permissions
All endpoints require authentication via an API token with appropriate permissions:
- Read Permission: Required for GET endpoints (list, show, deliveries)
- Write Permission: Required for POST, PATCH, DELETE endpoints (create, update, delete, test)
Webhook Endpoint Object
The webhook endpoint object contains the following fields:
| Field | Description |
|---|---|
| `id` | Unique identifier of the webhook endpoint |
| `url` | The URL where webhooks will be sent (`http://` or `https://`; HTTPS is recommended). Unique per channel |
| `description` | Optional description of the webhook endpoint |
| `active` | Whether the webhook endpoint is active |
| `event_types` | Array of event types this endpoint subscribes to |
| `retries_to_attempt` | The endpoint’s maximum retry attempts (default: 6). See the retry schedule |
| `secret` | HMAC secret for signature verification (redacted after creation) |
| `last_response_code` | HTTP status code from the last webhook delivery |
| `last_sent_at` | Timestamp of the last webhook delivery attempt |
| `created_at` | When the webhook endpoint was created |
| `updated_at` | When the webhook endpoint was last updated |
retries_to_attempt is stored and returned, but delivery does not use it. Every endpoint follows the fixed retry schedule, whatever this value.
2.53+
retries_to_attempt is the number of retries after the first attempt: 0 to 20. A failed delivery is retried that many times on the retry schedule.
Security Note
The webhook secret is used to sign webhook payloads using HMAC-SHA256. For security:
- The secret is only shown in full when creating a new webhook endpoint
- In subsequent GET requests, the secret is redacted (e.g.,
abcd••••••••wxyz) - Store the secret securely when you receive it during creation
- The secret cannot be modified; create a new webhook endpoint if you need a new secret
A change made through the API is never rejected because its webhook couldn’t be sent. If a webhook had a problem, the request still succeeds and says so in a warnings array and a Broadcast-Warning header. See API Response Warnings.
Available Event Types
You can subscribe to any combination of the following event types:
Email Events
| Event Type | Description |
|---|---|
email.sent |
Email was successfully sent to the mail server. Not sent for broadcasts while parallel sending is on |
email.send_delayed |
A transactional email is waiting because its email server is at the hourly limit. Sent once, at the first wait |
email.delivered |
Email was delivered to the recipient’s inbox |
email.delivery_delayed |
Email delivery is delayed |
email.opened |
Recipient opened the email |
email.clicked |
Recipient clicked a link in the email |
email.bounced |
Email bounced (hard or soft bounce) |
email.complained |
Recipient marked the email as spam |
email.failed |
Email failed to send. Not sent for broadcasts while parallel sending is on |
With parallel sending on (PARALLEL_BROADCAST_SENDING=true), broadcasts send broadcast.batch_completed instead of per-recipient email.sent and email.failed events. Sequential broadcasts (the default), sequences and transactional emails still send email.sent and email.failed. Delivery, open, click, bounce and complaint events are the same in both modes.
Subscriber Events
| Event Type | Description |
|---|---|
subscriber.created |
New subscriber was added |
subscriber.updated |
Subscriber data was updated |
subscriber.preferences_updated |
Topic values changed; sent together with subscriber.updated |
subscriber.deleted |
Subscriber was deleted. Not sent for subscribers removed by a purge |
subscriber.subscribed |
Subscriber opted in |
subscriber.unsubscribed |
Subscriber opted out |
subscriber.bounced |
Subscriber marked as bounced |
subscriber.complained |
Subscriber marked as complained |
Subscriber List Events
| Event Type | Description |
|---|---|
subscribers.purged |
Every subscriber in the channel was purged. Sent once per purge |
subscribers.purge_failed |
A purge stopped before it finished; running it again is safe |
Broadcast Events
| Event Type | Description |
|---|---|
broadcast.scheduled |
Broadcast was scheduled for sending |
broadcast.queueing |
Broadcast is being queued for sending |
broadcast.sending |
Broadcast is actively sending |
broadcast.sent |
Broadcast completed sending |
broadcast.failed |
Broadcast failed to send |
broadcast.partial_failure |
Some emails failed during broadcast |
broadcast.aborted |
Broadcast was aborted |
broadcast.paused |
Broadcast was paused |
broadcast.batch_completed |
One batch run of a parallel broadcast finished or stopped early. Only sent when parallel sending is on |
Sequence Events
| Event Type | Description |
|---|---|
sequence.subscriber_added |
Subscriber was added to a sequence |
sequence.subscriber_completed |
Subscriber completed a sequence |
sequence.subscriber_moved |
Subscriber was moved in a sequence |
sequence.subscriber_removed |
Subscriber was removed from a sequence |
sequence.subscriber_paused |
Subscriber was paused in a sequence |
sequence.subscriber_resumed |
Subscriber was resumed in a sequence |
sequence.subscriber_error |
Subscriber’s enrolment stopped being able to progress; the reason is in error_message. A step failing to send is email.failed instead |
Other Events
| Event Type | Description |
|---|---|
message.attempt.exhausted |
All retry attempts for a webhook delivery have been exhausted |
test.webhook |
Test webhook event |
List Webhook Endpoints
GET /api/v1/webhook_endpoints
Retrieve a list of all webhook endpoints for the authenticated broadcast channel.
Query Parameters
limit(optional): Maximum number of endpoints to return, from 1 to 1000offset(optional): Number of endpoints to skip, 0 or more
A limit or offset outside those ranges, or that is not a whole number, returns 422 Unprocessable Entity.
Request
curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-domain.com/api/v1/webhook_endpoints
Response
{ "data": [ { "id": 1, "url": "https://api.example.com/webhooks", "description": "Production webhook endpoint", "active": true, "event_types": ["email.sent", "email.delivered", "email.bounced"], "retries_to_attempt": 6, "secret": "abcd••••••••••••••••••••••••wxyz", "last_response_code": 200, "last_sent_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "total": 1 }
Get Webhook Endpoint
GET /api/v1/webhook_endpoints/:id
Retrieve details of a specific webhook endpoint.
Request
curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-domain.com/api/v1/webhook_endpoints/1
Response
{ "id": 1, "url": "https://api.example.com/webhooks", "description": "Production webhook endpoint", "active": true, "event_types": ["email.sent", "email.delivered", "email.bounced"], "retries_to_attempt": 6, "secret": "abcd••••••••••••••••••••••••wxyz", "last_response_code": 200, "last_sent_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-15T10:30:00Z" }
Create Webhook Endpoint
POST /api/v1/webhook_endpoints
Create a new webhook endpoint.
Required Parameters
url(required): A validhttp://orhttps://URL where webhooks will be sent (HTTPS is recommended). Each URL can be used only once per channelevent_types(required): Array of event types to subscribe to (at least one)
Optional Parameters
description: Description of the webhook endpointactive: Whether the endpoint is active (default: true)retries_to_attempt: Maximum retry attempts (default: 6). See the retry schedule
retries_to_attempt is stored only. Delivery always follows the fixed retry schedule.
2.53+
retries_to_attempt must be a whole number from 0 to 20, on create and on update. Any other value returns 422 Unprocessable Entity with an error message.
Request
curl -X POST \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "url": "https://api.example.com/webhooks", "description": "Production webhook endpoint", "active": true, "event_types": ["email.sent", "email.delivered", "email.bounced", "subscriber.created"], "retries_to_attempt": 6 } }' \ https://your-domain.com/api/v1/webhook_endpoints
Response
Note: The secret is shown in full only on creation. Store it securely!
{ "id": 1, "url": "https://api.example.com/webhooks", "description": "Production webhook endpoint", "active": true, "event_types": ["email.sent", "email.delivered", "email.bounced", "subscriber.created"], "retries_to_attempt": 6, "secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6", "last_response_code": null, "last_sent_at": null, "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-01T12:00:00Z" }
Update Webhook Endpoint
PATCH /api/v1/webhook_endpoints/:id
Update an existing webhook endpoint.
Request
curl -X PATCH \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "description": "Updated description", "active": false, "event_types": ["email.sent", "email.delivered"] } }' \ https://your-domain.com/api/v1/webhook_endpoints/1
Response
{ "id": 1, "url": "https://api.example.com/webhooks", "description": "Updated description", "active": false, "event_types": ["email.sent", "email.delivered"], "retries_to_attempt": 6, "secret": "a1b2••••••••••••••••••••••••y5z6", "last_response_code": 200, "last_sent_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-15T11:00:00Z" }
Delete Webhook Endpoint
DELETE /api/v1/webhook_endpoints/:id
Delete a webhook endpoint.
Request
curl -X DELETE \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-domain.com/api/v1/webhook_endpoints/1
Response
{ "message": "Webhook endpoint deleted successfully" }
Send Test Webhook
POST /api/v1/webhook_endpoints/:id/test
Send a test webhook to verify your endpoint is working correctly. The test webhook will contain realistic sample data for the specified event type.
Query Parameters
event_type(optional): The event type to test (default: “test.webhook”). Must be one of the event types configured for this endpoint.
Request
curl -X POST \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event_type": "email.delivered" }' \ https://your-domain.com/api/v1/webhook_endpoints/1/test
Response
{ "message": "Test webhook queued for delivery", "event_type": "email.delivered", "delivery_id": 42 }
List Webhook Deliveries
GET /api/v1/webhook_endpoints/:id/deliveries
Retrieve the delivery history for a webhook endpoint, including successful deliveries and failed attempts.
Query Parameters
limit(optional): Maximum number of deliveries to return, from 1 to 1000offset(optional): Number of deliveries to skip, 0 or more
A limit or offset outside those ranges, or that is not a whole number, returns 422 Unprocessable Entity.
Request
curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-domain.com/api/v1/webhook_endpoints/1/deliveries
Response
{ "data": [ { "id": 42, "event_type": "email.delivered", "event_data": { "receipt_id": 1234, "email": "subscriber@example.com", "delivered": true, "opened": false }, "attempt_number": 1, "response_status": 200, "response_body": "OK", "error_message": null, "first_attempt_at": "2024-01-15T10:30:00Z", "last_attempt_at": "2024-01-15T10:30:00Z", "next_retry_at": null, "successfully_delivered_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "total": 1 }
Webhook Delivery Object
The delivery object contains information about each webhook delivery attempt:
| Field | Description |
|---|---|
| `id` | Unique identifier of the delivery |
| `event_type` | The type of event that triggered this delivery |
| `event_data` | The payload that was sent to the webhook URL |
| `attempt_number` | The attempt number of the delivery (see below the table) |
| `response_status` | HTTP status code from your endpoint |
| `response_body` | Response body from your endpoint |
| `error_message` | Error message if delivery failed |
| `first_attempt_at` | When the first delivery attempt was made |
| `last_attempt_at` | When the most recent attempt was made |
| `next_retry_at` | When the next retry will be attempted (if applicable) |
| `successfully_delivered_at` | When delivery was successful (null if not yet delivered) |
| `created_at` | When the delivery record was created |
| `updated_at` | When the delivery record was last updated |
attempt_number is the current attempt number: 1 to 6, and 7 after the sixth attempt fails.
2.53+
attempt_number is the attempt due next while the delivery is pending. After that, it is the last attempt made, and never less than 1. It is the same number the dashboard shows.
Retry Schedule
Failed webhook deliveries are automatically retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | 10 hours |
Each delay counts from the previous attempt. The schedule is the same for every endpoint; retries_to_attempt does not change it. After 6 failed attempts, the delivery is marked as failed and a message.attempt.exhausted event is triggered.
2.53+
The first attempt is immediate. When it fails, Broadcast retries the delivery up to retries_to_attempt times (0 to 20, default 6). The wait before each retry is:
| Retry | Wait |
|---|---|
| 1 | 5 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | 10 hours |
| 7 to 20 | 10 hours |
Each wait counts from the attempt that failed.
When the last retry fails, the delivery is marked as failed and a message.attempt.exhausted event is sent. Its total_attempts is the number of attempts made. A delivery that was given up before any attempt reports 0. A failed message.attempt.exhausted delivery does not send another message.attempt.exhausted event.
If you lower retries_to_attempt, a pending delivery that is already past the new limit gets no more attempts. It is marked as failed and sends message.attempt.exhausted.
Verifying Webhook Signatures
All webhooks are signed using HMAC-SHA256. To verify a webhook:
- Extract the signature from the
broadcast-webhook-signatureheader - Extract the timestamp from the
broadcast-webhook-timestampheader - Create the signed payload:
{timestamp}.{raw_request_body} - Calculate HMAC-SHA256 using your secret and the signed payload
- Compare the calculated signature with the one in the header
Header Format
broadcast-webhook-signature: v1,{base64_encoded_signature}
broadcast-webhook-timestamp: 1705312200
Example Verification (Ruby)
def verify_webhook(payload, headers, secret) signature_header = headers['broadcast-webhook-signature'] timestamp = headers['broadcast-webhook-timestamp'] version, signature = signature_header.split(',') return false unless version == 'v1' # Check timestamp (5 minute window) return false if (Time.now.to_i - timestamp.to_i).abs > 300 signed_payload = "#{timestamp}.#{payload}" expected = Base64.strict_encode64( OpenSSL::HMAC.digest('sha256', secret, signed_payload) ) ActiveSupport::SecurityUtils.secure_compare(signature, expected) end
Error Responses
401 Unauthorized
{ "error": "Unauthorized" }
Returned when the API token is missing, invalid, or lacks the required permissions.
404 Not Found
{ "error": "Webhook endpoint not found" }
Returned when the specified webhook endpoint doesn’t exist or belongs to a different channel.
422 Unprocessable Entity
{ "error": "Url is invalid, Event types must have one selected" }
Common validation errors: - Missing or invalid URL (must be HTTP or HTTPS) - No event types selected - Duplicate URL (each URL must be unique per channel) - Invalid event type for test webhook
Example: Complete Setup Flow
# 1. Create a webhook endpoint curl -X POST \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "url": "https://api.myapp.com/broadcast-webhooks", "description": "Email event notifications", "event_types": ["email.sent", "email.delivered", "email.bounced", "email.opened"] } }' \ https://your-domain.com/api/v1/webhook_endpoints # Response: Save the secret! It won't be shown again. # { "id": 1, "secret": "abc123...", ... } # 2. Send a test webhook to verify your endpoint curl -X POST \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"event_type": "email.delivered"}' \ https://your-domain.com/api/v1/webhook_endpoints/1/test # 3. Check the delivery history curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-domain.com/api/v1/webhook_endpoints/1/deliveries # 4. Verify successful delivery # Response shows response_status: 200 and successfully_delivered_at is set