A webhook sends an HTTP POST to your URL each time an event happens in your account: an email bounces, a
contact unsubscribes, a campaign is scheduled. This guide covers creating and managing webhooks, the payload
your URL receives, checking that a delivery comes from the platform, and the three classes of events.
- Event catalog: every event you can subscribe to, and when it fires.
- Compliance events: the exact payloads of bounces, spam complaints, unsubscribes, and refused or failed sends.
Creating a webhook
A webhook subscribes one URL to one event. To receive several events, create one webhook per event; they can share a URL.
Code
The response (201) returns the webhook:
Code
| Field | Required | Description |
|---|---|---|
event | Yes | The event, such as Email.Bounced. See the event catalog. |
url | Yes | Where deliveries go: an http or https URL. |
rate_limit | No | The most deliveries per period. The default is 50, and the maximum is 50 per second or 3,000 per minute. |
rate_limit_period | No | second (the default) or minute. |
Sub-accounts. Webhook requests act on the account of the access token. A partner or an organization can act
on a sub-account by adding ?account_id=<account_id> to any webhook request. That webhook then receives the
sub-account's events.
Access tokens. With a personal access token, listing and showing webhooks needs the webhooks:read scope,
and creating or changing them needs webhooks:write.
Errors. Error responses have the form {"detail": [{"msg": "…", "type": "…"}]}.
| Status | Why |
|---|---|
422 | A field is missing or invalid, the rate limit is above the maximum, or the event is a high-volume event and the URL's domain isn't approved (High volume events can only be registered with approved webhook domains). |
400, code 2600 | Webhook not found: the id doesn't exist, or belongs to another account. |
400, code 2604 | The new URL of a high-volume webhook isn't on an approved domain. |
See Create a webhook in the API reference for every field.
Managing webhooks
| Request | What it does |
|---|---|
GET /webhooks | Lists the account's webhooks. Add with_archived=true to include archived ones. Paginate with page, per_page and with_count. |
GET /webhooks/{webhook_id} | Shows one webhook, and the account's signing key. |
PATCH /webhooks/{webhook_id} | Changes url, or rate_limit and rate_limit_period together. The event can't be changed: create another webhook. |
POST /webhooks/{webhook_id}/archive | Stops deliveries. |
POST /webhooks/{webhook_id}/unarchive | Starts them again. |
Receiving events
Each delivery is a POST with a JSON body, a Content-Type: application/json header and an x-signature
header (see Securing your webhooks).
The payload envelope
Every event has the same envelope. This is a campaign bounce:
Code
| Field | Type | Description |
|---|---|---|
id | string | The event's unique id. It stays the same when an event is delivered again, so deduplicate on it. |
event | string | The event name. |
account_id | integer | The account the event belongs to. |
account_lineage | null | Always null. |
ip | string | Where the event came from. For an event caused by an API request (an account, campaign, sender, user or export event), it's the address that made the request. For an event the platform records itself, such as a bounce, a complaint or an unsubscribe, it's a platform server, never the recipient: when the recipient's address is known, it's in data.context.ip. |
user | object | The id, email and account_id of the user whose action caused the event, when the platform knows it. They're null when no user acted, as for a bounce, a complaint or an unsubscribe, and can be null for an event caused by an API request. |
created_on | integer | When this delivery was created, in Unix seconds. It isn't when the event happened, and it changes when a delivery is repeated. Email API events have the event time in data.event_time. |
data | object | The event's details. Its shape depends on the event and on how the email was sent: see Compliance events. |
Handling deliveries
- Answer with a
2xxstatus quickly, and do the work afterwards. - The same event can arrive more than once. Deduplicate on
id. - Events can arrive in any order. When
data.event_timeordata.context.timestampis present, use it to order them. - A webhook's
rate_limitcaps how fast deliveries reach your URL.
Securing your webhooks
The x-signature header of each delivery is the HMAC-SHA256 of the raw request body, keyed with your account's
signing key, written as lowercase hexadecimal. Compute it and compare: a match proves that the platform sent this
exact body.
-
Get the signing key.
GET /webhooks/{webhook_id}returns it insignature.key. An account has one key, shared by all its webhooks.CodeCode -
Verify the body exactly as received, before parsing it. Parsing and re-serializing the JSON changes the bytes, and the signature no longer matches.
Node.js:
Code
With Express, read the raw body:
Code
Python:
Code
With Flask:
Code
Event classes
| Class | Events | What it needs |
|---|---|---|
| Standard | Account, campaign, export, list, segment, sender, suppression and user events | Any URL. |
| Compliance | Email.Bounced, Email.ReportedAsSpam, Email.Unsubscribed, Email.GlobalUnsubscribed, Email.Rejected, Email.Error, Contact.Removed | Any URL. Their payloads are in Compliance events. |
| High-volume | Contact.Added, Contact.Updated, Email.Sent, Email.Opened, Email.Clicked, Email.Submitted, Email.Queued, Email.Processing, Email.Delivered | A URL on an approved domain. Contact support to have a domain approved. |
Which events fire also depends on how you send: see the event catalog.
Limitations
- A webhook subscribes to one event, and its event can't be changed.
- A failed delivery isn't guaranteed to be retried. If your URL was unavailable, reconcile from the API.
- Payloads are documented for compliance events and
SuppressedEmail.Addedonly.