Send and receive data with webhooks
UpdatedThe Send and receive data action represents a webhook. Webhooks let you pass data to, and return data from, just about any public API on the internet. You can use it to update a person’s attributes in Customer.io, update your CRM, or generally take action on a customer.
Here’s James, a Senior Solutions Architect at Customer.io, with a quick explanation to help you understand webhook actions and how you can use them to personalize messages for your audience.
You can trigger webhooks without a campaign
Our shortcuts feature lets you manually trigger external APIs for anybody on your People page without entering that person into a campaign. You might do this to manually activate or deactivate a person’s account, to open a help desk ticket, etc.
Add a webhook action
In the workflow, drag Send and receive data—the webhook action block—into your campaign. You can modify a number of settings on the left-hand panel, like whether you want to track conversions, or proceed to create your webhook. Click Add Request to enter the message composer.
Set your request
HTTP Request Types
Customer.io webhook actions support 5 common HTTP request types for RESTful APIs:
- GET
- POST
- PUT
- DELETE
- PATCH
The one you use is dependent on what the API you’re using is expecting. POST is the default.
Request URL
Add the URL that you want us to send the request to.
If your API requires Basic Authentication, you’ll add the username and password to the request like: https://user:pass@api.example.com
(Optional) Allowlist our IP addresses
If you have firewalls or rules in place that only allow connections from specific IPs, add Customer.io IP addresses to your allowlist so your systems can receive connections from us.
| US Region | EU Region |
|---|---|
| 35.188.196.183 | 34.76.143.229 |
| 104.198.177.219 | 34.78.91.47 |
| 104.154.232.87 | 34.77.94.252 |
| 130.211.229.195 | 35.187.188.242 |
| 104.198.221.24 | 34.78.122.90 |
| 104.197.27.15 | 35.195.137.235 |
| 35.194.9.154 | 130.211.108.156 |
| 104.154.144.51 | 104.199.50.18 |
| 104.197.210.12 | 34.78.44.80 |
| 35.225.6.73 | 35.205.31.154 |
| 35.192.215.166 | |
| 34.170.204.100 |
Set your headers
You can customize the headers that Customer.io sends with each request:
- Specify the Content-Type
- Support other types of Authentication
- Set headers required by your API endpoint
Content-Type
By default, we pre-fill the request header with Content-Type: application/json but you can modify it. This header indicates the original content type of your request before any encoding. It’s best practice to accept and return JSON with a REST API request. Therefore, we recommend keeping this header.
Here are other example Content-Type values:
Learn more about Content-Type headers.
Idempotency
We add X-CIO-Idempotency-Key to request headers. This is useful for preventing duplicate requests from creating multiple records in a system. If the same request is sent multiple times, the result will be the same as if it were only sent once.
You may see the value more than once in these situations:
- Your server doesn’t respond with a 20x, but still accepts the request, so we automatically retry the action.
- You manually re-send the webhook within the UI.
Securely verify requests
For security purposes, webhooks include X-CIO-Signature in the header. This signature combines your webhook signing key with the body of the 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 webhooks in your workflows (also known as Send and receive data actions), you can find the signing key in Settings > Workspace Settings > API & Webhook Credentials under the Webhook signing keys tab.
To validate a signed request, follow these steps:
- Copy the
X-CIO-Timestampheader sent with the webhook. - Combine the version number, timestamp, and body, delimited by colons. It should form a string like this:
v0:<timestamp>:<body>, where the version number is alwaysv0. - Hash the string in the HMAC SHA256 standard using your webhook signing secret as the hash key.
- Compare this hashed string to the
X-CIO-Signaturevalue sent with the request. If it came from Customer.io, they will match.
Always use the request’s raw body to construct the hash
Do not use any transformations such as JSON.stringify() (Node.js) or json.dumps() (Python) as there are subtle differences between parsing libraries.
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
}
Structure your request body
JSON (recommended)
By default, we add Content-Type: application/json to your header. Unless you’re not using JSON, keep this header and make sure your request includes fully formed JSON. It is best practice to accept and return JSON with a REST API request. Therefore, we recommend using the header Content-Type: application/json whenever possible.
- If you’re new to JSON, check out our introduction.
- To test your JSON, try out http://jsonlint.com/.
Here’s a simple example of valid JSON:
{
"id":"1",
"email":"win@customer.io"
}
You’d pull in a customer’s id and email by adding liquidA syntax that supports variables, letting you personalize messages for your audience. For example, if you want to reference a person’s first name, you might use the variable {{customer.first_name}}..
{
"id":"{{customer.id}}",
"email":"{{customer.email}}"
}


You could also include the entire customer object in JSON format if you want your destination to have more insight into the customer.
{
"customer":
{{ customer | replace: "=>", ":"}}
}
Add liquid filters to clean up JSON
When working with webhooks or sending JSON data, make sure your JSON is clean and valid, especially if you’re personalizing data with liquid. You’ll want to avoid double quotations and line breaks within attribute values because some services will reject the request.
To ensure liquid pulls in valid JSON, you can use filters:
strip—This removes all whitespace, including tabs, spaces, and new lines, from the left and right side of a string.strip_newlines—This removes line breaks (\n) from a string.escape—This removes special characters from a string.normalize_whitespace—This replaces any occurrence of whitespace with a single space.
Check out our liquid syntax list for more filters.
Form-encoded
If you want to send a form-encoded webhook, you’ll set the Content-Type header as x-www-form-urlencoded.
Then make sure the request body is valid form-encoded text:
id=1&email=win@customer.io&custom_attribute=value
In the webhook editor, you’d use liquid to include customer attributes:
id={{customer.id}}&email={{customer.email}}&custom_attribute={{customer.another_attribute}}
The downside of form encoding is that it’s a bit harder to structure and read since there are no line breaks or separation other than the “&” characters that join everything together.
Response: set attributes
When sending requests to an endpoint, you can choose to set attributes on people based on the response data. This allows you to retrieve data from an endpoint to then use in the content of your messages down the road.
Select the Response tab then click Set up an attribute.


Select the attribute that you want to set using the dropdown. Then use the response variable and liquidA syntax that supports variables, letting you personalize messages for your audience. For example, if you want to reference a person’s first name, you might use the variable {{customer.first_name}}. to define the value in the editor. Here are a few examples of things you might do with a webhook response:
After creating a new lead in your CRM, sync the lead_id returned in the response with your Customer.io lead.

Increment the lifetime_value of a person based on their purchases.

People wait to move forward until attribute updates are complete
If you configure a webhook to update attributes in the Response tab, a person will not move to the next step of the workflow until the attribute update is complete (successful or all retries have failed). This ensures that any subsequent actions that are dependent on this update work as expected.
Test your requests
If you’d like to test the format of your requests before pointing them at the correct API, you can use a service like Webhook.site. It generates a URL which you then paste into the request URL field. We strongly recommend only using test data with any third-party service you do not have a trusted relationship with.
If the message includes redacted data (that is, an admin has hidden sensitive attribute values from you), then test sends will not show the values for those sensitive attributes.
Manage webhook settings
Update sending behavior
By default, webhooks are set to Draft. This means you must manually send the webhook. Update this to Send automatically if you want it to send when a person reaches this step in the workflow. You’ll need to set up your request before you can update this to “Send automatically.”


Track conversions
If you want your webhook to count towards conversion metrics, you can allow conversions when you create a webhook action. By default, we don’t track conversions based on webhooks because they are often internal or used for analytics purposes; they typically don’t send messages to end-users.
To allow conversions for a webhook action:
- Within a campaign, select your webhook action.
- On the panel, click Settings.
- Turn on the toggle “Track conversions.”


- Save your changes.
You need to do this for any webhook that you want contributing to conversions; you cannot globally enable webhook conversions.
You then need to make sure your goal has specific conversion criteria for us to track conversions based on webhooks:
- Click the name of the campaign.
- Click Manage under Goal.
- Specify that a conversion is counted when a person matches the conversion criteria within a timeframe of being sent a message. This is because you can’t “open” a webhook.


Wait before continuing journey
If you’d like people to wait to move forward in a campaign until a webhook action is sent or all retries are exhausted, you’ll need to adjust the settings of your webhook action.
Click your webhook then click Settings. Turn on Wait before continuing journey. Then save your changes.


This is helpful when the next step in your campaign depends on something happening in another tool or service you use. This is on by default if your webhook sets attributes.
Timeouts and failures
We have a 16-second timeout for webhook requests. If the request times out or returns a 408, 409, 429, or 5xx response code in that period, Customer.io will retry your request up to 11 times over a period of approximately 1 hour.
If the request returns code 429, we’ll look at the previous requests to automatically determine a rate limit. We calculate and adjust the rate limit based on responses from the previous 10 minutes. We give the rate a bit of breathing room so the rate limit adapts if you increase your endpoint’s throughput.
If all retries fail, any attribute update set in the Response tab will not execute, and the person will move to the next step of the workflow.
In a campaign or broadcast, you can manually retry failed webhooks from the Sent tab. When you retry a webhook, the system sends a new request with the latest template content and attribute values, if applicable. The latest content and attribute values could be different from the original request, but represents the most up-to-date information.
If you wish to temporarily block our servers, our IP addresses are above.
Response size
The response size is limited to 10 KB. If the response is larger than 10KB, the webhook will fail.


If your webhook is over 10 KB, consider sending an event instead which allows for larger sizes. In a campaign, you can add a Send event action, which has a limit of 100 KB.
Examples
For all of the following examples, you must create an account at the partner’s site first to use them in a webhook request in Customer.io.
Create a Zendesk Ticket
With webhooks, you can create a ticket in Zendesk. Here’s an example where someone canceled their account and provided feedback. We use the event and feedback to create a ticket in Zendesk.
Request Header: https://[YOUR-EMAIL-ADDRESS]:[YOUR-PASSWORD]@[YOUR-ZENDESK-SUBDOMAIN].zendesk.com/api/v2/tickets.json
Request Body:
{
"ticket": {
"subject": "{{customer.company_name}} has canceled their account.",
"comment": { "body":"{{event.feedback}}" },
"requester": { "name":"{{customer.firstName"}} {{customer.lastName"}},"email":"{{customer.email}}"
}
}
}
You can find more Zendesk webhooks and available fields here.
Send Data to Zapier
Zapier is one of the most flexible integrations we have and you might want to use it to do things like:
- Add data to a Google sheet
- Follow a person on Twitter when they enter the segment “Has twitter name”
- Add a ticket in Jira when a user performs the event “bug_reported”
- Send new, paid accounts an invitation to review your service on Trustpilot
To start, go to your Zapier account:
- Create a new Zap.
- Choose the “Webhooks” Trigger App and then select Catch Hook. You don’t need to set any options, so continue past this screen.
- Copy the Webhook URL.
Then head over to your Customer.io workspace:
- Navigate to or create your webhook action.
- Add the Zapier Webhook URL.


- Send the customer and/or event to Zapier as JSON.
Now that your data is flowing in to Zapier, you can complete the rest of their process and connect Customer.io data to any one of their integrations.
Create a Trello Card with Customer.io
Use webhooks to create a Trello card. In this example, we create a task to schedule a call with a new user.
Request Header: https://api.trello.com/1/cards?idList=[ID-OF-THE-LIST-YOU'RE-ADDING TO]&key=[YOUR-API-KEY]&token=[YOUR-TOKEN]
Request Body:
{
"name": "Schedule call with {{customer.name}}",
"due":"null",
"desc":"The customer can be reached at {{customer.email}}."
}
You can find more Trello endpoints and available fields here.
Start mailing Lob postcards with Customer.io
Send a postcard to customers with a Lob webhook!
Request Header: https://[API-KEY]:@api.lob.com/v1/postcards
Request Body:
{
"description": "What a fancy postcard",
"to": {
"name": "{{ customer.first_name }} {{ customer.last_name }}",
"address_line1": "{{ customer.address_1 }}",
"address_line2": "{{ customer.address_2 }}",
"address_city": "{{ customer.city }}",
"address_state": "{{ customer.State }}",
"address_zip": "{{ customer.zip }}",
"address_country": "US"
},
"from": {
"name": "Your Company",
"address_line1": "123 Main St",
"address_line2": "Ste 300",
"address_city": "Portland",
"address_state": "OR",
"address_zip": "97205",
"address_country": "US"
},
"front": "http://userimg.customeriomail.com/WimaKyKwRvusmPIsxsUm_Ian1.jpg",
"back": "<html style='padding: 1in; font-size: 18;'>Hi {{ customer.first_name }}, Thanks for being our customer!</html>"
}
If you’re trying to send a letter or a check, more endpoints are in the Lob documentation.
Start sending data with IFTTT
IFTTT is a great option for connecting to devices, web apps, or services for things like:
- Turning on a light in your office when you get a new “plan_enterprise” user
- Creating a calendar event to follow up with users who enter the “Signed Up” segment
- Emailing your success team when a user triggers “payment_failed”
An example request that sends an email based on an event would look like this:


The event in the request header and the value1, value2, and value3 in the request body would then populate the email you had drafted in IFTTT.


Create a Help Scout conversation
Easily create Help Scout conversations from Customer.io:
Request Header: https://[API-KEY]:@api.helpscout.net/v1/conversations.json
Request Body:
{
"type": "email",
"customer": {
"email": "{{customer.email}}",
"firstName":"{{customer.firstName}}",
"lastName":"{{customer.lastName}}"
},
"subject": "Low NPS rating from customer {{customer.firstName}} {{customer.lastName}}",
"mailbox": {
"id": MAILBOX-ID
},
"tags": [
"Customer.io"
],
"status": "active",
"createdAt": "{% assign current_time = 'now' %}{{current_time | date: "%F"}}",
"threads": [
{
"type": "customer",
"createdBy": {
"email": "{{customer.email}}",
"type": "customer",
"firstName":"{{customer.firstName}}",
"lastName":"{{customer.lastName}}"
},
"body": "{{customer.comment}} \n \n Rating: {{customer.NPS_rating}}",
"status": "active",
"createdAt": "{% assign current_time = 'now' %}{{current_time | date: "%F"}}"
}
]
}
Replace MAILBOX-ID with your own and adapt any attributes to your own data. customer, subject, mailbox, and threads are mandatory fields.
Please be aware that free plans don’t allow API access, and only owners and administrators are able to create API keys.
Create a Lead in Close
Close is a streamlined sales platform that helps you close more deals. With built-in calling and automatic email tracking, you can focus on selling rather than data entry. In our example, we’ll show you how to send leads from Customer.io to Close, using our segment triggers. If you don’t already have an account, you can create one at Close. At any point, you can read all about adding a lead to Close via their API. Their docs include details on custom fields, give you example data, and more.
The simplest case is to push a lead into Close for every signup that enters Customer.io.
Request Header: https://YOUR-API-KEY@api.close.com/api/v1/lead/
Request Body:
{
"name": "{{customer.company}}",
"url": "{% if customer.company_url.size > 0 %}{{customer.company_url}}{%else %}mycompany.com{% endif %}",
"description": "{{customer.bio}}",
"contacts": [
{
"name": "{{customer.first_name}} {{customer.last_name}}",
"title": "{{customer.jobTitle}}",
"emails": [
{
"type": "office",
"email": "{{customer.email}}"
}
],
"phones": [
{
"type": "office",
"phone": "{{customer.phone}}"
}
]
}
],
"custom": {
"Source": "{% if customer.source.size > 0 %}{{customer.source}}{%else %}Unknown{% endif %}"
},
"addresses": [
"{{customer.address}}"
]
}
