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
{
"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
{
"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:
{
"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
{
"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
}
}
}
Field Type Description bounce.idinteger or null The bounce record. Set for transactional emails; null for campaigns and workflows. bounce.categorystring bounce_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_messagestring The receiving server's diagnostic. bounce.source_ipstring The platform address that sent the message. contactobject id (integer), email, and status, which is always null here. Transactional: email only.listobject Campaign: id and name. Workflow: id, with name always null. Transactional: both null. campaignobject Campaign only: type ("campaign"), id, name. automationobject Workflow 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_emailobject Transactional only: id (a string), sender (name, email), envelope_from, and additional_headers, a list of name/value objects (or null). segment, contextobject Always 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
{
"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:
{
"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"
}
}
}
Field Type Description event_timeinteger When the bounce was recorded, in Unix seconds. email.idstring The id that POST /v2/emails returned. contactobject Present when the message went to a list contact: id, email, status, subscribed_on, custom_attributes, tags, interests. listobject Present 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
{
"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
{
"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
{
"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
}
}
}
Field Type Description fbl.providerstring The mailbox provider that reported the complaint, such as hotmail, yahoo or aol. fbl.source_ipstring The platform address that sent the message. contactobject id, email, status (usually spam). id is a string here. Transactional: email only.listobject id and name. id is a string here. Transactional: both null.segmentobject The targeted segment (id, name, query), or null values. campaign, automation, transactional_emailobject As in Email.Bounced . contextobject Always present, with null values.
Email API
{
"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.
{
"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)"
}
}
}
Field Type Description event_timeinteger When the recipient unsubscribed, in Unix seconds. email.idstring The id that POST /v2/emails returned. contactobject id, email, status, subscribed_on, custom_attributes, tags, interests. status is read while the unsubscribe is processed, so it can still be active.listobject The list the recipient left: id, name. Always present. contextobject The 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.
{
"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)"
}
}
}
Field Type Description event_timeinteger When the recipient unsubscribed, in Unix seconds. email.idstring The id that POST /v2/emails returned. contextobject The 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.
{
"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:
{
"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"
}
}
Field Type Description reasonstring Why 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_timeinteger When the message was refused, in Unix seconds. email.idstring The id that POST /v2/emails returned. contact, listobject Present 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.
{
"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"
}
}
Field Type Description errorstring The error. It starts with internal error;. event_timeinteger When the error happened, in Unix seconds. email.idstring The 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.
Unsubscribed through a link
{
"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
{
"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"
}
}
}
Field Type Description listobject The list the contact left: id, name. contactobject id, email, status (unsubscribed or deleted), subscribed_on, last_bounce_type, bounces_count, custom_attributes.contextobject timestamp: 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.
{
"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"
}
}
}
Field Type Description suppressedEmail.emailstring The 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:
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 September 29, 2026