Cakemail
Webhooks

Compliance events

Compliance events tell you when a recipient can't receive your email or doesn't want it: bounces, spam complaints, unsubscribes, and messages the Email API refused or failed to queue. Subscribe to them to keep your own records in step with the platform's decisions. A webhook for a compliance event can point to any URL.

Every example below is generated from the platform's code and is the exact body your URL receives. The values are placeholders. The fields every event shares (id, event, account_id, ip, user, created_on) are described in the payload envelope. This page describes data.

The shape of data depends on how the email was sent:

  • Campaign: a campaign.
  • Workflow: an email sent by a workflow (the /workflows endpoints), including the confirmation email of a double opt-in list.
  • Transactional: an email sent with the deprecated POST /emails endpoint.
  • Email API: an email sent with POST /v2/emails. Automations send this way too, so their events have the Email API shape.

Email.Bounced

Fires when a message bounces, whichever way it was sent.

Campaign

JSONCode
{ "id": "570bfeff-8a80-5f41-bb57-f2f343e49381", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "bounce": { "id": null, "dsn_message": "smtp; 550 5.1.1 <jane@example.com>: Recipient address rejected: User unknown", "source_ip": "192.0.2.10", "category": "bounce_hb" }, "campaign": { "type": "campaign", "id": 4321, "name": "September newsletter" }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "contact": { "id": 5678, "email": "jane@example.com", "status": null }, "list": { "id": 111, "name": "Newsletter subscribers" }, "segment": { "id": null, "name": null, "query": null } } }

Workflow

JSONCode
{ "id": "ba5e6f52-aa7d-5c17-a238-29ca5a7fe955", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "bounce": { "id": null, "dsn_message": "smtp; 452 4.2.2 Mailbox full", "source_ip": "192.0.2.10", "category": "bounce_sb" }, "automation": { "trigger": { "id": 2222, "name": "Welcome series step 1" } }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "contact": { "id": 5678, "email": "jane@example.com", "status": null }, "list": { "id": 111, "name": null }, "segment": { "id": null, "name": null, "query": null } } }

A double opt-in confirmation email names the system action instead of a trigger:

JSONCode
{ "id": "f0834bac-2a2b-59e6-9849-eb035d2607d2", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "bounce": { "id": null, "dsn_message": "smtp; 550 5.1.1 User unknown", "source_ip": "192.0.2.10", "category": "bounce_hb" }, "automation": { "workflow": { "id": null, "name": null }, "action": { "id": null, "name": null }, "system_action": { "type": "douopt-in", "id": 3333, "name": "list_111_douopt_in" } }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "contact": { "id": 5678, "email": "jane@example.com", "status": null }, "list": { "id": 111, "name": null }, "segment": { "id": null, "name": null, "query": null } } }

Transactional

JSONCode
{ "id": "e4bb5dcb-bacc-524c-a881-3d3dcc2d07bb", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "id": 987657, "email": "jane@example.com", "sent_id": "445566", "sender_name": "Acme Billing", "sender_email": "billing@acme.example", "source_ip": "192.0.2.10", "envelope_from": "relay_1234.77.445566.123456789@bounce.example", "dsn_message": "smtp; 550 5.1.1 User unknown", "additional_headers": [ { "name": "X-Order-Id", "value": "A-1001" } ], "contact": { "email": "jane@example.com" }, "transactional_email": { "id": "445566", "sender": { "name": "Acme Billing", "email": "billing@acme.example" }, "envelope_from": "relay_1234.77.445566.123456789@bounce.example", "additional_headers": [ { "name": "X-Order-Id", "value": "A-1001" } ] }, "bounce": { "id": 987657, "dsn_message": "smtp; 550 5.1.1 User unknown", "source_ip": "192.0.2.10", "category": "bounce_hb" }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "list": { "id": null, "name": null }, "segment": { "id": null, "name": null, "query": null } } }
FieldTypeDescription
bounce.idinteger or nullThe bounce record. Set for transactional emails; null for campaigns and workflows.
bounce.categorystringbounce_hb hard bounce, bounce_sb soft bounce, bounce_fm mailbox full, bounce_mb blocked, bounce_tr transient failure, bounce_df DNS failure, bounce_ar automatic reply, bounce_cr challenge-response, bounce_ac address change.
bounce.dsn_messagestringThe receiving server's diagnostic.
bounce.source_ipstringThe platform address that sent the message.
contactobjectid (integer), email, and status, which is always null here. Transactional: email only.
listobjectCampaign: id and name. Workflow: id, with name always null. Transactional: both null.
campaignobjectCampaign only: type ("campaign"), id, name.
automationobjectWorkflow only. trigger (id, name) for a workflow email. For an email that a list event sends, such as the double opt-in confirmation, system_action: type is that event, such as douopt-in or opt-in, plus id and name. workflow and action are then present but null.
transactional_emailobjectTransactional only: id (a string), sender (name, email), envelope_from, and additional_headers, a list of name/value objects (or null).
segment, contextobjectAlways present, with null values.
id, email, sent_id, sender_name, sender_email, source_ip, envelope_from, dsn_message, additional_headersTransactional only: an older, flat copy of the same values. Read bounce and transactional_email instead.

Email API

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000001", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "contact": { "id": 5678, "email": "jane@example.com", "status": "active", "subscribed_on": 1735689600, "custom_attributes": [ { "name": "first_name", "value": "Jane" } ], "tags": [ "vip" ], "interests": [ "news" ] }, "list": { "id": 111, "name": "Newsletter subscribers" } } }

When the message wasn't sent to a list contact, data identifies the email only:

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000002", "event": "Email.Bounced", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" } } }
FieldTypeDescription
event_timeintegerWhen the bounce was recorded, in Unix seconds.
email.idstringThe id that POST /v2/emails returned.
contactobjectPresent when the message went to a list contact: id, email, status, subscribed_on, custom_attributes, tags, interests.
listobjectPresent with contact: id, name.

An Email API bounce doesn't say whether it was hard or soft, or why. To find out, look the email up.

Email.ReportedAsSpam

Fires when a recipient reports a message as spam to a mailbox provider that shares complaints with the platform. For campaign, workflow and transactional messages, by the time you receive the event the contact's status is already spam and the address is on the suppression list.

Campaign

JSONCode
{ "id": "6c901b05-a966-53e2-aafe-e71f32a05aa9", "event": "Email.ReportedAsSpam", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "campaign": { "type": "campaign", "id": 4321, "name": "September newsletter" }, "fbl": { "provider": "hotmail", "source_ip": "192.0.2.10" }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "contact": { "id": "5678", "email": "jane@example.com", "status": "spam" }, "list": { "id": "111", "name": "Newsletter subscribers" }, "segment": { "id": 9, "name": "Canada", "query": "(`country` = \"CA\")" } } }

Workflow

JSONCode
{ "id": "d221bed9-06c6-56f5-8074-ee3003cade41", "event": "Email.ReportedAsSpam", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "automation": { "trigger": { "id": 2222, "name": "Welcome series step 1" } }, "fbl": { "provider": "yahoo", "source_ip": "192.0.2.10" }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "contact": { "id": "5678", "email": "jane@example.com", "status": "spam" }, "list": { "id": "111", "name": "Newsletter subscribers" }, "segment": { "id": null, "name": null, "query": null } } }

Transactional

JSONCode
{ "id": "0e5183b6-36b3-58b1-ae43-52c86448359f", "event": "Email.ReportedAsSpam", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "contact": { "email": "jane@example.com" }, "transactional_email": { "id": "445566", "sender": { "name": "Acme Billing", "email": "billing@acme.example" }, "envelope_from": "relay_1234.77.445566.123456789@bounce.example", "additional_headers": [ { "name": "X-Order-Id", "value": "A-1001" } ] }, "fbl": { "provider": "aol", "source_ip": "192.0.2.10" }, "context": { "timestamp": null, "user_agent": null, "ip": null }, "list": { "id": null, "name": null }, "segment": { "id": null, "name": null, "query": null } } }
FieldTypeDescription
fbl.providerstringThe mailbox provider that reported the complaint, such as hotmail, yahoo or aol.
fbl.source_ipstringThe platform address that sent the message.
contactobjectid, email, status (usually spam). id is a string here. Transactional: email only.
listobjectid and name. id is a string here. Transactional: both null.
segmentobjectThe targeted segment (id, name, query), or null values.
campaign, automation, transactional_emailobjectAs in Email.Bounced.
contextobjectAlways present, with null values.

Email API

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000003", "event": "Email.ReportedAsSpam", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" } } }

The Email API complaint identifies the email only: event_time and email.id. To find the recipient, look the email up.

Email.Unsubscribed

Fires only for the Email API, when a recipient unsubscribes from the list with the link or the List-Unsubscribe header of an Email API message. An unsubscribe from a campaign or workflow email fires Contact.Removed instead.

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000004", "event": "Email.Unsubscribed", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "contact": { "id": 5678, "email": "jane@example.com", "status": "active", "subscribed_on": 1735689600, "custom_attributes": [ { "name": "first_name", "value": "Jane" } ], "tags": [ "vip" ], "interests": [ "news" ] }, "list": { "id": 111, "name": "Newsletter subscribers" }, "context": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" } } }
FieldTypeDescription
event_timeintegerWhen the recipient unsubscribed, in Unix seconds.
email.idstringThe id that POST /v2/emails returned.
contactobjectid, email, status, subscribed_on, custom_attributes, tags, interests. status is read while the unsubscribe is processed, so it can still be active.
listobjectThe list the recipient left: id, name. Always present.
contextobjectThe recipient's ip and user_agent.

Email.GlobalUnsubscribed

Fires only for the Email API, when a recipient uses the link to unsubscribe from all email in an Email API message.

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000005", "event": "Email.GlobalUnsubscribed", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "context": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" } } }
FieldTypeDescription
event_timeintegerWhen the recipient unsubscribed, in Unix seconds.
email.idstringThe id that POST /v2/emails returned.
contextobjectThe recipient's ip and user_agent.

The payload has no contact and no address. The same click also fires SuppressedEmail.Added, which carries the address. You can also look the email up. A global unsubscribe from a campaign or workflow email fires SuppressedEmail.Added and Contact.Removed, not this event.

Email.Rejected

Fires only for the Email API, when POST /v2/emails refuses a message before queueing it.

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000006", "event": "Email.Rejected", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": 1002260, "email": "dev@acme.example", "account_id": 1234 }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "contact": { "id": 5678, "email": "jane@example.com", "status": "unsubscribed", "subscribed_on": 1735689600, "custom_attributes": [ { "name": "first_name", "value": "Jane" } ], "tags": [ "vip" ], "interests": [ "news" ] }, "list": { "id": 111, "name": "Newsletter subscribers" }, "reason": "unsubscribed" } }

When the message wasn't sent to a list contact:

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000007", "event": "Email.Rejected", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": 1002260, "email": "dev@acme.example", "account_id": 1234 }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "reason": "suppressed" } }
FieldTypeDescription
reasonstringWhy the message was refused: bounced, unsubscribed, pending (the contact hasn't confirmed), spam, deleted or suppressed. A message submitted with content.type set to transactional is never refused as unsubscribed.
event_timeintegerWhen the message was refused, in Unix seconds.
email.idstringThe id that POST /v2/emails returned.
contact, listobjectPresent when the message went to a list contact, as in Email.Bounced.

Unlike the other compliance events, user is set: it's the user who submitted the message.

Email.Error

Fires only for the Email API, when a submitted message hits an internal error before it's queued. The message isn't sent.

JSONCode
{ "id": "7f3c9a2e-0000-4000-8000-000000000008", "event": "Email.Error", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": 1002260, "email": "dev@acme.example", "account_id": 1234 }, "created_on": 1790431393, "data": { "event_time": 1790431391, "email": { "id": "b1e2c3d4-0000-4000-8000-00000000abcd" }, "error": "internal error; unable to queue" } }
FieldTypeDescription
errorstringThe error. It starts with internal error;.
event_timeintegerWhen the error happened, in Unix seconds.
email.idstringThe id that POST /v2/emails returned.

user is the user who submitted the message.

Contact.Removed

Fires when a contact leaves a list. That happens when the contact is unsubscribed (a link in a campaign or workflow email, the API, a list unsubscribe or a global unsubscribe), or deleted (DELETE /lists/{list_id}/contacts/{contact_id}). It doesn't fire when an import unsubscribes contacts.

JSONCode
{ "id": "4c1256b2-53f1-55b4-a33d-92e7b549b0bc", "event": "Contact.Removed", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "context": { "timestamp": 1790431391, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "ip": "203.0.113.7" }, "contact": { "id": 5678, "email": "jane@example.com", "status": "unsubscribed", "subscribed_on": 1735689600, "last_bounce_type": "none", "bounces_count": 0, "custom_attributes": [ { "name": "first_name", "value": "Jane" } ] }, "list": { "id": 111, "name": "Newsletter subscribers" } } }

Deleted by a user

JSONCode
{ "id": "71e172f5-0f1a-5ea1-86cf-a8277dfe10ce", "event": "Contact.Removed", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": 1002260, "email": "dev@acme.example", "account_id": 1234 }, "created_on": 1790431393, "data": { "context": { "timestamp": 1790431391, "user_agent": null, "ip": null }, "contact": { "id": 5678, "email": "jane@example.com", "status": "deleted", "subscribed_on": 1735689600, "last_bounce_type": "none", "bounces_count": 0, "custom_attributes": [ { "name": "first_name", "value": "Jane" } ] }, "list": { "id": 111, "name": "Newsletter subscribers" } } }
FieldTypeDescription
listobjectThe list the contact left: id, name.
contactobjectid, email, status (unsubscribed or deleted), subscribed_on, last_bounce_type, bounces_count, custom_attributes.
contextobjecttimestamp: when it happened, in Unix seconds. ip and user_agent are the contact's when they unsubscribed through a link, and null otherwise.

user is the user who unsubscribed or deleted the contact. Its values are null when the contact unsubscribed themselves.

SuppressedEmail.Added

Fires when an address is added to the suppression list. That happens when it's added with the API, and when a recipient unsubscribes from all email, from a campaign, a workflow email or an Email API message. This is the event to subscribe to if you need the address of a global unsubscribe.

JSONCode
{ "id": "3bd96742-0aa7-527a-b50a-9f260d74290d", "event": "SuppressedEmail.Added", "account_id": 1234, "account_lineage": null, "ip": "192.0.2.20", "user": { "id": null, "email": null, "account_id": null }, "created_on": 1790431393, "data": { "suppressedEmail": { "email": "jane@example.com" } } }
FieldTypeDescription
suppressedEmail.emailstringThe suppressed address. Note the camel case in suppressedEmail.

This page shows the global unsubscribe. Payloads for suppressed domains and local parts aren't documented yet.

Enriching Email API events

Email API events identify the message with data.email.id. To get the recipient and the delivery details, call Retrieve a submitted email with that id:

TerminalCode
curl --request GET \ --url 'https://api.cakemail.dev/v2/emails/b1e2c3d4-0000-4000-8000-00000000abcd' \ --header 'Authorization: Bearer <access token>'

The endpoint allows one request per second per user and account, so make these lookups from a queue, not inside your webhook handler.

Last modified on