Reporting Webhooks
Reporting Webhooks send real-time message activity events (e.g. sends, opens, clicks) as JSON in an HTTP POST. They’re useful in many cases, including analyzing message activity outside of Customer.io.
You can find information about each individual event type on our Reporting Webhooks API documentation.
Looking to send a custom webhook to a specific API via your workflow?
Check out Webhook Actions, instead.
Setup
Log in and go to Data & Integrations > Integrations.
Find and select Reporting Webhooks.
Click Add Reporting Webhook.
Enter the Webhook Endpoint URL where you want to receive events. While the endpoint URL can be either HTTP or HTTPs, we recommend HTTPS to protect customer information.
Select the events you want to receive.
(Optional) Select the Send Frequency and Body Content options.
- Send Frequency: This determines whether you receive events the first time they happen or every time they happen.
- Body Content: Enable this to include message body content (and email headers) in all of the “Sent” events we send to you.
Choose Save and Enable Webhook at the bottom right of the page.


Disabling webhooks
If you want to stop sending webhook events, you can disable the webhook.
- Go to Data & Integrations > Integrations.
- Go to Reporting Webhooks.
- Select your webhook and click Disable.


You can also edit your webhook, change the state to disabled, and click Save.
Test a webhook
To inspect Webhook Events before pointing them at your own servers, use a service like webhook.site.
Warning
Enabling a webhook endpoint can cause you to send sensitive data to an external recipient. We recommend creating a test/sandbox workspace to test your webhooks so that you don’t inadvertently leak sensitive data to a potentially unsecured endpoint.
To send a test event, press Send Test while editing your Webhook Endpoint.


Events
The following events are available via webhook:
Name | Description |
---|---|
customer_subscribed | A person’s “unsubscribed” attribute was explicitly set to “false” |
customer_unsubscribed | A person’s “unsubscribed” attribute was explicitly set to “true” |
cio_subscription_preferences_changed | A person changed their subscription preferences—either through our subscription center, or you changed their cio_subscription_preferences attribute values. Learn more about the subscription center. |
email_drafted | An email draft was created |
email_attempted | An email that couldn’t be sent to the delivery provider will be retried |
email_deferred | An email that the delivery provider couldn’t send will be retried by the delivery provider |
email_sent | An email was sent from Customer.io to the delivery provider |
email_delivered | The delivery provider reported the email was delivered to an inbox |
email_opened | An email was opened |
email_clicked | A tracked link in an email was clicked |
email_converted | A person matched a conversion goal attributed to an email |
email_unsubscribed | A person unsubscribed via a particular email |
email_bounced | The delivery provider was unable to deliver the email |
email_dropped | An email wasn’t sent because it was addressed to a person who was suppressed |
email_spammed | An email was marked as spam by the recipient |
email_failed | An email couldn’t be sent to the delivery provider |
email_undeliverable | An email was undeliverable, likely because it hit a message limit |
push_drafted | A push notification draft was created |
push_attempted | A push notification that couldn’t be sent to the delivery provider will be retried |
push_sent | A push notification was sent from Customer.io to the delivery provider |
push_delivered | A push notification was delivered to a recipient. You must use our SDKs or report delivered metrics to us using the API. |
push_opened | The app on a person’s device reported the push notification was opened. You must use our SDKs or report opened metrics to us using the API. |
push_clicked | A tracked link in a push notification has been clicked. Note that we do not track Clicked metrics or enable link tracking for push notifications by default. When a person taps a push notification, the message is marked as Opened. |
push_converted | A person matched a conversion goal attributed to a push notification |
push_bounced | The delivery provider reported at least one invalid device token |
push_dropped | A push notification wasn’t sent because at least one device token previously bounced |
push_failed | A push notification couldn’t be sent to the delivery provider |
push_undeliverable | A push notification was undeliverable, likely because it hit a message limit |
sms_drafted | An SMS draft was created |
sms_attempted | An SMS that couldn’t be sent to the delivery provider will be retried |
sms_sent | An SMS was sent from Customer.io to the delivery provider |
sms_delivered | The delivery provider reported the SMS was delivered |
sms_clicked | A tracked link in an SMS has been clicked |
sms_converted | A person matched a conversion goal attributed to an SMS |
sms_bounced | The delivery provider was unable to deliver the SMS |
sms_failed | An SMS couldn’t be sent to the delivery provider |
sms_undeliverable | An SMS message was undeliverable, likely because it hit a message limit |
slack_drafted | A slack draft was created |
slack_attempted | A slack message that couldn’t be sent will be retried |
slack_sent | A slack message was sent from Customer.io to slack |
slack_clicked | A tracked link in a slack message has been clicked |
slack_failed | A slack message couldn’t be sent to slack |
slack_undeliverable | A Slack message was undeliverable, likely because it hit a message limit |
webhook_drafted | A webhook draft was created |
webhook_attempted | A webhook that couldn’t be sent will be retried |
webhook_sent | A webhook was sent from Customer.io to the specified Webhook URL |
webhook_clicked | A tracked link in a webhook payload has been opened |
webhook_failed | A webhook couldn’t be sent to the specified Webhook URL |
webhook_undeliverable | A webhook was undeliverable, likely because it hit a message limit |
To only receive specific events, refine your selections within the nested list:
If you have a specific request for an event not listed here that you would like to be notified of, please contact us.
Webhook Attributes
Attribute | Description |
---|---|
action_id | If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the unique workflow item that caused the delivery to be created. It can be used to retrieve full message details, including content, via the Campaign endpoint of our API. |
broadcast_id | If applicable, the ID of the API Triggered Broadcast that generated the message. It can be used to retrieve message details, including the actions in your broadcast, via the Braodcast endpoint of our API. |
campaign_id | If applicable, the ID of the Event-triggered, Segment-triggered, or Date-triggered Campaign that generated the message. |
content | The body content of a sent message; if a message is an email, body content also contains headers. This can be useful if you want to display message content in your app. You must opt in to receiving content. |
content_id | If the message was part of a newsletter split test, this is the ID of the split test variation. |
customer_id | The ID of the person the webhook event represents. In a workspace supporting both email and id as identifiers, this value can be null. The value is empty if the person has been deleted. This field is generally considered deprecated. You should ignore this value and rely on the identifiers object. |
identifiers | Contains identifiers for the person the webhook event is associated with. The object is empty if a person was deleted. If your workspace supports both email and ID as identifiers, this object contains id , email , and cio_id , and both id and email can be null. If your workspace only supports ID, this object only contains id . |
delivery_id | The unique ID of the delivery record associated with the message. |
device_id | Only on push-related events, the ID of the associated mobile device. |
device_platform | Only on push-related events, the platform of the associated mobile device. |
email_address | Only on customer_subscribed and customer_unsubscribed events, this is the email address of the person. |
event_id | The unique ID of the reporting webhook event being sent. This can be useful for deduplicating purposes. |
event_type | The type of event sent (e.g. email_sent, sms_drafted). |
href | Only on “clicked” events, the fully rendered URL of the link that was clicked. |
journey_id | The ID for the path a person went through in a Campaign or API Triggered Broadcast workflow. In our Data Warehouse Sync, this is referred to as subject_id. |
link_id | Only on “clicked” events, the ID of the tracked link that was clicked. |
newsletter_id | If applicable, the ID of the Newsletter that generated the message. It can be used to retrieve full message details via the Newsletters endpoint of our API. |
recipient | The address of the message recipient. This could be an email address, a phone number, a mobile device ID, a Webhook URL, or a Slack username or channel. |
subject | For email events, this is the subject of the email. |
failure_message | If applicable, the reason a message failed to send. |
timestamp | The timestamp at which the event being reported took place. |
transactional_message_id | If a message is transactional, this is the unique identifier of the transactional message “template” that you sent (and referenced in the transactional message payload). If you send transactional messages without referencing transactional_message_id (by passing body , subject , and from values at send time), this value is 1. |
The identifiers object
Webhook payloads include an identifiers
object. This object contains the unique identifiers for the person your event represents.
The items that the identifiers
object can contain are defined by your workspace settings. If your workspace supports both email
and id
as identifiers, the identifiers block contains both (though either can be null if not set) and a cio_id
—a unique, immutable identifier set by Customer.io to identify people canonically across changes to their other identifiers.
If your workspace uses ID as its only identifier, the identifiers
object only contains the id
.
{
"data": {
"action_id": 36,
"broadcast_id": 9,
"customer_id": "abcd-1234",
"identifiers": {
"cio_id": "03000001",
"id": "abcd-1234",
"email": "test@example.com"
},
"delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh",
"recipient": "test@example.com",
"subject": "hello"
},
"event_id": "01E4C8AY5K21N2QNRBD9YXJ13Z",
"object_type": "email",
"metric": "sent",
"timestamp": 1585254331
}
Format
Go to our new webhook documentation for examples of each event type.
Customer.io Webhooks are HTTP POST requests encoded in JSON. The requests have a User Agent header containing “Customer.io Web Hooks x.x” where “x.x” is the version number.
The JSON body contains a general top-level section included in all webhook requests, as well as a “data” attribute, which contains data specific to the type of event.
Below is an example of an HTTP request for an email-related event:
User-Agent: Customer.io Web Hooks 1.0
Host: webhook.site
Content-Type: application/json
Accept-Encoding: gzip
Cf-Connecting-Ip: 167.114.157.9
X-Request-Id: 7e6f46cd-480e-4354-93ce-74b770015c7f
Connect-Time: 1
Content-Length: 1100
Cf-Visitor: {"scheme":"http"}
Total-Route-Time: 0
Cf-Ipcountry: CA
Cf-Ray: 2b62237a83a12507-ORD
Connection: close
Via: 1.1 vegur
RAW BODY
{
"data": {
"action_id": 36,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
"email": "test@example.com",
"cio_id": "d9c106000001"
},
"delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh",
"recipient": "test@example.com",
"subject": "hello"
},
"event_id": "01E4C8AY5K21N2QNRBD9YXJ13Z",
"object_type": "email",
"metric": "clicked",
"timestamp": 1585254331
}
Timeouts and Failures
We have a 4 second timeout and if we don’t get a successful (2xx) response during those 4 seconds, Customer.io will retry sending the notification over a period of 7 days with an exponential backoff. We will not move past a failing webhook until we get a successful (2xx) response or until 7 days have expired. Subsequent webhooks will be backlogged and will get processed after the success or failure of the webhook that failed and triggered the backlog.
If you wish to temporarily block our servers, you can look up the current set of IP addresses we use via this API endpoint.
Securely verify requests
For security purposes, webhooks are delivered with an X-CIO-Signature
header. This signature is generated by combining your webhook signing key with the body of webhook request using a standard HMAC-SHA256 hash.
For reporting webhooks, you can find the signing key on the same page you enter your webhook endpoint: Data & Integrations > Integrations > Reporting webhooks. For webhook actions, you can find the signing key in Settings > Workspace Settings > API & Webhook Credentials under the Webhook Signing Key tab.
To validate a signed request you’ll first need to retrieve the X-CIO-Timestamp
header sent with the webhook request and the body of the request. Combine the version number, timestamp, and body—delimited by colons—to form a string like v0:<timestamp>:<body>
(the version number is always v0
). Using HMAC SHA256, hash the string using your webhook signing secret as the hash key. Compare this value to the value of the X-CIO-Signature
header sent with the request to confirm that the request originated with Customer.io.
Here’s an example of a validation function in Golang.
import (
"encoding/hex"
"crypto/hmac"
"crypto/sha256"
"strconv"
"fmt"
)
func CheckSignature(WebhookSigningSecret, XCIOSignature string, XCIOTimestamp int, RequestBody []byte) (bool, error) {
signature, err := hex.DecodeString(XCIOSignature)
if err != nil {
return false, err
}
mac := hmac.New(sha256.New, []byte(WebhookSigningSecret))
if _, err := mac.Write([]byte("v0:" + strconv.Itoa(XCIOTimestamp) + ":")); err != nil {
return false, err
}
if _, err := mac.Write(RequestBody); err != nil {
return false, err
}
computed := mac.Sum(nil)
if !hmac.Equal(computed, signature) {
fmt.Println("Signature didn't match")
return false, nil
}
fmt.Println("Signature matched!")
return true, nil
}
Frequently Asked Questions
- Can webhooks contain the message body? Yes! We can send the message body for all channels, but you must opt in to this option during setup.
- How can I secure webhooks? It’s possible to add basic authentication in the Webhook Endpoint URL field (e.g. http://username:password@example.com).
- How do you identify each message that is going out? Each message sent from Customer.io has a
delivery_id
unique identifier that is also part of the default unsubscribe link:https://track.customer.io/unsubscribe/MjYyMTI6Fs_YAmQAAnMAFeEaAU2YoV7tFRoYVh6HYAFzOjIyOTkwMQA=
To view a particular message in the UI, select Deliveries & Drafts from the left panel, click under the “Action” column for any message in the list, then replace the end of the URL in your browser with the delivery_id
you’re interested in. For example:
https://fly.customer.io/env/26212/outbox/deliveries/MjYyMTI6Fs_YAmQAAnMAFeEaAU2YoV7tFRoYVh6HYAFzOjIyOTkwMQA=
The delivery_id
is also displayed under the Metadata details in the right-hand column of the page.
- Can I specify which campaign(s) get forwarded to an external webhook? No. If you need to monitor only a specific campaign, however, it’s possible to handle the logic on your end to filter out unwanted webhook events based on
campaign_id
. - Can I get a webhook when a customer gets added to a segment? No. But if you want to pull a list of people in a given segment, you can do so using the
/segments/:id/membership
endpoint of our API. Alternatively, you could create a segment-triggered campaign based on the segment you’re interested in, set the email inside to “Queue Draft” and then monitor theemail_drafted
events for thatcampaign_id
. - Do you send an event for each click performed by a user? By default, only the first click event is sent. If you wish to have each click recorded, you can set the Send Frequency on your Webhook Endpoint accordingly:
- Can Reporting Webhooks be rate limited? By default, we only send one event per action (sent, opened, clicked, etc.) to limit the output. No further rate limiting is available.
- Is it possible to host a webhook endpoint in Customer.io? No. For incoming data, you’ll need to use our REST API or our Segment integration.
Webhook Event Examples
We’ve moved webhook examples, including details about the schema for each individual webhook event, to our API documentation.
Customer event examples
{
"data": {
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"email_address": "test@example.com"
},
"event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ",
"object_type": "customer",
"metric": "subscribed",
"timestamp": 1585250199
}
Email event examples
{
"data": {
"action_id": 36,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh"
},
"event_id": "01E4C4G1S0AMNG0XVF2M7RPH5S",
"object_type": "email",
"metric": "drafted",
"timestamp": 1585250305
}
Push notification event examples
{
"data": {
"action_id": 37,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY"
},
"event_id": "01E4C4G1S0HZ7C4220T6QNY8JX",
"object_type": "push",
"metric": "drafted",
"timestamp": 1585250305
}
In-App message event examples
{
"event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ",
"object_type": "in-app",
"timestamp": 1613063089,
"metric": "drafted",
"data": {
"trigger_id": 1,
"customer_id": "42",
"delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==",
"action_id": 96,
"broadcast_id": 2,
"journey_id": "01GW20GXAAXBKZD8J96M8FNV3R",
"parent_action_id": 1,
"identifiers": {
"id": "42",
"email": "test@example.com",
"cio_id": "d9c106000001"
}
}
}
SMS event examples
{
"data": {
"action_id": 38,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"delivery_id": "RPILAgIBcRhIBqSZNqzgZVFoivwW"
},
"event_id": "01E4C4G1S02P8D0G2JMY88KAFN",
"object_type": "sms",
"metric": "drafted",
"timestamp": 1585250305
}
Slack event examples
{
"data": {
"action_id": 39,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"delivery_id": "RPILAgQBcRhIBqRiZAc0fyQiLvkC"
},
"event_id": "01E4C4G1S0T3Y4V8W7F6MNFA8S",
"object_type": "slack",
"metric": "drafted",
"timestamp": 1585250305
}
Webhook example events
{
"data": {
"action_id": 40,
"broadcast_id": 9,
"customer_id": "0200102",
"identifiers": {
"id": "0200102",
},
"delivery_id": "RPILAgEBcRhIBqSrYcXDr2ks6Pj9"
},
"event_id": "01E4C4G1S04QCV1NASF4NWMQNR",
"object_type": "webhook",
"metric": "drafted",
"timestamp": 1585250305
}
Legacy Email Webhook Format
On April 8, 2020, we streamlined our email webhook payloads, removing unneeded data in order to improve our processing speed and reliability.
If you had Reporting Webhooks enabled before April 8, 2020, the old email webhook payload remains unchanged.
The example below covers any of the email-related activity:
User-Agent: Customer.io Web Hooks 1.0
Host: webhook.site
Content-Type: application/json
Accept-Encoding: gzip
Cf-Connecting-Ip: 167.114.157.9
X-Request-Id: 7e6f46cd-480e-4354-93ce-74b770015c7f
Connect-Time: 1
Content-Length: 1100
Cf-Visitor: {"scheme":"http"}
Total-Route-Time: 0
Cf-Ipcountry: CA
Cf-Ray: 2b62237a83a12507-ORD
Connection: close
Via: 1.1 vegur
RAW BODY
{
"data": {
"campaign_id": "1000002",
"campaign_name": "Upgrade to Premium",
"customer_id": "98513",
"email_address": "customer@example.com",
"email_id": "NTE4MzE6FwGLxwJkAAJkABcBIfcaAVVvdGukFUsYV2hY6QFlOjQ4YTZhODljLTM3MjktMTFlNi04MDQwLTYzNGY3NzAzM2NhNjozNDMwMzEA",
"message_id": "1000013",
"message_name": "First Upgrade Email",
"subject": "Have any doubts?",
"template_id": "343031",
"variables": {
"attachments": null,
"customer": {
"created_at": 1466453747,
"email": "customer@example.com",
"id": 98513,
"name": "John Doe",
"plan_name": "free"
},
"email_id": "NTE4MzE6FwGLxwJkAAJkABcBIfcaAVVvdGukFUsYV2hY6QFlOjQ4YTZhODljLTM3MjktMTFlNi04MDQwLTYzNGY3NzAzM2NhNjozNDMwMzEA",
"event": {
"page": "https://customer.io/pricing/"
},
"event_id": "48a6a89c-3729-11e6-8040-634f77033ca6",
"event_name": "viewed_pricing_page",
"from_address": null,
"recipient": null,
"reply_to": null
}
},
"event_id": "b50cb221c60f87cdf06e",
"event_type": "email_drafted",
"timestamp": 1466456299
}
Legacy Email Webhook Attributes
campaign_id and campaign_name: refer to the transactional message, segment-triggered campaign or newsletter that generated the email
customer_id: user id (can be retrieved from the person profile). Only present if the person is still active (not included if the person has been deleted).
email_address: “To” email address
email_id: unique message id (each individual message sent from Customer.io has a different “email_id”); can also be found in the unsubscribe link URL
event: specific to event-triggered campaigns; includes all the event attributes
event_id (data section): specific to event-triggered campaigns; id of the event that generated the message (not visible in the UI)
event_id: internal attribute; id associated with the email_type action
event_name: specific to event-triggered campaigns; name of the event that powers the campaign
event_type: type of event (“email_drafted”, “email_sent”, etc.)
from_address: specific to anonymous event-triggered campaigns; from_address set via the event
href and link_id: specific to “email_clicked” events
href: first URL clicked by the user
link_id: internal attribute (not visible in the UI)
message_id: campaign email id; can be found in the campaign URL after emails/ (e.g. https://fly.customer.io/env/51831/v2/composer/emails/225039)
message_name: the name of the campaign email
reason: specific to the “email_bounced” and “email_dropped” events, mentions the cause of the bounce/suppression (e.g.: Invalid)
recipient: specific to anonymous event-triggered campaigns; email address of a user that does not exist inside Customer.io
reply_to: specific to anonymous event-triggered campaigns; reply_to address set via the event
subject: email subject
template_id: internal attribute, each email inside a campaign can have multiple template ids depending on the changes made over time. You can view it in the UI by filtering for a specific email under Email Log. For example: https://fly.customer.io/env/51831/email_logs?campaign=139744&template=343216
timestamp: date and time when the event took place in unix (seconds since epoch) format
variables:
- attachments: specific to transactional emails with small attachments (e.g. .ics files)
- customer: all the attributes associated with your user