Webhooks
Register endpoints that receive signed event notifications.
Create a webhook
Creates a webhook endpoint in your workspace. Emailit sends matching events to the URL in batches of up to 100, as a JSON array signed with the webhook’s secret. See Webhook requests for the request format. Requires an API key with full scope.
/webhooksRequest body
namestringRequiredWebhook name. Must be unique in the workspace; you can use it instead of the ID in other webhook endpoints.
urlstringRequiredEndpoint that receives the events. http and https URLs are accepted; use https in production.
Emailit resolves the hostname when you save and rejects localhost, private, link-local and other reserved IP addresses. Redirects aren’t followed when delivering, so use the final URL.
all_eventsbooleanSend every event type, including types added later. Default false. When true, events is ignored.
enabledbooleanWhether Emailit delivers events to the webhook. Default true.
eventsstring[]Event types to send, for example ["email.delivered", "email.bounced"]. See Event types. Default [], which with all_events: false means the webhook receives nothing.
Event names aren’t validated. A misspelled type is saved but never matches an event.
filterobject | nullPayload filter. Emailit only sends events whose object matches the rules. Available on Pro, Business and Custom plans; a filter with rules on Pay as you go returns 403.
filter.matchstringall (default) sends an event when every rule matches. any sends it when at least one rule matches.
filter.rulesobject[]Up to 25 rules.
filter.rules[].fieldstringRequiredDotted path into the event object, for example to, status, meta.plan or, for click and open events, email.campaign.id. A leading payload. is ignored.
filter.rules[].operatorstringRequiredequals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than, is_set, is_not_set, in or not_in. Text operators compare values as strings; greater_than and less_than compare numbers.
filter.rules[].valueanyValue to compare with. Required for every operator except is_set and is_not_set. Use an array with in and not_in.
Returns
Returns 201 Created with the webhook object, including the signing secret (whsec_ followed by 64 hex characters). Use the secret to verify request signatures. You can read it again with Retrieve a webhook and rotate it with Rotate the signing secret.
| Status | When |
|---|---|
400 |
name or url is missing, the URL is invalid, can’t be resolved or points to a blocked address, or the filter is invalid. |
403 |
The filter has rules and your plan doesn’t include webhook filters. The body is {"error": "plan_required", "required_plan": "pro"}. |
409 |
A webhook with this name already exists. The body includes the existing webhook’s id and name. |
422 |
The workspace has reached its plan’s webhook limit. The body includes usage.used and usage.limit. See Limits. |
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": null,
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T09:41:05.302000+00:00",
"secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}{
"error": "URL resolves to a private/reserved IP address"
}{
"error": "plan_required",
"required_plan": "pro"
}{
"error": "Webhook with this name already exists",
"existing": {
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events"
}
}{
"error": "Pay as you go includes 3 webhook endpoints.",
"usage": {
"used": 3,
"limit": 3
}
}{
"name": "Enterprise bounces",
"url": "https://api.acme.com/webhooks/emailit",
"events": ["email.bounced", "email.complained"],
"filter": {
"match": "all",
"rules": [
{ "field": "meta.plan", "operator": "equals", "value": "enterprise" },
{ "field": "to", "operator": "not_contains", "value": "@acme.com" }
]
}
}Retrieve a webhook
Returns one webhook, looked up by ID or by name. This is the only read endpoint that returns the signing secret. Requires an API key with full scope.
/webhooks/:idPath parameters
idstringRequiredWebhook ID (wh_…) or the webhook’s name, URL-encoded.
Returns
Returns 200 OK with the webhook object, including secret and filters_allowed (whether your plan lets the webhook use a payload filter). last_used_at is the time of the last successful delivery, or null if nothing has been delivered yet.
Returns 404 with error: "Webhook not found" if no webhook matches.
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": {
"match": "all",
"rules": [
{ "field": "meta.plan", "operator": "equals", "value": "enterprise" }
]
},
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T09:41:05.302000+00:00",
"secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}{
"error": "Webhook not found"
}Update a webhook
Updates a webhook. Send only the fields you want to change; at least one is required. The signing secret doesn’t change; rotate it with Rotate the signing secret. Requires an API key with full scope.
/webhooks/:idPath parameters
idstringRequiredWebhook ID (wh_…) or the webhook’s name, URL-encoded.
Request body
namestringNew name. Must be unique in the workspace.
urlstringNew endpoint URL, http or https. Validated the same way as on create.
all_eventsbooleantrue sends every event type and clears the events list. If you set it to false, also send events, or the webhook receives nothing.
enabledbooleanfalse stops deliveries and true resumes them. Events that happen while the webhook is disabled aren’t queued for it and aren’t sent later.
eventsstring[]Replaces the list of event types. Ignored while all_events is true. Names aren’t validated.
filterobject | nullReplaces the payload filter, in the same format as on create. Send null to remove it. A filter with rules needs a Pro, Business or Custom plan.
Returns
Returns 200 OK with the updated webhook. The secret isn’t included; use Retrieve a webhook to read it.
| Status | When |
|---|---|
400 |
The body has none of the fields above, or the URL or filter is invalid. |
403 |
The filter has rules and your plan doesn’t include webhook filters (plan_required). |
404 |
No webhook matches id. |
409 |
Another webhook already uses the new name. |
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": false,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T10:15:40.000000+00:00"
}{
"error": "No valid fields provided for update. Provide at least one of: name, url, all_events, enabled, events, filter"
}{
"error": "Webhook not found"
}{
"error": "Another webhook with this name already exists"
}List webhooks
Returns the webhooks in your workspace, newest first, and how many your plan allows. Signing secrets aren’t included in the list. Requires an API key with full scope.
/webhooksQuery parameters
pageintegerPage number, starting at 1. Default 1.
limitintegerWebhooks per page, from 1 to 100. Default 10.
searchstringCase-insensitive match on the webhook name or URL.
matchstringall (default) requires every filter. or matches any filter. See Filtering.
orderstringSort key for this list. See the sort keys below.
directionstringasc or desc.
Filters and sort
List filters are one layer of key.condition=value query parameters. See Filtering for match, order, direction and the condition list per type.
Filter keys
| Key | Type | Conditions | Notes |
|---|---|---|---|
name | string | exact, not_exact, contains, not_contains, starts_with, ends_with, empty, not_empty | |
url | string | exact, not_exact, contains, not_contains, starts_with, ends_with, empty, not_empty | |
enabled | boolean | exact, not_exact | |
created_at | date | exact, before, after, empty, not_empty |
Sort keys
Pass order as one of these keys and direction as asc or desc: name, url, enabled, created_at
Returns
Returns 200 OK with the webhooks in data, next_page_url and previous_page_url (null at either end), and a usage object: used is the number of webhooks in the workspace, limit is your plan’s maximum, and filters_allowed says whether your plan includes payload filters.
{
"data": [
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T10:02:17.845000+00:00"
}
],
"next_page_url": null,
"previous_page_url": null,
"usage": {
"used": 1,
"limit": 10,
"filters_allowed": true
}
}{
"statusCode": 401,
"error": "Unauthorized",
"message": "Invalid API key"
}Delete a webhook
Permanently deletes a webhook and its event subscriptions. To stop deliveries temporarily, update the webhook with enabled: false instead. Requires an API key with full scope.
/webhooks/:idPath parameters
idstringRequiredWebhook ID (wh_…) or the webhook’s name, URL-encoded.
Returns
Returns 200 OK with the deleted webhook’s id and name and deleted: true. Returns 404 with error: "Webhook not found" if no webhook matches.
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"deleted": true
}{
"error": "Webhook not found"
}Send a test event
Sends a sample event of the type you choose to the webhook’s URL and returns your endpoint’s response. Use it to check that your endpoint is reachable and verifies signatures correctly. Requires an API key with the full scope.
The request has the same format, headers and signature as a real delivery: a JSON array with one event whose event_id starts with evt_test_, signed with the webhook’s current secret. It’s sent even if the webhook is disabled or not subscribed to that type, isn’t stored as a webhook request, and isn’t retried. The sample data is fixed and doesn’t refer to real objects.
You can send 5 test events per minute from the same IP address; more return 429.
/webhooks/{id}/testPath parameters
idstringrequiredwh_…) or the webhook’s name.Body parameters
typestringrequired| Resource | Event types |
|---|---|
email.accepted, email.scheduled, email.delivered, email.bounced, email.attempted, email.failed, email.rejected, email.clicked, email.loaded, email.complained, email.received, email.suppressed, email.canceled, email.unsubscribed, email.resubscribed |
|
| Domain | domain.created, domain.updated, domain.deleted |
| Audience | audience.created, audience.updated, audience.deleted |
| Subscriber | subscriber.created, subscriber.updated, subscriber.deleted |
| Contact | contact.created, contact.updated, contact.deleted |
| Template | template.created, template.updated, template.deleted |
| Suppression | suppression.created, suppression.updated, suppression.deleted |
| Email verification | email_verification.created, email_verification.updated, email_verification_list.created, email_verification_list.updated |
| Campaign | campaign.created, campaign.updated, campaign.deleted, campaign.scheduled, campaign.queued, campaign.sending, campaign.testing, campaign.sent, campaign.canceled, campaign.archived |
See Event types for what each event means.
Returns
okbooleantrue if your endpoint answered with a 2xx status.status_codeinteger0 if Emailit couldn’t connect, the request timed out after 30 seconds, the endpoint redirected (redirects aren’t followed), or the URL points to a blocked address.bodystringtypestringpayloadobject[]Returns 400 if type is missing or unknown, 404 if the webhook doesn’t exist, and 429 when you exceed the test limit.
{
"ok": true,
"status_code": 200,
"type": "email.delivered",
"payload": [
{
"event_id": "evt_test_3G0pUekgBm1uIxi9m4C380H70Zq",
"type": "email.delivered",
"object": {
"id": "eml_test_001",
"email_id": 12345,
"message_id": "<test-token@mydomain.com>",
"from": "sender@mydomain.com",
"to": "recipient@example.com",
"subject": "Test email",
"status": "delivered",
"delivered_at": "2026-01-15T10:30:00.000Z"
},
"data": {
"object": {
"id": "eml_test_001",
"email_id": 12345,
"message_id": "<test-token@mydomain.com>",
"from": "sender@mydomain.com",
"to": "recipient@example.com",
"subject": "Test email",
"status": "delivered",
"delivered_at": "2026-01-15T10:30:00.000Z"
}
}
}
],
"body": "{\"received\":true}"
}{
"error": "Unknown event type"
}{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded, retry in 1 minute"
}Rotate the signing secret
Generates a new signing secret for the webhook and returns it. Requires an API key with the full scope.
The old secret stops being used immediately: every request sent after the rotation, including retries of earlier events, is signed with the new secret. There’s no overlap period, so update the secret in your endpoint right after rotating, or accept both secrets for a short time while you switch. See Verify request signatures.
/webhooks/{id}/reset-secretPath parameters
idstringrequiredwh_…) or the webhook’s name.Returns
Returns the webhook object with the new secret (whsec_ followed by 64 hexadecimal characters). Returns 404 if the webhook doesn’t exist.
{
"object": "webhook",
"id": "wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ",
"name": "Order notifications",
"url": "https://acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced", "email.complained"],
"filter": null,
"last_used_at": "2026-10-01T12:58:40.000000+00:00",
"created_at": "2026-08-14T09:12:03.000000+00:00",
"updated_at": "2026-10-01T13:20:11.000000+00:00",
"secret": "whsec_a0a593d006bdec5aff2f21e88afd543ba239431acbe7d8dc7a9723d76e0a64e2"
}{
"error": "Webhook not found"
}Retry failed requests
Queues every request of this webhook that permanently failed in the last 7 days for delivery again. Requires an API key with the full scope.
A request fails permanently after its last automatic retry (11 attempts over several days; see Retries and failures). Retried requests start over with a full retry schedule and are delivered within seconds. If at least one request is queued and the webhook was disabled, for example after 3 days of continuous failures, it’s enabled again.
Fix your endpoint first, or the requests fail again. To retry a single request, use Retry one request.
/webhooks/{id}/retry-failedPath parameters
idstringrequiredwh_…) or the webhook’s name.Returns
retriedinteger0 if there was nothing to retry; the webhook’s enabled state doesn’t change then.Returns 404 if the webhook doesn’t exist.
{
"retried": 37
}{
"error": "Webhook not found"
}Retry one request
Queues one permanently failed webhook request for delivery again, with a fresh retry schedule. If the webhook was disabled, it’s enabled again. Requires an API key with the full scope.
Only requests that have exhausted their automatic retries can be retried this way; requests that are still pending or retrying return 400. Find request IDs (whr_…) on the webhook’s Requests tab under Email APIWebhooks. To retry everything from the last 7 days at once, use Retry failed requests.
/webhooks/{id}/requests/{request_id}/retryPath parameters
idstringrequiredwh_…) or the webhook’s name.request_idstringrequiredwhr_…).Returns
retriedinteger1.idstring| Status | When |
|---|---|
400 |
The request hasn’t permanently failed, or it has no event to resend. |
404 |
The webhook doesn’t exist, or the request doesn’t belong to it. |
{
"retried": 1,
"id": "whr_3tWVVMd2eHv3R9B5DWTLtwwU05X"
}{
"error": "Only permanently failed requests can be retried"
}{
"error": "Webhook request not found"
}