# Customer.io Documentation — Full Content > Customer.io is a messaging automation platform for sending targeted email, push notifications, SMS, in-app messages, and webhooks based on customer behavior and data attributes. This file contains the full text of all documentation pages. --- ## Welcome to Customer.io URL: https://docs.customer.io/get-started/welcome/ While the platform offers plenty of features and options, this guide breaks down the basic things you need to set up to help you get started as quickly as possible. You'll create a workspace, set up your messaging channels, and add people! How Customer.io Works In Customer.io, you’ll spend most of your time setting up campaigns to send messages to people. But you’ll get the most out of Customer.io when you integrate your data with Customer.io and make us a central part of your martech/messaging stack. By getting your data into Customer.io, enriching it, and sending it out to other places where you activate it, you can focus on your messaging strategy without having to jump from platform to platform. Browser support Before you get started with Customer.io, make sure you’re using a compatible browser! We actively test our user interface with the latest two major versions of Google Chrome, Mozilla Firefox, Microsoft Edge, and Safari. You might find that a different browser or an earlier version works for you. But, if you run into trouble in our UI, you should try updating your browser or switch to a supported platform to make sure that you haven’t encountered a browser-based issue. Ready to get started? When you’re ready to get started, you’ll follow the basic setup path below. Your path might change depending on whether you’re comfortable with code, the kinds of messages you want to send, and so on. But the general order of operations is the same. Create your workspace. Set up your message channels—like email, push, and SMS. (Developers) Integrate with Customer.io. If you’re not a developer, you can move ahead to the next step; there are ways to use Customer.io without integrating but you’ll get the most out of Customer.io when you do. Add people and their data to Customer.io: If you’re a developer: make your first identify and track calls. If you’re not a developer: add people via CSV or manually. Capture events and create segments—both are essential for automating your messaging strategy. Set up campaign and broadcast workflows. flowchart LR z(1. Create your workspace)-->d a{Are you a developer?} d-->a subgraph d[2. Set up channels] direction LR u(Email) t(Mobile Push) s(In-App) r(SMS) end subgraph b[4. Add people] direction LR f(Upload CSV or use the UI) g(Send identify calls) end a--->|no|f a-->|yes|c subgraph c[3. Integrate Customer.io] direction LR x(Add JS to your website) w(Add server side libraries) y(Integrate mobile SDKs) v(Use Reverse ETL to connect your database) end c-->g subgraph h[5. Add Events and Segments] direction BT i(Create data-driven segments) j(Send events via track calls) j-.->i end g-->j f-->i i-->k(6. Send campaigns and broadcasts) Plan your setup path There’s a lot you can do with Customer.io, but we want to help you focus on your needs so you get the most out of Customer.io in the shortest amount of time. Before you get started, ask yourself the following questions. Your answers can help you understand what you need to do in Customer.io! Where do your “people” come from—your website, mobile app, backend database, etc? These are the data source(s) you should connect to Customer.io. What do you need to know about people? These are the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. you need to send to Customer.io. What kinds of messages do you want to send? These are the message channels you need to set up. What goals will your messages accomplish? These can help you determine the types of segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. and campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. you build! Data Sources: getting people and data into Customer.io While you can always add people and data to Customer.io manually, you’ll get the most out of Customer.io if you integrate your data sources with us. For example, if you have a website that people visit, you probably want to integrate it with Customer.io so that you can identify your audience and send messages to people based on their activities on your site! Even if you’re not a developer, knowing where your data comes from can help you know who to talk to in your organization to get the data you need into Customer.io. Common data sources include: Your website(s): use our JavaScript libraries or server-side libraries Your mobile app(s): use our mobile SDKs Your backend database(s): use our Reverse ETL integrations Attributes and Events: understanding your people and data People have data associated with them: identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.: the values that make people unique in your system—like a database id or email address. AttributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.: the things you know about a person, like their name, interests, birthdate, etc. Events: the things that people do, like visiting a page on your website, logging into your service, or making a purchase. You’ll use attributes and events to automate campaigns, personalize messages, and so on—so you’ll need to make sure that you capture the right information in Customer.io. Similarly, you don’t want to send noise—data that you don’t care about—into Customer.io if you can help it. So, as you get started, you should think about the attributes and events that you want to track and send to Customer.io. --- ## Quick start guide URL: https://docs.customer.io/get-started/quick-start-guide/ Welcome to Customer.io! While the platform offers plenty of features and options, this guide provides a basic setup path to get you started quickly and demonstrate some of the things you can do with Customer.io. You'll create a workspace, import at least one person, and set up your messaging channels! Get started This guide is designed to walk you through the most common parts of the setup path and introduce you to the major parts of our platform. When you start setting up Customer.io, we provide you with a Setup List on your Workspace’s Dashboard. You don’t have to do everything on the list, but you must do the following things before you can really take advantage of Customer.io: Create an account and a workspace. Set up the types of messages you want to send. On this page, we’ll set up email. Add people to your workspace. As a part of this guide, you’ll add yourself as a test person. Start sending messages! In this guide, you’ll add a test person and send your test person a message, demonstrating how you’ll use your data to automate and personalize messages. Create your workspace After you create your account, you’ll make a workspace. You can think of a workspace as a container for your people, data, messages, etc. You can have more than one workspace, but, in general, you won’t share information across workspaces. In general, we encourage you to use the default settings. They support most use cases and you can change them later! Set up your email channel You can set up and send messages over different channels, but email is our most common—and one of the easiest to set up. Before you can send messages, you need to configure the types of messages you want to send: email, push, in-app, SMS, or Slack. You can start with just one channel and add new ones later by going to Settings > Workspace Settings and clicking Get Started on the channel(s) you want to add. Enter your domain—like example.com. Enter your From address(es). These are the addresses that your messages will come from. Verify and authenticate your domain(s). This involves adding some records to your domain host. (Recommended) Set up link tracking so you can record the links that people click in your messages. This involves adding a CNAME record to your domain host(s).  Start sending emails slowly! If you’re just getting started with Customer.io, you’ll need to start slowly. Sending a large number of emails right away can hurt your domain reputation and deliverability. See our guide to sending slowly for more information. Add a test person to your workspace To get started quickly, you can go to the People page in your workspace and add a person with a few attributes to test with. In this case, you might want to add yourself to your workspace, so you can test a campaign against yourself and get a feel for Customer.io without inadvertently messaging your customers or users. Later in this guide, we’ll use your test person in a live segment and campaign. Go to the People page, click Add People and select Add a Person. Set your ID and email address. To make it easy, you might assign your test user an ID of 1. Add first_name and last_name attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to your person. These are the values you’ll use to target your test person and personalize messages! You can add as many other attributes as you want, and we’ll show you how to take advantage of them later on this page. Click Save Changes when you’re done.  There are plenty of ways to add people This process shows the easiest way to add a single person, but it’s not efficient for adding your whole audience. When you’re ready, you can learn more about adding people programmatically or by uploading CSVs. What is a person in Customer.io? It might help to understand what we mean by a “person”. In Customer.io, you’ll represent a person with data, the most important three data points being identifiers, attributes, and events. IdentifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.: the values that make people unique in your system—like a database id or email address. AttributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.: the things you know about people, like their names, interests, etc. EventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.: the things that people do. You can use events to trigger campaigns in response to your audience’s activity in your system. You can associate other data with people, like their relationships to objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. (e.g. companies, accounts, etc) and their mobile devices, but the three things you’ll work with most often are identifiers, attributes, and events.  Before you add people, think about the attributes and events you want to track You’ll use attributes and events to group people into segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., determining who goes through your campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. and who you send messages to. Plan out your “groups” of people and the types of messages you want to send to make sure that you gather the right data in Customer.io! Create a campaign and send your person a message Now that you’ve added a person and set up a message channel, you can set up a campaign to send your person a message. Go to Campaigns in the left-hand menu, then click Create Campaign. Click “Untitled” in the top left to change the name of the campaign so your team members can easily find it on the campaigns page. Set your trigger Click the trigger block on the canvas, and choose Segment change as your trigger type. Triggers determine who enters your campaign and when. In this example, people will trigger your campaign when people join a segment. Click the segments dropdown then choose Create a new data-driven segment. Click Add condition or group. Select Attribute, choose first_name from the profile attribute dropdown, and set your test person’s first name. If you have multiple people with this first_name, choose another attribute so real members of your audience don’t enter this test campaign! Add a Name for your segment. You can filter by this on the Segments page. Click Create segment. Click Save and build workflow. Create your workflow Click Build to see a menu of actions. Drag an Email onto the canvas. Click the email to edit the name then click Add Content. Pick one of the editors. In most demonstrations, we use the drag-and-drop editor to send stylized messages that resonate with your audience. But, for this example, we’re using the rich text editor because it’s easier to show off the personalization features of Customer.io. In the From field, choose one of the Sender addresses you added when you set up email as a message channel. Add a subject line. Create your email. We added 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 personalize the message with your test person’s first name. You can use {{customer.<attribute_name>}} to reference any attribute value associated with your audience. You can also create conditions and set fallbacks when attributes don’t exist. Click Save at the bottom then Back to Workflow at the top. Click Settings. Under Sending Behavior, note that this message is set to “Queue Draft.” This means after the campaign is live, we would draft this email, but not send it to your audience. Update this to “Send automatically” then save your changes. Review your campaign Click Review 1 item next to Start Campaign. Notice you still have to set a goal. Click Set Goal. Since this is a test campaign, we’ll choose “No goal.” Then click Save changes. Notice you can also change message settings - like subscription preference and message limits - and exit conditions from this panel. By default with segment-triggered campaigns, people exit early when they don’t match your trigger or filter criteria - in this case, when their first name changes. Click Start Campaign to review your campaign. Under Trigger, choose who you want your campaign to include: existing matches (people already stored in your workspace) and/or future additions. We’re using Current people and future additions so our existing test person enters the campaign. Click Start Campaign. Within a minute, you should get your first email! This was quick. Is it really that easy? This guide walks you through the most common parts of Customer.io so you can start sending messages as quickly as possible. But it’s not comprehensive. You’ll get the most out of Customer.io when you carefully plan your implementation, capture the right data, and automate your messaging strategy. That all takes time. So, while we can get you started in just a few minutes, and demonstrate some common features of Customer.io, it’ll take more than this quick start to get you over the finish line! --- ## Academy: Structured Learning URL: https://docs.customer.io/get-started/get-started-academy/ Our docs site provides conceptual info, step-by-step guides, and detailed information about features and functionality across our platform. But we also offer an Academy where you can take courses and complete exercises to learn about Customer.io! **Use our docs to explore topics in depth and our Academy for a more structured learning experience.** The Academy is a free resource where anybody with a Customer.io account can learn about Customer.io. Access the academy To access the Academy: Log into your account. Click Need help? at the top. Click Learn with Academy. From there, you can browse individual courses or complete a longer learning path composed of multiple courses. Learning paths are great for getting started with Customer.io! You can also filter by topic or use the search bar to find relevant learning material. Learning paths A learning path is a series of courses designed to teach you about a broad concept or range of topics in Customer.io. For example, the Customer.io quick start path covers the core concepts of Customer.io, including how to use your data, target audiences, send messages, and track performance. Note that some paths are only available if you’re on our Premium or Enterprise plans. Learning Path Courses Description Customer.io quick start 🚀 5 Get started fast with core messaging skills—learn how to use your data, target audiences, send messages, and track performance in Customer.io. Email deliverability 3 Build and maintain a strong sender reputation that keeps your emails out of spam folders and in front of your audience, where they belong. Build customer journeys 4 Level up from campaign builder to automation strategist with advanced workflows and techniques. Data foundations PremiumThis learning path is only available to users on our Premium and Enterprise plans. 3 Your messaging is only as powerful as the data behind it. Learn how to model, integrate, and activate data in Customer.io to create highly personalized, data-driven campaigns that drive real engagement. Courses Beyond learning paths, you can browse and take any individual course in the Academy outside of learning paths. These courses are geared towards specific topics and skills. Check out the courses below or browse the course catalog to find a course that interests you. Note that some courses are only available if you’re on our Premium or Enterprise plans. Course Description Understand the data that powers your messages Discover the building blocks of Customer.io—people, attributes, and events—that make smart, personalized messaging possible. Send newsletters that drive engagement Create and send targeted one-time messages for product announcements, updates, and promotions using Customer.io's newsletters feature. Track and optimize performance Analyze campaign metrics, identify optimization opportunities, and make data-driven improvements to boost engagement and conversions. Convert with campaigns Design multi-step automated workflows that personalize based on customer behavior, delivering onboarding and lifecycle messaging at scale. Target your audience with segments Build dynamic segments that automatically group customers based on behavior and attributes, so you send the right message to the right people every time. How to stay in the inbox Learn about the email deliverability traps that can keep your customers from seeing your emails in their inboxes–no matter how good they are–and the best practices to sidestep the landmines. Repair your domain reputation Uh, oh. Have your open rates landed in the basement? Learn best practices and proven methods to repair your domain reputation and start landing more of your email in the inbox versus the spam folder. Establish your sending reputation Build trust with inbox providers by properly warming your sending domain—a critical step for email deliverability success. Build B2B and other non-people campaigns with Objects PremiumThis course is only available to users on our Premium and Enterprise plans. Accounts. Companies. Plans. Policies. Opportunities. These are all examples of entities that can be related to people that you might want to use to activate and/or personalize your messaging. Build workflows to automate customer engagement Combine messaging, branching, and timing actions in workflows to create truly responsive and engaging campaigns that nurture and convert your customers. Learn how you can leverage your data to reach out to your customers in this course! Set campaign goals, messaging options, and exit criteria Define the goal and exit criteria of your campaign to set the boundaries of your campaign and how you’ll measure success. Use these important campaign settings to provide high-level guidance for the operation of your campaign. Trigger campaigns with data The most effective messages are sent right when a customer needs and wants the information. Triggers inform campaigns when to kick the messaging off and who should receive the messages. Understand the different options buy completing this course. Get data out of Customer.io PremiumThis course is only available to users on our Premium and Enterprise plans. Send messaging metrics and/or customer data out of Customer.io to other systems like your CRM, data warehouse, or analytics platform. Use built-in integrations to simplify the sharing of data whether it be for data enrichment or deeper analysis. Get data into Customer.io PremiumThis course is only available to users on our Premium and Enterprise plans. Drive targeted campaigns and messaging by bringing your data into Customer.io. This course introduces the data concepts and structures you’ll be working with within Customer.io, as well as the menu of data integration options available to you. --- ## 1. Create your workspace URL: https://docs.customer.io/get-started/create-workspace/ After you create your account, you'll make a workspace. You can think of a workspace as a container for your people, data, messages, integrations, etc. You can have more than one workspace, but, in general, you won't share information across workspaces. Create your account and workspace After you create your account, you’ll follow the on-screen steps to make a workspace. If you’re new to Customer.io, we encourage you to use the default settings. They support most use cases and you can change them later! When you create your account, you’re considered an administrator. You can add other users to your account and determine their roles within your account and workspaces. Adding additional workspaces You can always set up extra workspaces when you go to Settings > Account-settings > Workspaces and click Add Workspace. When you create a workspace, you’ll get all the same options as when you created your first workspace, and you’ll see the setup guide again.  Don’t forget to color-code your workspaces If you have multiple workspaces, providing handy names can help you understand which workspace you’re working in. But you can also color code your workspaces, making it easy to tell your workspaces apart at a glance. The setup guide When you create a workspace, we provide a setup guide on the Dashboard page. The pages in this section of our documentation loosely follow that guide! You can follow the guide to set up your workspace, or you can skip it and set up your workspace later. But the setup guide contains the major steps involved in getting set up and using Customer.io. Invite your team to your workspace You’ll invite team members and grant them access to your workspace at the account level. If you create a new workspace after you add team members, you may need to grant them access to your new workspace. Add team members and grant workspace access You can invite team members to your account by going to Settings > Account settings > Team members and clicking Invite team member. When you add a team member, you assign an account-level role and, optionally, a role for each workspace they should have access to. See who has access to your workspace When you add a new workspace, you decide which existing team members should gain access to it. You can see who has access to your workspace when you look at your workspace settings. Go to Settings > Workspace settings > General workspace settings. --- ## 2. Set up message channels URL: https://docs.customer.io/get-started/set-up-messages/ If you're new here, you'll need to set up message channels before you can send messages. The setup process, and your involvement, varies depending on whether you want to set up email, in-app messages, push notifications, or SMS/MMS, and so on. Before you begin Before you can send messages, you need to configure the types of messages you want to send. If your workspace is new, you can follow the onscreen setup guide, but you can always add channels later under ** Settings > Workspace settings. Some message channels take more time and effort to set up than others. Most require access to resources outside of Customer.io. You might need to work with your development team or other teams in your organization to set up your message channels. For example, email setup often requires access to your domain provider and/or your email service provider. Push and in-app messages to mobile devices require some development effort. Set up email You can deliver email through us or use your own provider. If you’re new to email marketing, it’s easiest to send email through us and use our default settings! But if you already have an email provider, you can skip this section and use them instead. We ensure that all email sent from us is authenticated and meets industry technical standards. We have a no-tolerance policy towards spammers and diligently monitor our network for denylistings, problematic senders, and other factors that could negatively impact your messages’ deliverability. Enter your domain—like example.com. Enter your From address(es). These are the addresses that your messages will come from. Verify and authenticate your domain(s). This involves adding some records to your domain host. (Recommended) Set up link tracking so you can record the links that people click in your messages. This involves adding a CNAME record to your domain host(s).  Start sending emails slowly! If you’re just getting started with Customer.io, you’ll need to start slowly. Sending a large number of emails right away can hurt your domain reputation and deliverability. See our guide to sending slowly for more information. Use your own email provider You can also use your own account with any of the following fully-supported platforms: Mailgun Mailjet Mandrill Sendgrid Sparkpost Postmark  Start sending emails slowly! If you’re just getting started with Customer.io, you’ll need to start slowly. Sending a large number of emails right away can hurt your domain reputation and deliverability. See our guide to sending slowly for more information. Set up in-app messages You can enable in-app messaging and start setting up your messages right away, but you’ll need to integrate your website or mobile app with Customer.io before you can send messages. If you haven’t already, it’s easy to add our Journeys Web SDK to your site and start sending messages right away. To send messages to your website visitors, you need to add the JavaScript client to your site. To send messages to your mobile audience, you’ll need to integrate with our mobile SDKs. Set up push notifications We support push notifications for mobile applications; we don’t support web push. If you’re starting fresh, you should integrate with our mobile SDKs. But, if your app is already set up to receive push notifications and you don’t want to integrate our SDKs, you should check out our integration guide. You’ll also need to add your Apple Push Notification Service (APNS) and Firebase Cloud Messaging (FCM) credentials to your workspace. APNS FCM When you’re ready, you can send a simple push notification or use a custom push payload to send a push notification that includes a deep link or an image. Set up SMS/MMS We support SMS and MMS messages through Twilio. If you’re located in, and only send messages to users in, the US or Canada, you can manage SMS sending entirely through Customer.io! Sign up today to see if your eligible. Otherwise, you’ll need to set up a Twilio account and add your credentials to Customer.io before you can send SMS messages. To send SMS and MMS messages in Customer.io, you’ll need to have a Twilio account and the Sender phone number. This might be a regular phone number, short code, or an alphanumeric ID. Twilio can lease these numbers to you. Or, if you have a paid Twilio account with Alphanumeric Sending enabled, you can send messages from an Alphanumeric ID instead of a Twilio phone number. Set up a Twilio account if you don’t already have one. We recommend using a trial account to get started. Set up a Twilio-specific Sender if you don’t already have one. You can’t use your own phone number to send SMS; you need to purchase a number from Twilio. If you already set up your sender number, select it when you compose messages. In your Customer.io workspace, go to > Workspace Settings > SMS and add your Twilio Account SID and Auth Token. You’ll find these values in your Twilio dashboard. Other message channels Slack: Enable Slack and authorize Customer.io to send messages to your Slack workspace. Webhooks: You can use webhooks to create quick, if-this-then-that style integrations that send data to, or trigger actions in, systems outside of Customer.io. --- ## 3. Integrate with Customer.io URL: https://docs.customer.io/get-started/integrate/ While you can add people manually or upload CSVs, you'll get the most out of Customer.io when you integrate with us, adding people and tracking the events they perform programmatically. How it works While you can add people manually or upload CSVs, you’ll get the most out of Customer.io when you integrate with us, adding people and tracking the events they perform programmatically. You can integrate different data sources with Customer.io—your website, mobile apps, databases, and so on. flowchart LR d{Where does your data come from?} d-->|Website|a d--->|Mobile App|f(Integrate with a mobile SDK) d--->|Database or Data Warehouse|g(Reverse ETL Integration) a{Will you send in-app messages?} a-->|Yes|c(Integrate with the JavaScript Library) a-->|No|e(Integrate with a Server library) style c fill: #FFEDF0 style e fill: #FFFAE5 style f fill: #E6FAF3 style g fill: #E5FBFE Integrations (Data In) Description RecommendedJavaScript Helps you identify people, send events, etc from your website, and lets you send in-app messages to your website visitors. Server-side libraries Support Customer.io in your Node.JS, GoLang, or Python server. If you’re running a web server and want to send in-app messages, you should use our JavaScript library instead. Mobile SDKs Support push notifications and in-app messages in your mobile app. We support iOS, Android, React Native (including Expo), and Flutter. Reverse ETL Add and update people, events, objects, and relationships from your backend database. Incoming webhooks Trigger campaigns from any external service that can send data to Customer.io. Outgoing webhooks Send data to a URL as a part of campaigns. Provides a low/no-code way to integrate Customer.io with external services. Direct API Integration If you want to integrate with a database or language we don’t yet support, you can integrate directly with our APIs. Data in and out Customer.io integrations are separated into two categories: data inAn integration that feeds data into Customer.io. and data outAn integration that sends data out of Customer.io.. You send data into Customer.io, enrich it, and send it out to other services. You should start by setting up data-in integrations to capture data from your website, mobile apps, and other data sources. Whatever services you want to capture data from, we strongly recommend that you use our SDKs to get data into Customer.io. With our SDKs, you’ll use functions like identify to add people, track to capture the events people perform, and so on. If you want to send data to other services outside of Customer.io, you can set up data-out integrations. Within each data-out integration, you can set up actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. that determine what kinds of data you send to your outbound service. flowchart LR subgraph a[Data In] direction LR c(Your website) h(Your mobile app) i[(Your Database)] end subgraph b[Data Out] direction LR e((Customer.io Journeys)) f(Your analytics platform) j(Another Destination) end c-->g-->|Standard/required integration path|e g-.->f g-.->j h-->g i-->g g((Customer.io)) Your people are your data In Customer.io—and in most marketing stacks—your “data” typically refers to people who use your services, the things you know about them (their attributes), and the events they perform. You’ll use this data to trigger and personalize messages, so you send the right messages to the right people at the right times. When you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. a person, you’ll capture their attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.—things like their name, email address, and more. You’ll also track the events people perform. Events make it easy to respond to people’s behaviors in your app or on your website. For example, you can thank people for filling out a form, remind them to complete a purchase, or suggest additional content when they watch a video.  People aren’t all of your data We’ve simplified things by talking about people, but you can also capture custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. to represent groups that people are related to—like companies they work for, online classes they take, or recreational sports leagues they belong to. Plan your integration Different kinds of data use different functions across the system. Knowing what data you want to capture can help you understand what calls you’ll need to make and when. For example, when you want to record attributes, you’ll use an identify call. As you integrate with Customer.io, think about the data that you want to store: The identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. values you want to use for people and the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you want to capture about them. you’ll add/update people with identify calls. The events you want to record: you’ll record events with track calls. The groups and relationships you want to maintain: you’ll record objects and relationships with group calls. You can store all kinds of data in Customer.io. But be careful when handling certain kinds of data. In general, you should not send us sensitive personal information like bank account information, credit card numbers, personal health information and so on.  You don’t have to store everything in your workspace Some of our integrations let you forward data to other destinations without having to store it in your workspace. Integrate with Customer.io When you integrate with Customer.io, you’ll set up data in integrations, like your website, and then you can send data out to other services—like your analytics platform or your backend database. Most of the work you’ll do to get started is in setting up your data in integrations—your sources of data for Customer.io. Then you can set up and customize the data you forward to other services. Data in: Add a JavaScript integration In most cases, we suggest that you integrate your website with Customer.io using our client-side JavaScript library because it acts as both a data source and also lets you send in-app messages to your audience. We’ll reference this library in our examples. But we also offer server-side SDKs in other languages. Go to Integrations and click Add Integration. Find and click the JavaScript integration. Enter a Name for your integration, and click Submit. You should give your source a name that helps you identify not only the source medium (“JavaScript” in this case), but helps you identify the resource you’re capturing data from—like “company website” or “community forums.” Under Installation Instructions, you’ll find relevant code that you need to add to your website. Using our JavaScript client as an example, you’ll paste the code snippet in your website’s <head> tag. You’re now ready to start sending calls to Customer.io. If you enable in-app messaging, you’re also ready to send in-app messages to your website visitors. Data out: Set up third-party integrations Data out integrations are the places you want to send, and act on, data. Sending your Customer.io data to all the places where it’s useful to you eliminates “data gaps” in your stack. Check out our integrations catalog to see all the integrations we support natively. If you don’t find one you need, let us know! This video shows an overview of a third-party data out integration as well as reporting webhooks—how you can send metrics and message activity out of Customer.io. Integrate your mobile app(s) While you can write your own integration with our APIs, we strongly suggest that you integrate your mobile apps using our SDKs. At a basic level, you must use our SDKs if you intend to send in-app messages. Beyond that, they’re already set up to help you identify people, track events, and receive push notifications, making it easy to get set up with Customer.io without writing all your own code from scratch. When you integrate mobile apps, you’ll need to enable push and in-app messaging in your workspace. You’ll also need to set up push notifications with your push provider. See our Push Notifications documentation for more information. Database and Reverse ETL integrations We offer a number of reverse ETL integrations that help you send data from your database into Customer.io. If you already aggregate your data in a database or data warehouse, you can use a reverse ETL integration to keep your Customer.io workspace up to date with your latest data. --- ## 4. Add people URL: https://docs.customer.io/get-started/add-people/ Learn how to add people to Customer.io, and how to get the most out of your data. **In Customer.io, we refer to the process of adding a person as *identifying* them.** How it works Customer.io Journeys revolve around your audience of people. And the data you store in your workspace typically represents people and the things they do. To really take advantage of Journeys, you’ll add people and their events to Customer.io. Because we represent people with data, a “person” consists of a few things—all of which you can use to group people into segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., trigger campaigns, and personalize messages: Their identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.: the values that make people unique in your system—like a database id or email address. Their attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.: the things you know about a person, like their name, interests, birthdate, etc. The eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. they perform: the things that people do on your website or in your app, like the pages they view, the buttons they click, and so on. You can use events to trigger campaigns in response to your audience’s activity in your system. Their relationships: the things they’re related to (commonly known in Customer.io as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.)—like a company they work for or the online classes they take. (For push notifications): their mobile devices. flowchart BT a(Person) b-->a e-->a j-->a n-->a subgraph b[Identifiers] direction LR c(id) d(email) r(cio_id) end subgraph e[Attributes] direction LR f(first_name) h(phone) i(fav_food) end subgraph j[Events] direction LR k(added_to_cart) l(started_class) m(enabled_feature) end subgraph n[Relationships] direction LR o(Employee of Company X) p(Taking online class Y) q(Test group for feature Z) end style b fill: #FFEDF0 style j fill: #E6FAF3 style n fill: #E5FBFE  Before you add people, think about how you want to group them You’ll use attributes and event data to group people into segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., determining who goes through your campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. and who you send messages to. Plan out your “groups” of people and the types of messages you want to send to make sure that you gather the right data in Customer.io! Ways to add people In general, you’ll get the most out of Customer.io when you integrate with us and send real-time data about your audience and their activities. This helps keep your audience up to date and automate your messaging—the closest you can get to “set it and forget it” in the Marketing Automation world. You can also add people manually or upload a list. These kinds of operations represent people at a “point in time” rather than keeping information up to date, but can still be useful when you gather information outside your integration path—like when you gather sign-ups or business cards at a conference. There are a few different ways to add people to Customer.io: Add a test person manually. Upload a CSV of people. Integrate with Customer.io. This lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. people in real time and keep up to date with their activities in your systems. But however you add people, we recommend that you add a test user to your workspace. This lets you test your messages and campaigns before you send them to your audience. You can manipulate your test user in a number of ways to prove your integrations, campaigns, and so on. Add a test user If you want to demonstrate the capabilities of Customer.io—or you want a handy way to test your messages—you might want to add a test user. You can add a person manually with the qualities you want to test, making it easy to see how your campaigns, messages, and segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. perform. Go to the People page, click Add People and select Add a Person. Set your ID and email address. To make it easy, you might assign your test user an ID of 1. Add attributes to your person, like first_name and last_name. These are the values you’ll use to target yourself and personalize messages! Click Save Changes when you’re done. Upload a CSV of People If you want to add people to Customer.io but you’re not creating an integration—or you just want to get started before you set up an integration—you can upload a CSV of people to Customer.io. To get started quickly, you can go to the People page in your workspace and add a person with a few attributes to test with, or upload a CSV of people. Identify people via integrations The way that you add people to Customer.io depends on how you integrate with us: Through client-side JavaScript Through a server-side library Through your mobile app Using a database. But, in almost every case, the method to add people is called identify. For example, if you’re using our JavaScript client library, you’ll invoke the identify method, passing an ID and an object containing a person’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. (data associated with the person). cioanalytics.identify('example-person-id', { first_name: 'cool', last_name: 'person', favorite_foods: ['pizza', 'ice cream'], created_at: 1339438758, // strongly recommended when you first identify someone }); --- ## 5. Send events and make segments URL: https://docs.customer.io/get-started/segments-and-people-data/ The real power of Customer.io comes from using the things you know about people, and the things they do, to organize them into groups, automatically trigger campaigns, and so on. How it works People have data associated with them—the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you know about them and the eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. they perform. You can use this data to automatically group people into segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and trigger campaigns. This helps you automate your messaging strategy, so you send messages to your audience at the right times—without having to manually update lists or trigger messages. flowchart LR a(person assigned attributes) b(person performs event) a-->c{Does person match segment criteria?} c-->|yes|d(person joins segment) d-->e{Does this trigger a campaign?} e-->|yes|f(person joins campaign) b-.->|events can be segment criteria|c b-->e Data Index: Know your data If you’re going to use attributes and events to group people, trigger campaigns, and so on, you should know what attributes and events are available to you. If you’re not already aware of the data coming into your system, you can see the attributes and events in your system under Integrations > Data Index. The Data Index shows all of the attributes in your system and a list of events performed within the last 30 days. If you don’t see an attribute or event that you think you might need, you may need to update your integration to send the right attributes and events to Customer.io!  Find an individual’s data on the People page If you go to People, you can click an individual person to see their specific attributes (in the Attributes tab) and events (in the Activity tab). Segments A Segment is a group of people in your workspace—people who share the same attributes, performed the same events, and so on. You can use segments to trigger campaigns, as the audience for a broadcast, as a goal for a campaign (e.g. you want people to join a particular segment), and so on. You can add people to segments manually or automatically—which we call data-driven segments. In general, you should use data-driven segments whenever possible, because they update as your audience’s data changes—they’re easier to set up and update automatically. People join and leave data-driven segments automatically based on whether they match the conditions you set. But manual segments can be helpful when you don’t want a segment to update in real time—like if you want to maintain a static list of people you met at a conference. Unlike data-driven segments, people don’t join or leave manual segments automatically. You can add people to manual segments by uploading a CSV file, or by adding them to a segment as part of a campaign or broadcast workflow. Data-driven segments Data-driven segments group people based on the data you send to Customer.io, so people join the segment when they match certain criteria, and leave it when they stop matching the criteria. These kinds of segments are an essential part of your marketing automation plans because you can use them as campaign triggers. In the same way that people join segments when they match your criteria, they can also start campaign journeys when they join your segments. With well-defined segments, you can automate your messaging strategy and send messages to your audience at the right times—without having to manually update lists or trigger messages. You can define data-driven segments on the Segments page. You can use most of your customer’s data as segment criteria, including attributes, events, page views, and devices. You can also use metadata from inside Customer.io like when people click tracked links in emails, open messages, etc. Example segments Lifetime value: Imagine People have a lifetime_value attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you increase when they pay you. You could set up a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. that people join when their lifetime_value value is greater than a certain amount; for our example below, we’re using 25000. You might trigger a campaign based on this segment to send special messages to your high-value customers! High value offers: Let’s say you send events to Customer.io when your people visit high value offers on your site. You might create a segment that matches people who see the same high value offer five times in the past week but haven’t used it or made a purchase yet. The segment could trigger a campaign that sends those users a special offer or nudge that encourages them to finish their purchase. Manual Segments Manual segments let you define exactly who belongs in the segment, and they’ll remain there until you remove them. They’re good for keeping static lists of people—like people you met at a conference, or people who signed up for a beta program. You can add people to (and remove people from) manual segments by uploading a CSV file or invoking our API. You can remove people from manual segments as a part of a campaign or broadcast workflow. Events Events are the things that people do in your system—like signing up, logging in, or making a purchase. You can use events to trigger campaigns, as segment criteria, and so on. flowchart LR a(person performs event)-->b{Is the event a campaign trigger?} a-->c{Is the event segment criteria?} b-->|yes|d(person starts campaign journey) c-->|yes|e(person joins segment) e-->f{Is the segment a campaign trigger?} f-->|yes|d Unlike attributes, which you can set manually, events are a standard part of your integration(s) with Customer.io. You’ll need to send events into Customer.io programmatically to take advantage of your audience’s real-time activities. Beyond their use in Customer.io, events are a typical way to pass data outAn integration that sends data out of Customer.io. to other services. For example, you might send events to Customer.io to trigger campaigns and send those same events to an analytics platform like Mixpanel. Event data Each event carries data with it. Data properties can provide important information about the event a person performed. For example, if you send a purchase event, you might include product_name and price properties so you know what a person bought and how much it cost. You can use these properties in a few different ways: Fine-tuning campaign triggers: for example, you might want to trigger a campaign that sends a coupon to people when they spend over a certain amount in a purchase. Personalizing messages with 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}}.: for example, if someone signs up for an online class, you might send them a message that reminds them of the class they signed up for, and includes a link to the class materials. Here’s an example of a signup event that includes some relevant properties. { "type": "track", "event": "Course Started", "properties": { "course_in_series": 1, "course_format": "pass/fail", "title": "Intro to Customer.io" } } Page and Screen View Events There are two very specific kinds of events: page and screen views. These events represent the pages (web) and screens (mobile) that your audience visits. These events work much like other events. You can do everything you do with standard events, plus two special things: Create special segments based on the pages and screens your audience visits. Determine the pages/screens on which people receive in-app messages. We call these Page Rules.  Capture page views automatically If you use our JavaScript snippet, we’ll automatically send page view events. Semantic events Some events have special meaning in Customer.io or your downstream destinationsAn integration that sends data out of Customer.io—your data’s ultimate destination.. We call these semantic events. You can use semantic events to perform actions in Customer.io Journeys (and other destinations). For example, we don’t have a call from our source libraries to remove a person from your Customer.io workspace using our JavaScript source snippet. So, if you need to delete a person from your workspace, you can send an event called Delete Person. // Remove a person from your Customer.io workspace cioanalytics.track("User Deleted"); Semantic events are based on the event name. You can set up your own events for any destination, but we’ve pre-defined a significant number of these events to support various destinations—so you don’t have to define them yourself if you’re just getting started. See Semantic Events for more information. --- ## 6. Start sending campaigns and workflows URL: https://docs.customer.io/get-started/send-messages/ Set up your first campaign or broadcast to send messages to your audience.  Want to send your first message right away? Check out our quick start guide to send your first message and jump right in. How it works A message is what you want to send. But before you send a message, you have to determine who to send your message to and when to send it. You answer these questions when you set up a campaign, broadcast, newsletter, or transactional message. Each of these mechanisms has a different use case, but they all contain the same basic concepts/questions: How do I trigger messages? Who should receive messages? When should people get messages? What messages should people get? Trigger Audience Timing Messages Campaign When people meet your criteria People who meet your criteria Automated by workflow Multiple Broadcast When you trigger the broadcast (API or UI) People who meet your criteria When you send the API call Multiple, all at once Newsletter When you click send in the UI People who meet your criteria When you trigger the newsletter (manually) Single Transactional Message When a customer requests a message (API) A specific person When you send the API call Single When should I use campaigns, broadcasts, or newsletters? We’re glad you asked! There are a multitude of factors that can help you pick the right message mechanism for your needs. But, in general, you can use the following guidelines to help you pick the right one. I want to control when people get messages: You should send a newsletter or a broadcast. You’ll send newsletters manually (or on a schedule), which makes it easy to determine who gets messages and when. Broadcasts are a bit trickier, but you base the send on an API call that you control—so you can send messages when something relevant to your business happens. I want to send messages in response to my audience’s behaviors: You should send a campaign or a transactional message. These things are very different, but they’re both predicated on your audience’s behaviors. You’ll send transactional messages when a person performs an action that expects a message—like a receipt or a password reset. You’ll send campaigns when people meet certain criteria, like when they perform an event, gain certain attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., and so on. If you want to send multiple messages or perform multiple actions: you should send a campaign or a broadcast. Campaigns contain a workflow that you can send people through to get multiple messages, or perform multiple actionsA block in a campaign workflow—like a message, delay, or attribute change. over time. Broadcasts also have a workflow, but the whole workflow happens at once; this means broadcasts aren’t really ideal for sending multiple messages, but you can perform multiple actions—like changing your audience’s attributes or sending webhooks.  Transactional messages follow regional regulations Transactional messages are defined by regional regulations, like the CAN-SPAM Act in the US. Make sure that you use transactional messages for their intended purposes: as direct responses to your audience’s implicit requests for messages (e.g. purchase receipts or password reset requests). Try a newsletter if you’re just getting started Many of the sending mechanisms in Customer.io rely on your data. If you’re just getting started, a Newsletter, like the one in our quick start guide, is a great way to get started with Customer.io. Where other kinds of messages rely on your data, API calls, etc, you can control all aspects of a Newsletter from our UI. That can make it easy to test your implementation and get a feel for how our platform works. Campaigns Campaigns are the most powerful and common way to automate your messaging strategy with Customer.io. When people enter a campaign, they start a journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. through your campaign workflow. Your workflow can define different paths people can go down and different messages they can receive based on the actions they take and the attributes they have. A campaign consists of a trigger, workflow, and goal. When people meet your trigger criteria, they start a journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. through your workflow. They travel through the workflow until they: Finish the workflow Match your goal criteria (depending on your settings) Stop matching the trigger criteria (depending on your settings) Because people can match your trigger at different times and exit campaigns for different reasons, each person’s journey is unique and personalized for them. This makes it easy for you to maintain an automated messaging strategy that resonates with your individual users. Campaign triggers The trigger determines when people start a campaign. You can set up campaigns to trigger when people perform an event, move into (or out of) a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., they reach a certain date, and so on. In many cases, understanding the purpose of your campaign and the kinds of messages you want to send can help you determine the trigger you want to use. For example, you might use date-triggered campaigns for things like birthday or anniversary messages. You might use event-triggered campaigns to send coupons encouraging your audience to complete their purchases when they abandon their shopping carts. Learn more about campaign triggers. But to set up campaign triggers correctly, you’ll also need to know a bit about your data—what attributes and events are available to you. You can see the attributes and events in your system under Integrations > Data Index. Workflows The workflow is where you determine what messages people get and when. In a workflow, you can send people down different branches based on their attributes or behaviors. With a complex workflow, you can automatically send messages and respond to your audience in ways that feel personal and relevant to them. For example, we have a workflow below based on a cart abandonment scenario with three paths. If a person abandoned their cart for a certain amount of time, we send them a reminder message encouraging them to finish their purchase. But we don’t want to send them this email if they finish their purchase or they’re still shopping, so we bypass this message if the person completes their transaction or they add new items to their cart! Campaign workflows also help you send messages at the right times with blocks like Wait until. These blocks let you delay a message for a set period of time, or until a customer does something relevant. For example, you might send a series of emails explaining how to use your platform when someone joins your service. You can set up Wait until blocks to send follow-up emails when a person completes each step in the setup process. Goals Goals help you measure the success of your campaigns. Typically, we represent success when a person performs an event or gains an attribute—like when someone completes a purchase or renews their subscription. If someone reaches your goal criteria in response to a part of your campaign, we record a conversion, so you can see how effective your campaign is. Exit criteria Exit criteria determine when someone ends their journey through the campaign. For most campaigns, people will by default exit if they no longer match the trigger’s filter criteria; for segment-triggered campaigns, the default is when people no longer match the filter criteria OR the trigger conditions. Filters are additional criteria that people must meet to join your campaign, giving you more granular control over who travels through your campaign. We evaluate filters when someone enters a campaign and before actions that affect them (like messages). For example, if the purpose of your campaign is for someone to buy something, you probably want to stop sending messages after someone actually completes the purchase. You could make the goal of the campaign: “performs event purchase,” and your exit criteria They match the conversion criteria. The options for exit criteria include: They match the conversion criteria They stop matching the filters (or trigger if it’s a segment-triggered campaign) They match the conversion criteria or they stop matching the filters (or trigger if it’s a segment-triggered campaign) Broadcasts and Newsletters Broadcasts and newsletters are grouped together in Customer.io under Broadcasts. You determine whether you’re going to send an API-triggered broadcast or a Newsletter when you set up your message. Unlike campaigns, you trigger broadcasts and newsletters—they aren’t triggered by your audience. And, where campaigns can run for a long period of time and people can start or end campaign journeys independently of each other, everybody in your broadcast or newsletter audience gets your messages at roughly the same time. API-triggered broadcasts API-triggered broadcasts let you trigger the broadcast with an API call—which helps you send messages to your audience when something relevant happens. For example, you might trigger a broadcast to your audience when a new product launches, a new coupon becomes available, or when tickets become available for a concert. Unlike newsletters, you can set up a workflow for your broadcast and perform multiple actions. The workflow in a broadcast is designed to help you send a message and perform other non-message actions, like updating a person’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., sending webhooks, or adding people to segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. In general, you shouldn’t send multiple messages in a broadcast because when you trigger a broadcast, the whole workflow happens at once. If you send multiple messages as a part of your broadcast workflow, a person will get them at roughly the same time. Like other messages, you can personalize broadcasts using your audience’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. But, you can also pass data in your API call that you can use to personalize your messages. For example, you might send a broadcast to let people know that a new product is available, and you can use the product’s name in your message. Below is an example API trigger. curl --request POST \ --url https://api.customer.io/v1/campaigns/{broadcast_id}/triggers \ --header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \ --header 'content-type: application/json' \ --data-raw ' { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "email_add_duplicates": false, "email_ignore_missing": false, "id_ignore_missing": false, }' Newsletters Newsletters are the easiest way to send a single message to a large group of people. You can send a newsletter to segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. or a list of people. Unlike campaigns, which people enter when they meet certain criteria, or broadcasts, which you trigger with an API call, you send newsletters manually—which makes them predictable and ideal for one-off communications to large groups. You can also A/B test newsletters to improve your communications over time. While you can also A/B test messages in campaigns, newsletters have a much easier time reaching statistical significance: it’s much easier to tell which of your A/B test messages worked better when you send them all at once.  Control your send rate if you’re new here When you’re new here and you’ve just authorized/verified your domain, you’ll need to ramp-up your domain reputation. This means that you’ll need to send messages slowly at first, and then increase your send rate over time. You can control your send rate when you send newsletters! Transactional messages Transactional messages are messages that you send to a specific person when they perform an action that expects (or demands) a message—like a receipt when a person makes a purchase or a link to reset a password when someone has trouble logging into your service. You’ll send a transactional message via API call, similar to the way you trigger a broadcast, but each trigger is unique to a specific person. It’s a one-to-one message. Like other messages, you can create your transactional message in our UI and personalize it with your audience’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and other data from the API call that triggers the message. But, if you want to maintain complete control of your message, you can also send the entire raw message in your API call if you want. This gives you complete, programmatic control over the message you send—the content, the audience, and the personalize data you send. Here’s an example of a transactional message payload: { "transactional_message_id": 44, "to": "cool.person@example.com", "subject": "Reset your password", "identifiers": { "email": "cool.person@example.com" }, "message_data": { "password_reset_token": "abcde-12345-fghij-d888", "account_id": "123dj" }, "bcc": "bcc@example.com", "disable_message_retention": true, "send_to_unsubscribed": true, "queue_draft": false } Personalizing messages with liquid As you write your messages, you can use your audience’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., event properties, and other data to personalize your messages. This helps you generate highly relevant, personalized communications with your audience at scale. The message personalization syntax we use is called 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}}.. For example, if you wanted to reference a person’s first name in a message, you might use something like this: {{customer.first_name | capitalize }} You can use liquid almost anywhere in your messages. For example, when you write an email, you can use liquid to personalize your subject line, the recipient’s email address, and the content of your email. --- ## Introduction to Journeys URL: https://docs.customer.io/journeys/journeys-overview/ Welcome to Journeys—Customer.io's messaging interface! The pages in this Getting Started section aim to introduce you to our platform. This page introduces the core concepts involved in working with Customer.io. The following pages dive deeper into each concept. Journeys incorporates data from your customer touchpoints, helping you customize campaigns and send sophisticated, personalized messages that support and engage your audience.  New here? Check out our Quick Start Guide This section provides an overview of Journeys. But we recommend using our Quick Start Guide if you’re ready to jump in and start using Customer.io. How it all works The image below illustrates what a well-rounded integration with Customer.io looks like. Your Product sends Real-time Data to Customer.io. We’ll associate your real-time data with People and Objects. People are your leads, users, customers, etc that you want to send messages to. Objects are things that are important to your users, like an account they belong to. Next, you Activate your data: populate Segments and build powerful campaigns that send personalized Messages to your audience. When you’re fully set up, data will flow into Customer.io, triggering campaigns and messages, helping you automate customer communications!  Want to send customer activity to your workspace? Check out our Get Started guide to learn more about integration methods. People In Journeys, everything revolves around people. In Customer.io, you represent a person with data: the things you know about them (attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.) the things they do (eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.) the things they’re related to (objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. relationships) the campaigns they’re in and the messages they’ve received You’ll find people and their associated data on the People page. Objects Objects are the things that are important to your people, like an account they belong to, online courses they enroll in, or homes they’re interested in purchasing. A change to an object and people’s relationships to objects can trigger campaigns. You’ll find objects under People on the left-hand menu. Learn more about setting up objects here: Objects: how they work. Segments Segments are groups of people who match the same criteria. A segment can trigger campaigns, define recipient lists, represent conversion criteria, and more. To help you get started, we’ve pre-configured a few segments for you. If you go to the Segments page, you’ll see segments like “Paying Customers”, “Have not logged in recently” and “Unsubscribed”. Be sure to check those out! Learn more about segmenting your users here: Getting Started: Segments Campaigns, Broadcasts, and Transactional Messages Campaigns and Broadcasts send email, SMS, push, and in-app messages to your people. But, beyond that, your campaign and broadcast workflows can send Slack messages, webhooks, update attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., and more. Transactional messages are emails that you send in response to your audience’s actions in your app. Campaigns are the quintessential messaging automation workflow. They help you send one or more messages in a workflow sequence to people when they meet your trigger condition(s). You can trigger campaigns when people perform events, when they join Segments (e.g. they meet attribute conditions), and so on. Campaigns are ideal for dripping content to people as they become eligible to receive it. Welcome and onboarding series Re-engagement series Behavioral messages (respond to actions people take in your app or on your website) Broadcasts send a message to a list of recipients. Broadcast messages are queued to send to all the defined recipients at a time you specify. Broadcasts are ideal for manually sending newsletters or programmatically triggering announcements in bulk, like: Newsletter messages Pre-scheduled announcements Promotions Periodic release notes Community alerts Event change notifications Product launches Transactional messages help you respond directly to your audience’s actions in your app. Unlike broadcasts and campaigns, transactional messages are one-to-one interactions. Transactional messages are ideal for: Purchase receipts Registration confirmations Password resets Event reminders Shipping updates Learn more about Campaigns and Broadcasts or Transactional Messages. Integrations Once you grasp the basics of Customer.io, you’ll want to plan your integration. And don’t worry - you can also use your preferred tools and sources to transit data through Customer.io. It’s up to you! --- ## People and their profiles URL: https://docs.customer.io/journeys/people-overview/ In Customer.io, you'll store information about your audience. We show your contacts on the *People* page, including the data that you have about people, the events they've performed, their message history, and so on. When you go to the People page, you’ll see a list of people you’ve identifiedThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously.. From here, you can click on any individual person to see their profile—all the data that you have about a person. The Overview page shows you a summary of a person’s data. Subscription Preferences: if you use our Subscription CenterCustomer.io’s subscription center feature provides a way for customers to subscribe to, or unsubscribe from, specific topics. This helps you manage your audience’s preferences and make sure that they only get the messages they’re interested in., you’ll see what topics a person is subscribed to or unsubscribed from. identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. and attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.: the things you know about them, like their email address, name, interests, birthday, etc. Recent Activity: including changes to their attributes, the eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. they’ve performed, and the messages they’ve received. Deliveries: the messages they’ve received. segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.: the segments the person belongs to. Devices: A person’s phones, tablets, and so on. Relationships: the things they’re related to (commonly known in Customer.io as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.)—like the companies they work for or the accounts they belong to. In the example above, this person is related to a company called Acme, Inc. Subscription Preferences If you’ve enabled our Subscription CenterCustomer.io’s subscription center feature provides a way for customers to subscribe to, or unsubscribe from, specific topics. This helps you manage your audience’s preferences and make sure that they only get the messages they’re interested in., you’ll see each person’s subscription preferences on their profile. This helps you understand what topics your audience is interested in, and what they’ve opted out of. At the top of the profile, you’ll also see Status—“subscribed” or “unsubscribed.” This tells you if a person unsubscribed from emails, push notifications, or SMS messages all together. Attributes Attributes are pieces of data you know about a person—typically information they tell you, like their name, email address, or birthday. You can use attributes to personalize messages, segment your audience, and trigger campaigns. While we show a number of attributes on a person’s profile, the Attributes tab shows a complete list of a person’s attributes and other attributes in the system that you might want to apply to (or request from) a person. Learn more about the different types of customer attributes we support. Recent Activity On a person’s profile Overview, we show what a person has done recently—like the events they’ve performed, messages they’ve received, attributes that changed, and so on. Go to the Activity tab to see, and search through, a person’s historical activity. People can perform the same event multiple times. When people perform message-related events multiple times, we’ll show both the first time and actual time the event was performed. For instance, we track each time a person opens an email. The timestamp of the opened attribute always reflects the first time the event happened. The timestamp inline with the event name shows the timestamp of the event you’re viewing. If this is the only time the event has been performed for this specific message, both timestamps would match. Most Recent Deliveries Recent Deliveries shows the most recent messages for a person and their statuses—whether they’ve been sent, delivered, etc.  Why is it deliveries instead of messages? In Customer.io, a message is what you set up in a campaign, but a delivery is the instance of a message that we send to a person—complete with personalized information, links, and everything. Segments Segments are the groups that your audience belongs to. You’ll use segments to target members of your audience for campaigns, broadcasts, and so on. The Segments section includes two tabs that show the segments the person does vs does not belong to. You can track when someone enters or leaves a segment through their Activity log. Devices If you use our mobile SDKs, the devices area shows a person’s devices—their phones, tablets, and so on. It can help you understand your audience’s mobile footprint. You can see more information about each device, like the device’s app version, on the Devices tab. Relationships Relationships shows the objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. that a person is related to—like companies they work for, accounts they belong to, or classes they’re enrolled in. Objects can be any kind of non-person thing, and this section shows you the things a person is affiliated with. --- ## Add or update people URL: https://docs.customer.io/journeys/manually-adding-or-updating-people/ Generally, you'll [integrate your platforms](/get-started/integrate/#integrate-with-customerio) with your Customer.io account to create and update people (your customers). However, sometimes you may need to add an individual person or update an existing user manually for testing or other ad-hoc purposes. Add a person via the UI Go to People. Click Add People. Scroll to the bottom of the modal and click Add a single person. Under Identifiers, you’ll see id and/or email as options. The identifiers you see depend on the identifiers you’ve allowed in workspace settings. You only need to set one identifier to create a person. Learn more in How to identify people. Add other data you want to store on this person’s profile. Keep in mind, changing data can make people join or leave segments as well as trigger workflows. Click Save Changes. You’ll see this person on your People page and can now add them to segments, send them messages, and more. Update a person via the UI  Updates that you make on the People page don’t take precedence over other sources If you update a person’s attribute through the People page, and then send an update that changes that attribute through our API, integration, or any other source, your update will be overwritten. We update people using the latest data or request. You can either search for the person you want to update, or go directly to their profile by adding their id in the URL (eg. https://fly.customer.io/env/xxxxxx/people/5). If you search, you can click the user’s email to go to their profile: To delete or unsubscribe this person, you can do so on the right-hand side of the screen. When you confirm the delete or unsubscribe, you’re finished editing. If you want to update their attributes or add new ones, go to Attributes and click Edit Attributes. Don’t forget to save when you’re done! Adding or updating multiple people You can add or update multiple people at once by importing a CSV file. For more information, check out: Uploading People via CSV Email address validation When you add people by email addresses or update email addresses for people, we validate the new address against the RFC 5322 standard. In general that means that your addresses are formatted first last <first.last@domain.com> or simply name@domain.com. The validation standard for email addresses is dense, but if an address is invalid, you might check for the following things: The address contains an @ character. The address “name”, the portion of the address before the @, contains letters, numbers, and does not have a leading or trailing periods. The domain, the portion of the address after @ contains only letters, numbers, and does not have leading or trailing - characters. --- ## How to identify people URL: https://docs.customer.io/journeys/identifying-people/ In Customer.io, people are your customers—the recipients of your messages. To add or update people in your workspace, you must identify them. Depending on your workspace settings, you can identify unique people by ID or email address. How it works You can identify people using values that are unique for each person, like an email address or a user id. You add and update people based on their identifiers. To identify people, you need to first understand your workspace settings. Then you can add or update people based on the allowed identifiers: email: A person’s email address. You may want to use this identifier to track leads before they become customers. id: A case-sensitive, unique identifier that you assign a person. Generally, this value represents a person in your backend systems, like a user ID of someone who has an account with you. cio_id: A unique, immutable identifier that Customer.io generates when you identify a person. When you identify a person by cio_id, you can update that person’s other identifiers. Typically, you’ll programmatically add or update people through one or more integrations; this way your systems stay in sync and you know your automated messaging is sending to the right people. Sometimes it’s useful to make one-off updates or use campaign actions to update people though. Here’s how you can identify and modify people in our UI: Method Use Case People page Manually add or update customers. CSV import Import a file of people. Create or update person action Create or update a person from the data available in a campaign. Case sensitivity of identifiers id values are case sensitive; email addresses are not. Learn more about how this influences the way we identify duplicate people below. You should also review our info on case sensitivity across the platform. This will help you when searching your workspace, adding conditions, and adding liquid logic. How to handle duplicate identifiers When your workspace identifies people by email or id, we automatically merge duplicate people. Learn more in Resolve duplicate people. id values are case sensitive, while email addresses are not. This means that id values of ab100 and AB100 represent two distinct people in your workspace. Set allowed identifiers in Workspace settings When you create your workspace, you set which attributes are identifiers. You can always identify people by cio_id—a unique identifier assigned by Customer.io. By default, you can also identify people by email and id. To change whether email or id are identifiers, go to your General Workspace settings. If you’re not sure which setting is best, we recommend you leave the default setting. It doesn’t require you to add an id or email to every profile, just gives you the option to add or update your people when you identify them by email or id. It means you can store leads and existing customers so you can identify a person first by their email address and then by an id you add after they become a paying customer. If the customer ever needs to change their email address, you can do so by using their id to identify them. Learn more about setting identifiers in Workspace settings. Set people’s identifiers You set identifiers when you create or update people. You must include at least one identifier, based on what your workspace settings define as identifiers. Learn more in Add or update people. Update people’s identifiers If you want to update a person’s identifiers, you must first understand what your workspace settings are—if email and/or id are allowed identifiers. This chart shows what it means to update email or id based on these settings. Note, cio_id is a permanent id our system generates for each person. It can be useful when trying to update other identifiers for a person and is typically needed when you’re programmatically updating people: flowchart TD e["Update email #40;where id and email are identifiers#41;"] --> f{Is it set?} f -->|yes|h{"Is #quot;cio_id or id#quot; enabled?"} h -->|yes|j[You can set email with id] h -->|no|i[You must update email with cio_id] f -->|no|g[You can update email with id] a["Update id #40;where email and id are identifiers#41;"] --> b{Is it set?} b ---> |yes|k[You can update id with cio_id] b ---> |no|c[You can set id with email] n["Update id #40;where id is the only identifier#41;"] --> o{Is it set?} o --> |yes|p{"Is #quot;cio_id or id#quot; enabled?"} o --> |no|q[Person does not exist] p --> |yes|r[You can update id with cio_id] p --> |no|s[You cannot update id] If the workspace allows identification by email and/or id (The first two charts above): You can change a person’s id when you identify them by cio_id through the People page, but not CSV imports. You can change a person’s email when you identify them by id or cio_id. If email is enabled but you can only reference by cio_id, then you can only use cio_id to change email. If the workspace is id-only (The third chart above): You can’t change id unless you change your workspace settings. You can update email like any custom attribute. Delete people’s identifiers If your workspace lets you identify people by email or id, you can remove one or the other but not both. You can always identify people by cio_id; you can’t remove that identifier from a person. If your workspace lets you identify people by id only, you cannot delete id. Whether you can change it depends on your workspace settings. You cannot delete cio_id, no matter your workspace settings. This id is always needed for CSV imports and ensures each of your people have a unique identifier, which can help you and our support team troubleshoot issues. --- ## Manage customer attributes URL: https://docs.customer.io/journeys/attributes/ Customer attributes represent the things you know about your audience—their name, preferences, email address, etc. You can use them to personalize messages, track people with similar characteristics, trigger campaigns, and more! What are customer attributes? Customer attributes are data points about people in your audience: things like first_name, email or product_interests—any information that is important for your marketing workflows. Every attribute has a name and a value. The name is how you reference the attribute whenever you want to get or set the value. When you want to personalize messages with customer data or group people by shared characteristics, you’ll use the attribute name. Customer attribute is the umbrella term for a few types of data stored on people: Identifiers are how your workspace does just that—identifies people. At least one identifier is required. Learn more in How to identify people. Reserved attributes are attributes that have specific meanings in your workspace, where you can’t change the attribute name, but can often change the value. For instance, created_at is a reserved attribute whose value you can modify but not the name. There’s also an immutable reserved attribute called _created_in_customerio_at that our system generates when you add a person to your workspace. Check out the list of reserved attributes below. Subscription preferences: With our out-of-the-box subscription page, people can update their global unsubscribe attribute (whether they are subscribed or not to messages). You can also create a subscription center so people can manage subscription preferences for topics too. Global unsubscribes and subscription preferences are managed by different attributes. To learn more about setting subscription statuses, check out our overview. If you send push notifications, people also have device data stored on their profile. Naming best practices In general, these are best practices for developing naming conventions of attributes: Avoid using spaces, periods or special characters in attribute names because they can complicate what you need to do to personalize messages with 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}}.. Don’t start an attribute name with an underscore. This indicates that an attribute is internal and won’t be discoverable or useable throughout your workspace. _first_name should be first_name to use it in segments, trigger conditions, search results, etc. Use one casing convention for attribute names, like camelCase or snake_case. Attribute names are case sensitive, so we recommend auditing your data to ensure you’re only adding/updating information to one attribute. If you send an attribute for some people as first_name and an attribute labelled First_name for others, you’ll have two different attributes for names in your workspace. This can make it hard to personalize messages with 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}}. or add the right people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. because not everybody will have the same attribute.  Audit your attribute names in the Data Index You can use your workspace’s Data Index to identify inconsistencies in your attribute names and identify the sources of your data. Storing data as JSON You store attributes using JavaScript Object Notation (JSON). JSON data is a set of keys and values. A key stores a value. For example in the attribute "first_name": "Jack", first_name is the key and Jack is the value. In JSON, the syntax you use for a value determines what 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}}. filters and operators are available to you. In general, you should understand the difference between string, number, boolean, object, and array types. Learn more in Storing and using JSON. Don’t store sensitive data in your workspace  Avoid storing sensitive information in Customer.io You should limit the personal data you store to what you’ll use in Customer.io. You should not store sensitive data passwords and other tokens in Customer.io. While we take every effort to protect you and your customers’ data, limiting the data you share outside your backend systems limits potential security concerns and helps you respect your customers’ privacy. You can grant access to our Support and Customer Success teams when you need to troubleshoot issues for a limited time. When you grant access, Customer.io personnel can see your audience’s attributes and the data you store, but only during that timeframe. List of reserved attributes Customer.io has reserved attributes to support core functionality in the platform. Attribute Purpose Required Data Format id A unique identifier for people. If the id does not yet exist, we create a new person. When importing by id Our default id limit is set to 150 characters. All valid UTF characters are allowed. email A person’s email address. If your workspace uses email as a unique identifier (the default setting for new workspaces), and the email address does not yet exist, we create a new person. The TO line of your email templates is prefilled with this attribute. When importing by email Valid RFC 5322 email addresses. cio_id A unique, immutable identifier set by Customer.io, set automatically when you add a person. Created by Customer.io string created_at Recommended. Holds the date when a profile was created. This value lets you to take advantage of timestamp operators in segments and helps you determine the age of a person’s profile. Optional unix epoch _created_in_customerio_at The date-time when a person was added in Customer.io. This value can be different from created_at. In the API, this value is represented by the timestamp for the cio_id attribute. Created by Customer.io unix epoch unsubscribed Determines whether a person is subscribed or unsubscribed from your campaigns and newsletters. (reference) Optional We support any case of true (i.e. TRUE, true, tRUe, etc.), 1, or "1" to represent unsubscribed. Any other value is considered “false”, or subscribed. mobile_ad_id Used to record either Apple’s Advertising Identifier (IDFA) or Android’s Advertising ID (AAID). This id can improve match accuracy in Google or Facebook Ads when using Ad Audiences. Optional Apple’s Advertising Identifier (IDFA) or Android’s Advertising ID (AAID) email_sha256 To keep your data secure, you can hash your customer email using the SHA256 algorithm before sending it to your account. This field is only used in Ad Audiences and cannot be used to send messages. Optional Valid encoded sha256 mobile_ad_id_sha256 To keep your data secure, you can hash the mobile_ad_id using the SHA256 algorithm before sending it to your account. This field is only used in Ad Audiences. Optional Valid encoded sha256 Customer.io has also reserved these object and relationship attributes: Attribute Purpose Required Data Format cio_object_id A unique, immutable identifier for objects provided by Customer.io. If this does not yet exist in your workspace, we create a new object. When importing by cio_object_id object_id A unique identifier for objects. If the object_id does not yet exist, we create a new object. When importing by object_id Our default id limit is set to 150 characters. All valid UTF characters are allowed. objectId String An analog for object_id in some Customer.io integrations. relationship Used to reference relationships to objects. Cannot be used as the name of an object attribute. To reference relationships in liquid _relationship Used in relationship-triggered campaigns to reference audience members who did not trigger the campaign. Cannot be used as the name of a customer attribute. To reference relationships in liquid created_at Unix timestamp when the object was first created. Used when listing objects in the UI, for example. No Unix timestamp timezone The user’s time zone. Used for sending localized messages. No Region Format What can you do with customer attributes? Customer attributes are core to your messaging and workflows. They help you determine who to send to and the type of messaging and content that would engage them. You can use customer attributes in a variety of ways: To personalize messages and webhooks using 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 group people together according to shared characteristics through data-driven segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. To start campaigns using the Attribute or Segment trigger To send people down different pathways in campaigns. Learn more about branches. To limit who receives specific messages or enters delays through action conditions in campaigns Review our info on case sensitivity across the platform, so you know how to efficiently search for attributes and include them in conditions in workflows and liquid logic. Update customer attributes From a person’s page, you can manage the data you want stored on their profile including their identifiers, custom data, and subscription statuses. Go to a person’s profile. Click the Attributes tab and then Edit Attributes. Update the attribute name or value. Remember that you can’t update the name of reserved attributes, like id. Save your changes. Learn more about updating subscription statuses in these other articles: Unsubscribe from all messaging (available out-of-the-box in all workspaces); you’ll store this on the unsubscribed attribute. Unsubscribe from topics (only available if you’ve enabled a subscription center) Remove attributes from a person You may want to remove attributes so people are no longer a part of certain segments, trigger campaigns, or simply to clean up unnecessary data. Go to a person’s profile. Click the Attributes tab and then Edit Attributes. Click to remove the attribute. Save your changes. Remove an attribute from your workspace To delete an unwanted attribute from your workspace, you need to remove it from all segments and workflows that use it. Go to your Data Index. Click to open your attribute. Under Usage, go to where it’s used and remove it or archive the workflow. Click at the bottom of the attribute’s page in your index. This deletes the attribute from all profiles that use it. If that attribute is stored on any customer profiles, it may take 24 hours (or longer) for us to fully purge the attribute from your workspace. --- ## Manage devices URL: https://docs.customer.io/journeys/managing-customer-mobile-devices/ If you want to send push notifications, you can register your users devices with us. [Our SDKs](/integrations/sdk/) automatically associate device tokens with people when you identify a person, but you can also use [our API](/integrations/api/#operation/add_device) to add devices. You’ll find a person’s devices by going to the People page, selecting a person, and going to their Devices tab. For each device, we show the device platform platform, token, and when the device was “last used”. Last used indicates the last time you identified a device—something you typically do when someone logs into your app, or opens your app and is already logged in! You can hover over the truncated token to see the full string and copy it to your clipboard. This is especially useful when you need a device token for sending a test push notification from the composer. And in the activity stream, you can click on each individual row for more details. Here’s an example with an initial API call to add a device: Add and Delete devices In general, we suggest that you integrate with our SDKs to add and delete devices. They can help you associate device tokens with people you identify and remove tokens when you stop identifying them. Otherwise, you can integrate directly with our API to add and delete devices. You can’t add devices in the UI, but you can delete as many as you like. We automatically delete unregistered devices Device tokens are ephemeral: they become invalid when a person logs out of your app, and a push service can change them at any time. Beginning January 31, 2023, when a device token becomes unregistered, it is invalid and will never become valid again; if a person logs into your app again, they’ll get a new device token. To prevent invalid tokens from stacking up on people in your workspace, we automatically remove devices when they’re invalidated by your push services. If, when you send a push notification, your push service (FCM or APNs) responds with an unregistered message, we’ll automatically remove the token from your workspace. flowchart LR a[send push]-->b{Is the push sent?} b-->|yes|c[display push] b-.->|no, device token is unregistered|d[delete device] class d mermaid-error classDef mermaid-error fill: #ffedf0,color: #69002c, stroke:#69002c Device attributes Devices have their own attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. We collect last_status automatically. We also set a last_used attribute based on either the timestamp in your request to add or update a device, or the time of the request itself. When you add or update a device using our SDKs, we’ll set the platform automatically for you; otherwise, you must send the platform with each request to our API. If you use our SDKs, you’ll automatically collect additional device attributes by default. Whether or not you use our SDKs, you can also set custom attributes for each device—similar to attributes you set for people, but specific to each mobile device token. When you go to a person’s Devices tab, you’ll see each device’s PLATFORM, DEVICE LAST USED and LAST RESULT attributes. Click a device to reveal additional information and attributes attached to it. You can use device attributes anywhere you would otherwise use a person’s other attributes—True/False branches, Action Conditions, or segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. But you cannot use device attributes with Liquid today. Custom device attributes When you identify or update a device, you can pass an attributes object containing additional attributes specific to a device. If you use our SDKs, we capture a number of predefined attributes automatically—unless you disable automatic attribute collection. You can segment and filter audience members based on these attributes. However, device tokens can change—sometimes based on user behavior, like when a person uninstalls and reinstalls your app, and sometimes based on your messaging service (FCM or APNs). When you set attributes on a device, consider wether you need to retain an attribute past the life of the device token, and if an attribute is truly relevant to the device. The following attributes are automatically collected by alpha versions of the SDK, set automatically by Customer.io, or available to our APIs. If you disable automatic attribute collection in the SDK, we’ll stop automatically collecting properties other than platform, last_used, and last_status. id string Required The device token. Segment people based on their devices When you create segments, you can segment people based on their device attributes—their platform, the status of their last message, or when devices were last_used. --- ## Resolve duplicate people URL: https://docs.customer.io/journeys/merge-people/ If you inadvertently create duplicate instances of a person, you can merge them and consolidate information to accurately represent a single "person" in your workspace. How merging people works You can merge people manually or automatically. When you merge two people, you pick a primary person and a secondary person. The primary person remains after the merge and the secondary is deleted. This process is permanent: you cannot recover the secondary person. Depending on the data type, the primary person may retain its data or inherit the data from the secondary person.  The primary person might enter new segments and campaigns When you merge people, the primary person inherits new attributes, events, campaign history, etc, which can cause the person to join segments. The primary person can enter new segment-triggered campaigns, but will not enter new event-triggered campaigns; events inherited from a merge cannot trigger campaigns. When the primary person inherits the data from the secondary person The primary person inherits the following information from the secondary person: Attributes that are not set, or are empty, on the primary will be overwritten if the secondary person has values for the attributes. Event history: the most recent 29 days of events are merged immediately. It can take up to an hour to merge older events. Events merged from the secondary person cannot trigger campaigns. Campaign journeys that the primary person has not entered A journey is a person’s path through a campaign. If the secondary person has started a journey that the primary person has not, the primary person continues on that campaign journey after the merge. If the secondary person has completed journeys that the primary person has not, the primary person gains these historical journeys after the merge. This may be important for determining entry (or re-entry) criteria for subsequent campaigns, segments, etc. Manual segments that the primary person did not already belong to Message deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. history Conversions A conversion happens when a profile performs an action (enters/leaves a segment or sends an event) after interacting with a message (receives, opens, clicks) within a time window. Considering this, it’s possible one of the profiles has performed the action and the other hasn’t. In this situation: If the primary profile performs the action while the secondary only interacts with the message (no action performed), the primary profile will not inherit a conversion. The primary profile will only inherit the interaction from the secondary profile. If the secondary profile performs the action, the primary profile will inherit a conversion AS LONG AS the action is an event OR the primary profile inherits the attribute change that made the secondary profile enter/leave the goal segment. Relationships and relationship attributes that the primary did not have When the primary person retains its data When both the primary and secondary people have conflicting data—both people have the same attribute, subscription preference topic, or have experienced the same campaign—we pick a “winner” to resolve the conflict; the “winner” value remains after the merge, and the “loser” is lost with the secondary person. See the sections below to learn more about how we resolve conflicts in the merge process. Resolving conflicts between attributes If the primary and secondary people both have the same attribute, and the primary person’s attribute value is not empty, the primary person’s attribute “wins”: it remains on the merged person and the secondary person’s attribute value is lost in the merge. For example, if both the primary and secondary people have a first_name attribute, the first_name on the primary person remains and the attribute on the secondary person is lost in the merge. Resolving conflicts between subscription topic preferences If you’ve enabled a subscription center in your workspace, then people have subscription topic preferences. The subscription topic preferences of the primary person “win” over the secondary profile. These preferences remain on the merged person and the secondary person’s preferences are lost. For example, if you have a subscription topic “Product Education” and it’s set to TRUE for the primary profile but FALSE for the secondary profile, the merged profile will have a value of TRUE. Resolving conflicts between relationships If the primary and secondary people both have the same relationship, the primary person’s relationship attributes “win.” We add relationship attributes from the secondary to the primary if the primary did not have these attributes. We also overwrite empty values in the primary if the value is populated in a relationship attribute in the secondary profile. Resolving conflicts between campaign journeys A journey is a person’s path through a campaign. If the primary and secondary people have both started or completed the same campaign, we pick a “winner” journey to remain on the merged person. The “winner” depends on whether the people have completed a journey in the past or are active in a campaign at the time of the merge. If both people are active in a campaign, the primary person continues its campaign journey. The secondary person exits the campaign early. If both people have completed a campaign, the merged person keeps the most recently started campaign journey. The history of completed campaigns doesn’t necessarily affect people, but you may use it as re-entry criteria for follow-up campaign or segment membership. How to identify people How you identify people to merge depends on your workspace settings. Is email disabled or enabled as an identifier? Go to General Workspace Settings to find out. If email is enabled, you can identify people by email, cio_id, or id. You can also automatically merge people in this case. If email is disabled, you can identify people only by cio_id or id. You must also ensure id is set to “Reference people by cio_id” to make the merge possible. Merge two people together  This operation is permanent You cannot undo a merge, nor can you stop it after it starts. Make sure that you’re ready to merge people before you begin this process. When you merge two people, you pick a primary person and merge a secondary person into it. The primary person remains after the merge and the secondary is deleted. Merging people may cause the primary person to enter or exit segments and campaigns based on information you merge from the secondary person. Make sure that you understand how a merge will impact the primary person before you initiate a merge. Go to the People page. Click Merge. You can pick people before you click merge. Select your Primary and Secondary people and click Next. If you already selected people, you can click Swap to reverse your primary and secondary people. Remember, when you merge people, the primary gains some information from the secondary person, and any data that is not merged to the primary person is lost. Review your changes one last time. When you’re sure you want to merge people, click Confirm and merge. You can search for attributes on the primary person, ensuring that the values on the primary person are correct before you finish your merge. You’ll also see a count of the attributes, events, and deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. on each person. Items remaining on the Secondary person are lost. When you confirm a merge, you’ll go to the newly merged person’s page. Attribute changes take effect immediately, but event history may take a moment. Merge people via the API You can also merge people using the Track API. The payload contains primary and secondary profile objects. As when you merge people from the People page, the primary profile remains after the merge. The secondary profile’s information is merged into the primary, and then it is deleted.  Primary profile must exist The primary profile must already exist in Customer.io for the merge API call to work. If the primary profile doesn’t exist, the merge request won’t do anything. curl --request POST \ --url https://track.customer.io/api/v1/merge_customers \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data '{"primary":{"email":"cool.person@company.com"}, "secondary":{"email":"cperson@gmail.com"}}' Automatically merge people If your workspace supports email or ID as identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., there are situations where you can inadvertently create duplicate people. If you created your workspace using the email or ID option after September 24, 2021, we automatically recognize and merge duplicate profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. if you send an identify call with id and email, and all of the following things are true: The id and email in the request matched different people. The id and email in the request matched exactly one person each. The profile matched by id does not have an email The profile matched by email does not have an id. In this case, when we merge people, we treat the older profile as the Primary person and the younger profile as the secondary person. To turn this setting on or off: Go to Settings > Workspace Settings. Click Settings next to Merge options. Enable Multi-identifier profile merge. Merged people in the activity log When you merge people, the activity log reports a People merged activity on the primary person. The activity Source shows how the merge was performed, through the People page, the API, or automatically (if your workspace has the setting enabled). Merged people in data warehouse sync Like the Activity Log, person merges are represented in your data warehouse as attribute changes and a deleted person. In the People table, the secondary person is deleted. In the Attributes table, the primary person shows new entries for each attribute inherited from the secondary person in the merge. Segment membership and campaign triggers after a merge When you merge people, you may cause the primary person to enter or exit segments and campaigns. In many cases, these changes are easy to understand—the primary person directly inherits a manual segment membership or an attribute that adds the person to a data-driven segment. However, it’s important to remember that the primary person also inherits campaign and delivery history from the secondary person, both of which can affect segment membership and campaign filters. For example, if both the primary and secondary people received the same message, the merged person will become a member of a data-driven segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. of people who received that message more than once. --- ## What's the Last Visited field, and how do I use it? URL: https://docs.customer.io/journeys/last-visited/ Everybody in your workspace has a *Last Visited* attribute. This value is updated based on page view activity from our JavaScript snippet or Event calls to the API, helping determine the last time that someone was active on your website. You may have noticed that each person in your Customer.io account has a Last Visited field: If this field is blank in your account, there are several ways to set up and make use of it. Updating Last Visited with page views By default, the “Last Visited” attribute is updated based on page view activity. For profiles where page views exist, we will update “Last Visited” accordingly. Page views can be tracked using one of two options: Adding our JavaScript snippet to each page you want to track. The snippet will automatically track page views on pages where the snippet is present. Making event calls to our API with the type set to page. Sending page views for Last Visited via Segment If you’re integrating with us via Segment, their page call lets you record whenever a user sees a page of your website or app, and sends them to Customer.io. If you’re having trouble with this, we can help you troubleshoot! Email us with the following: The date and time (as precise as possible) of the API calls that were made. The exact code & data used to call the API, ideally shared in a Gist or via Pastebin to keep formatting intact. Some sample users that should have received the calls (their id and/or email). Updating last visited with mobile screen views We automatically update your audience’s “Last Visited” attributes when you send screen events, indicating that someone opened a screen in your app. You can track screen views by integrating with our SDKs or writing your own custom code to send events using the Track API. Our SDKs support automatic screen tracking, making it much easier to implement screen view events. Updating Last Visited by sending attributes If you want to update the “Last Visited” field without relying on page views, you can also pass over the attribute _last_visit with the timestamp of the last visit in unix (seconds since epoch format) when making identify calls to Customer.io through your integration. Within Customer.io, you can do this within event triggered campaigns by adding an Create or update person action your workflow. For example, if I wanted “Last Visited” to update whenever a user last signed in and saw my app’s home page, this is how that might be set up: The trigger would be an event (signing in, viewing the page, or interacting with a certain interface element on that page, to name a few event examples), and the specific Create or update person action looks like this: When the event occurs, the “Last Visited” field will be updated with that event’s timestamp– that’s what {{event_timestamp}}will do. Note that this won’t be seen in a user’s Activity feed (because _last_visit is a special internal attribute), but their Last Visited field will update. Filtering/Segmenting by Last Visited You can’t segment on a last visited attribute directly. But if you’re tracking page or screen views, you can get a similar result by looking for people with a Page or Screen condition and use the Refine option to set a time frame. Page view Page view Screen view Screen view Keep in mind that we can only match URLs or screens that your integration is sends in your page or screen events! We have a whole page to help you segment based on page views that can help set up the right conditions. --- ## Delete people and suppress profile IDs URL: https://docs.customer.io/journeys/deleting-users/ You can delete people from Customer.io from our UI or API. No matter which method you choose, when you delete a person, you're deleting all data with them too. There is no way to recover a deleted person's data! If you are honoring a person’s request to be forgotten, or simply want to prevent an identifier from being added to your Customer.io workspace in the future, you can suppress that identifier when deleting people. Types of suppressions Suppressions can mean a few different things: In Customer.io, you can suppress a deleted person’s identifiers so they can’t ever be readded to your workspace (good for GDPR compliance). Email service providers (ESP) suppress an email address to help you maintain good deliverability. Email service providers (ESP) maintain lists of suppressed email addresses when people log a spam complaint or the address experiences a hard-bounce. ESPs won’t send to suppressed email addresses. If you use Customer.io as your ESP, you can view and manage your list in Workspace Settings > Email > Suppression List. Otherwise, if you use a Custom SMTP server, you must manage your email suppression list through them. Delete people You can delete people in bulk or individually in your workspace. Go to the People page to get started. Delete people in bulk You can filter for, select and delete individuals from the People page. Alternatively, if you need to delete all of the people included in your search results (such as those matching a specific segment), click Select all ### people: When you’re ready, click Delete forever to confirm your action. Delete a single person You can delete a single person with the method above or by clicking Delete forever on their individual page: Once again, confirm that you want to delete them, and you’re good to go! Delete people via API See our API Documentation for help deleting people programmatically. DELETE https://track.customer.io/api/v1/customers/:id Remember, if you’re still sending data to Customer.io via other means (such as the JavaScript snippet), you can still re-create people you’ve deleted!  Use the correct URL for your region If your account is based in the European Union (EU), make sure you use endpoints beginning with track-eu. We redirect traffic from US endpoints to EU-based accounts, however the traffic still passes through US servers and data may be logged in the US. How suppressing IDs works You can “suppress” people you delete. Suppressing a person redacts activity attributed to the person and prevents you from adding a person with the same identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. to your workspace in the future. This is typically used for GDPR compliance. Suppressing a person suppresses all of their identifiers, but consider how you identify people according to your workspace’s settings: If your workspace is ID-only, suppressing a person suppresses their ID. If your workspace is set to identify people by email or ID and you suppress a person, you cannot add another person with the same ID or email address; both identifiers are suppressed. If you need to export a list of suppressed IDs AND email addresses, contact Customer.io.  Changing workspace identifiers affects suppression If you have an ID-only workspace and you change your workspace settings to email or ID, the email attribute belonging to suppressed profiles will also be suppressed. If you delete and suppress a person’s identifiers: You cannot reuse the deleted person’s identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. until you unsuppress them. Any attempt to re-add a person with a suppressed email or ID is ignored or results in an error (CSV imports, API). Activity attributed to the deleted and suppressed email or ID (the person) is redacted. Activity Logs show forgotten (anonymous) entries for suppressed identifiers. If you delete and don’t suppress a person’s identifiers: You can reuse a deleted person’s email or ID to create a new person. A person you add with the previously-deleted identifier will have no prior data, including their unsubscribe status: the re-added person can trigger previously received campaigns. Activity remains attributed to the deleted person’s cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. If you re-create a person with the same identifier as the person you deleted, the new person does not regain the activity history of the previously-deleted person. Suppress IDs After you click Delete forever, you’ll need to confirm your action and decide whether to suppress people’s identifiers. Suppress IDs via API You can suppress an identifier to redact activity attributed to it and prevent a person from being added to your workspace with the same identifier again. If you suppress an identifier, we’ll ignore API calls referencing the identifier in the future. If you attempt to re-add a person using an identifier that you previously suppressed, you’ll receive an error. Suppressing a person through our API also deletes a person. You don’t need to call both the delete and suppress endpoints. Use the following call, where the identifier is any of your workspace’s unique identifiers (normally id and email). POST https://track.customer.io/api/v1/customers/:identifier/suppress For more information, see our API Documentation.  Use the correct URL for your region If your account is based in the European Union (EU), make sure you use endpoints beginning with track-eu. We redirect traffic from US endpoints to EU-based accounts, however the traffic still passes through US servers and data may be logged in the US. Unsuppress IDs via API If you previously suppressed a person’s identifiers, you can unsuppress them so they’re available to use again in your workspace. If you add a person with the unsuppressed ids, we make a new person without any history (messages, journeys, etc.) formerly associated with the ids. You can only unsuppress deleted ids programmatically, not within your workspace.  Use the correct URL for your region If your account is based in the European Union (EU), make sure you use endpoints beginning with track-eu. We redirect traffic from US endpoints to EU-based accounts, however the traffic still passes through US servers and data may be logged in the US. --- ## Export a person's data URL: https://docs.customer.io/journeys/single-user-export/ You can export all of your Customer.io data for an individual person. Need to export all data associated with a single person? No problem!  Exports will include all attribute and event data for the selected profiles. If this includes sensitive data, you must purge that data before passing it to customers. Export data for a person Head to the individual Person page for the end-user whose data you need to export, and select the ‘Export Profile Data’ option in the dropdown: This will export all of the data that we have for a specific end-user. It may take a little while, but we’ll let you know when it’s done, and it will also appear on your Exports page like this: Hover over the count of rows/items exported for more details. That’s it!  Note: You can also export the attributes for a user or group of users to CSV. This is available via the People page; here’s how! What data we store For each user, we store several types of data, all available for export: All user attributes All user event data All relationships to objects User mobile device IDs, last_used timestamps, and platforms (if you’re sending us device data) All deliveries for that customer, with their full content Delivery metrics and metadata (delivery ID, the campaign, message name, type, etc.) What an export looks like A single export will contain several different .json files for the above data types: attributes.json contains all current attribute values for that end-user details.json contains deliveries and their content for that customer, one per line devices.json contains the device ID, platform, and when the device was last used– for up to 25 devices you associate with a given end-user (sorted by last_used) events.json.gz and events2.json.gz will contain all events (and their associated data) performed by the customer; from our event logs, one per line metrics.json includes all individual delivery stats (timestamps for activities, and meta information), one delivery per line relationships.json contains all relationships this person has to objects Expiry After 60 days, the export expires, and we delete it from your workspace. If you need the export again, you can always generate a new one! --- ## Export data for multiple people URL: https://docs.customer.io/journeys/exporting-users/ You can export data for a group of users to a CSV. You might do this to test your integration when you're getting started, or if you want to transfer a group of people to another workspace or external system. There are a few ways for you to export people data from the People page in Customer.io. You can… Export all your users and all their data Export all your users, but only with specific attributes Export a set of users who meet certain conditions — with all of their attributes, or a specific set  Exports will include all attribute data for the selected profiles. If this includes sensitive data, you must purge that data before passing it to customers. Export people First, head to the People section of your account. There, you’ll see the Export to CSV dropdown: When you click this, you’ll see four options: All attributes: Export all users and all their attributes. Displayed attributes: If you’ve displayed specific attributes in the UI via the Edit Displayed Attributes button , this option will export those. Choose attributes: Choose which attributes to export across all data you send to your workspace. Export other: Export duplicates or devices in your workspace. You can also export all attributes or a selection. For each option, the id attribute will always be included in your export. Export everyone In the Export to CSV dropdown, select “All attributes” and you’re done! It’s that simple; you’ll get a CSV with all your users and all their attributes. Export displayed attributes The “Displayed attributes” option will result in an export of all users in Customer.io with the attributes that are displayed in the People table, in addition to the attributes id, city and state in addition to created_at. Export specific attributes If you want to just export a set of specific attributes immediately without viewing them first, select Choose attributes… in the export dropdown, and then specify which attributes you’d like to export: You can also combine this with exporting users who meet certain conditions. Export relationships You can retrieve data for people’s relationships to objects by checking this box on the final modal before confirming export: You can identify people by the attribute cio_id in the CSV. You cannot export more than 2 million relationships. If you attempt to, the export will fail.  If you have disabled object types, the relationship CSV will NOT contain relationships to them. Export users who meet certain conditions By clicking the “adding filters” toggle switch, you can specify segment and/or attribute conditions to refine the users to be exported. Then, in the dropdown, you can choose to export those people with all their attributes, or choose exactly which ones you’d like, as above. So we can refine by segment or attribute conditions: …and then use the ‘Choose attributes…’ dialog to specify exactly which attributes to export for those users. Export suppressed emails and IDs Contact our technical support team to request a list of suppressed emails and IDs from Customer.io. You might do this if you need to verify that someone is on the list, or to remove them from it. We provide lists of suppressed emails and IDs as CSV files. We hash suppressed emails in the CSV to protect the privacy of the people on the list. But the CSVs we provide also include a function to reverse hashed values so that you can verify the addresses you’ve suppressed.  Are you looking for your ESP suppression list? You, or your email service provider (ESP), can suppress email addresses so that the ESP doesn’t send messages to that address. This list is separate from Customer.io’s suppression list. You’ll either find it in your workspace settings or you’ll need to work with your ESP to get your ESP suppression list. What segment-filtered exports look like In the above example, if we export everyone in the “Advanced Users” segment, and then choose to export their email and _last_visit attributes, the resulting exported CSV looks like this: The 25 in _segment:25 is the id of the segment used. This is found on the individual segment page, as well as in the overview: The values in this column are UTC timestamps. The column to the right is the human-readable version of that timestamp. Export duplicate people You can export a list of people who share the same attribute in your workspace. You might do this to dedupe people in your workspace by a non-identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. value, or to troubleshoot integrations. For example, if you don’t use email as an identifier in your workspace, you might export duplicates to figure out if you have multiple instances of the same email address. The same principle works for phone numbers as well! Your export will show a list of people, grouped by the duplicate attribute, as shown below. Regardless of the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you choose to show, the list will always include your audience’s identifiers. id cio_id email first_name 1 acb50600a307a401 person@example.com Alex 123b41623a307a402 person@example.com Alexandra To export a list of people with the same attribute: Go to the People page. Click Export to CSV and click Export other. Pick the attribute you want to deduplicate on under All duplicates of…. (Optional) Determine the specific attributes you want to export. This can help you limit the time it takes to export your file, and the amount of data you see. Click Export to CSV. We’ll email you when your export is complete. Or, you can find your export on the Exports page (under Configure data, click More). Export all devices You might want to export a list of mobile devices to clean up bounced/suppressed devices in your workspace. Or, in rare cases where people share a device, you might export devices to find duplicate tokens—a single device token shared by more than one person in your workspace that might result in duplicate notifications. Go to the People page. Click Export to CSV and click Export other. Select All devices in this workspace (Optional) Determine the specific attributes you want to export. This can help you limit the time it takes to export your file, and the amount of data you see. Regardless of the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you choose to show, the list will always include your audience’s identifiers. Click Export to CSV. We’ll email you when your export is complete. Or, you can find your export on the Exports page (under Configure data, click More). Helpful notes Expiry After 60 days, the export expires, and we delete it from your workspace. If you need the export again, you can always generate a new one! No exporting based on email address As mentioned above, you can add filters and export those users, but we don’t support exporting based on the “filter by email address” field. Bonus attributes In addition to all the attributes that you’re sending to Customer.io, you can also export a couple of internal ones that we track. These are: _created_in_customerio_at: this is when a user was first created in Customer.io. This is different from created_at because it’s specific to when this user was created in Customer.io, not in your system or application. _last_visited: the most recent timestamp of a pageview event for that particular user Why the id attribute is always included We do this in order to make it easier for you (and our support team, if necessary) to troubleshoot your exports, by ensuring that there is always a reference to the unique identifier of your end-user in Customer.io. --- ## Storing and using JSON URL: https://docs.customer.io/journeys/getting-started-with-json/ You can pass data into Customer.io, and store it, in complex JSON. If you're not familiar with JSON, this page can help you get started. If you're familiar with JSON, you can skip this page and move on to [integration planning](/journeys/getting-started-integration-planning/). How it works JSON stands for JavaScript Object Notation. It’s a common format to send and store data over the internet, and the way in which you’ll send, store, and reference data in Customer.io! If you’re a Customer.io user, you’re already using JSON—even if you don’t know it! For example, when you send an eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. or set an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in the Customer.io interface, you’re actually using JSON! This event... is this JSON { "name": "example_event", "data": { "stringProperty": "value", "numberProperty": 1, "booleanProperty": true } } JSON is simply the syntax you’ll use to store values; it provides a notation you’ll use to reference your values in 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}}., segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., and other places in Customer.io. In many cases, you’ll store simple values—like a person’s name or their phone number. But you can also nest properties inside of attributes, event data, and so on—like if you want to store a list of purchases or appointments. Key-value pairs: storing your data JSON data is a set of keys and values. A key stores a value, like an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. name represents information about a person or your street address represents your home. You might think of the key as the “address” of a value. For example in the attribute "first_name": "Jack", first_name is the key and Jack is the value. If you wanted to reference a person’s first name attribute in a message, you’d use {{ customer.first_name }} where “customer.first_name” is the key. In your message, we’ll print Jack. A key can contain child keys as well, but we’ll get to that in the objects section below. The value is the data that you store. In JSON, the syntax you use for a value determines how we treat it. In general, you should understand the difference between string, number, boolean, object, and array type values. { "string": "literal value", "number": 1, // this is a number; it doesn't use quotes "boolean": true, // true/false values don't use quotes "simple_array": ["index0", "index1"], // order matters here! simple_array[0] equals index0 "object": { // order doesn't matter here. object.key will always return "string" whether "key" is the first or last item in the object. "key": "string", "number": 1, "boolean": true } } Simple strings, numbers, and booleans Many attributes and event data properties contain simple values. The format of those values determines how we interpret them and what you can do with them. In most cases, this is common sense: numbers: if you store a number value without quotes, you can do math with it! text: if you store a text value with quotes, you can capitalize it or modify it to fit your message. In JSON, a text value is called a string. true/false: storing true or false without quotes makes it easy to answer binary yes/no-style questions. In JSON, these are called boolean values. Text—called strings in JSON—are wrapped in quotes. We sometimes call this a “literal” because the quotes represent a literal sequence of characters. Strings are the only values that you wrap in quotation marks. Don’t wrap a number or a boolean (true or false) in quotes or you’ll change how we treat it in the app! Objects An object is a group of key-value properties wrapped in curly braces—{}. Within an object, keys must be unique. You can use objects to group keys that belong together and might be duplicated. For example, in a purchase, you might want to capture the total amount a person spent, the items they purchased, and the purchase_date. Every purchase is likely to have these same keys. Grouping these keys in an object provides a way to represent a single purchase and provides a predictable structure of information for each purchase. You’ll reference properties inside an object with a period. For example, when you reference a customer’s first_name attribute in liquid, you use {{ customer.first_name }} because the first name attribute resides inside the customer object (or namespace)! { "an_object": { "property1": "value", "property2": 2 } } Arrays An array is an ordered group of properties wrapped in square brackets—[]. Unlike in objects, the order of items in an array matters! For example, imagine an array of favorite foods. { "favorite_foods": ["pizza", "mangoes", "ice cream"] } If I want to reference pizza in the array, I’d use favorite_foods[0]. The index—the number in brackets—tells us the position in the array that we want to reference and fetches that value. Arrays are zero-indexed, so the first item in the array is 0, the second is 1, etc. In Customer.io, I can also search against the entire array with empty brackets; for example, favorite_foods[] equals pizza would match if any value in the array is pizza. Arrays can contain values or objects! Taking our example from the objects section, you might have an array of purchases, where each item in the array is an object representing a purchase! Arrays of objects An array of object provides a way to group sets of keys and value—a group of items in a purchase where each object in the array represents an item. In the example below, you could get the name of the first item in the purchase with purchase.items[0].name. When you create segments, filters, or other conditions, you can match against all item names using purchase.items[].name equals Monitor. This would return true because at least one item name is “Monitor”. { "purchase": { "items": [ { "id": 123, "type": "computers", "name": "Monitor", "price": 25 }, { "id": 456, "type": "computers", "name": "Mouse", "price": 15 } ] } } Referencing JSON data: dot notation You may have noticed that throughout this document, we’ve shown examples separated by periods—like key.anotherKey. This is known as JSON dot notation, and it’s how you’ll point to values in JSON—like when you create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. or use a value in 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}}.. Take the example below. You’ll use a dot to separate parent and children properties. parent.child: the dot indicates that the child property is inside the parent object. Use this to reference the child property in the parent object. parents[].child: the square brackets indicate that the parents array contains objects—where each object represents a parent. Within each parent object is a child property. Use this to represent any property called child inside the parents array. parents[0].child: the number in square bracket points to a specific item in the parents array. In this case, 0 refers to the child property in the first object in the parents array. parents[0].children[0].name: here we have a nested arrays of objects. In this case, we’re referring to the name property in the first object in the children array, in the first object in the parents array. { "parent": { "child": "simple object" }, "parents": [ //array of objects { "name": "Ken Griffey Sr.", "child": "Ken Griffey Jr." }, { "name": "Bobby Bonds", "child": "Barry Bonds" } ] } The ? operator: for arrays or objects On some screens in Customer.io, you may not have access to sample data. This can make it hard to remember the exact relationship between nested JSON keys—are they in an object or an array of objects? That’s why we have the ? operator: you can use it anywhere you would otherwise use square brackets to indicate a key that might be an array. For example, if I didn’t know if the parents array above were an object or an array, I could use parents?.name. Whenever we encounter a key using the ? operator, we’ll process it gracefully whether the actual data is an array of objects, object, or simple array. --- ## Events URL: https://docs.customer.io/journeys/events/ Events are actions people perform in your app, on your website, etc—things like button clicks, scrolling to the bottom of a page, or purchases. When you send us events, you can start campaigns and segment your users based on the things they do (or don't do) in your app. How does it work? Where an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. represents something you know about a person, an event represents point-in-time data about something a person did. Throughout Customer.io’s APIs and SDKs, you’ll use the track command to send events. You can use events to trigger campaigns, as conversion criteria, or as segment criteria, helping you respond relevantly to the things people do on your website or in your app. An event consists of an event name and data about the event. The name is how you’ll target events in Customer.io, and the data represents information that you might reference in 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}}., segment criteria, and so on. Event name and data Event data can take any structure. But it generally helps to make all events with the same name take the same data structure. You can use your event data in 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 display relevant information in messages or in segments to filter people who performed the same event containing different values or data. The method you use to send event data changes slightly depending on your integration—JavaScript, API, mobile SDK, but you can access nested event data in your campaigns, segments, and so on with standard JSON dot notation. For example, if you send an event representing a person’s abandoned cart like the example below, you could create a segment of people who abandoned their cart with a monitor using items[].type equals monitor. { "name": "abandoned_cart", "data": { "total": 215, "items": [ { "category": "computers", "type": "monitor", "price": 200 }, { "category": "cables", "type": "hdmi", "price": 15 } ] } } Reserved event data There are a few attributes in the data object that are reserved by Customer.io—recipient, from_address, and reply_to. These properties automatically override settings in emails triggered by the event. For example, a recipient in an event will override the To field in your emails. { "name": "event_name", "data": { "recipient": "person@example.com", //email override for To field "from_address": "from@example.com", //email override for From field "reply_to": "manager@example.com" //email override for Reply To field } } How do I send event data? The methods you use to send events depend on the mediums you use to communicate with your audience and the types of events you want to track. You can send events to Customer.io using one or more of the following: From your website: The JavaScript snippet From an integration: The API From your app: Our Mobile SDKs Manually: By uploading a CSV From inside a campaign: With the Send Event action Send events using JavaScript When you add our JavaScript source snippet to your website (or our classic JavaScript sdk) to your website, you’ll call the track method whenever your users perform an event that you want to record in Customer.io. The track function works based on the current browser session, so you don’t need to tell us who to associate the event with. You can send additional event properties representing values from the event. You can use additional event data in 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}}., as criteria when segmenting your audience, and so on. JavaScript Source (Recommended) JavaScript Source (Recommended) cioanalytics.track('likes_pizza', { favorite_topping: "pepperoni", dislikes: ["anchovies", "onions"] }); Classic JavaScript SDK Classic JavaScript SDK _cio.track('likes_pizza', { favorite_topping: "pepperoni", dislikes: ["anchovies", "onions"] }); If you send an event before you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone, we’ll log an anonymous event. By default, we’ll associate anonymous events with a person after you identify them. See anonymous events for more information.  We trim leading and trailing spaces from event names You would not be able to reference events with leading or trailing spaces in the name, so we automatically trim leading and trailing spaces from event names for you. Send events using the API While some of the other methods for sending events might support a specific medium—like your website or your mobile app—you can always send events using our Pipelines API (recommended) or our classic Track API. Pipelines API (Recommended) Pipelines API (Recommended) curl --request POST \ --url https://cdp.customer.io/v1/track \ -u api_key: \ -H 'content-type: application/json' \ --data-raw ' { "userId": "97980cfea0067", "event": "likes_pizza", "properties": { "favorite_topping":"pepperoni", "dislikes":["anchovies", "onions"] } }' Classic Track API Classic Track API curl --request POST \ --url https://track.customer.io/api/v1/customers/{identifier}/events \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data '{"name":"likes_pizza","data":{"favorite_topping":"pepperoni","dislikes":["anchovies", "onions"]}}' As with our JavaScript snippet, you can send an event before you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone, to log an anonymous event. When you identify someone, you can pass their anonymous_id attribute to associate anonymous events with a person after you identify them. See anonymous events for more information. Send events using the SDK When you use our SDKs, you can send events with the track command. You can also set up our SDKs to automatically send screen events—events representing the pages in your app that your audience visits. See our SDKs for help integrating your app with Customer.io. Uploading a CSV You can upload a CSV containing event data for each person in the CSV. You might do this if you need to backfill events that your audience performed outside of your normal integration path—things your audience did before you integrate with Customer.io. See uploading people for more information about uploading a CSV. If you want to pass events this way, the people in your list must already exist. Unlike other CSV-upload operations, you cannot add new people when you upload event data. To upload a CSV, go to People, click Add People > Upload a CSV. The first two columns in your CSV must be _cio_name, _cio_customer_id. Take a look at an example CSV. You can also include a third _cio_timestamp column representing the date-time when the event occurred if you want to back-date your event. If you don’t include this column, we’ll use time that we process the event as the event’s timestamp. Additional columns are event data attributes that you want to associate with the person. Event data provided via CSV (columns 4 and greater) is flattened and cannot contain nested JSON. For example, a column called property.subproperty will simply become a stringified key called "property.subproperty". col col name required description 1 _cio_name The name of the event. 2 _cio_customer_id The identifier for a person. Depending on your workspace settings, this can be the person's id or email. You must use the same identifier for the entire CSV—you cannot mix IDs and email addresses. 3 _cio_timestamp The date-time when the event occurred. 4+ Subsequent columns contain additional attributes for the data object in the event; one column per event property. These are properties you'll use in 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}}.—i.e. {{event.column_name}}. These columns can have any name, though you may want to make sure your column names don't contain spaces or special characters that might make them hard to use in Customer.io Send events as a part of a campaign You can create an event within a campaign, making it easy to trigger downstream campaigns or add people to segments. For webhook-triggered campaigns, this makes it easy to reshape and associate data from an outside source with people in your workspace, all without having to talk to a developer or write your own code. See the Send Event action to learn more about creating events within campaign workflows. Event segments Once you’re sending us these events, you can then build data-driven segments from them based on actions users have or have not done. Make sure that the event name used in _cio.track('your_event_name') and the event name used to create a segment are the same. You can also set a time frame for the action—for example, “have not created_project in 30 days”, or “have invited_friend in the last 14 days”. Here’s an example without a timeframe: And if you’d like to add the timeframe, click Refine, and adjust the time inputs: Back-date events with event timestamps When you send an event, you can send a timestamp representing the date-time that the event occurred. Format your timestamp value as a Unix timestamp (seconds since epoch). Take care when backfilling events: events with a timestamp in the past 72 hours can trigger campaigns. Before you send back-dated events, make sure that you won’t inadvertently trigger campaigns or send messages that aren’t relevant to your audience. If you don’t provide we’ll use the time we process the event as the timestamp.  We only show activity for the last 30 days in Customer.io. Events that are older than 30 days are logged for use in segmentation (e.g., “event ‘made_purchase’ has been performed at least once in the past 90 days) but they will not appear in your activity logs. Here’s an example of how to send this data using curl to the REST API. If your account is in the EU region, make sure you use https://track-eu.customer.io as the base for your calls. curl -i https://track.customer.io/api/v1/customers/5/events \ -u YOUR-SITE-ID-HERE:YOUR-SECRET-API-KEY-HERE \ -d name=purchased \ -d data[price]=23.45 -d timestamp=1359389415 Format your event parameters without spaces or special characters We recommend that you avoid using spaces in event names and parameters. For example, instead of sending new sign up, use new_sign_up or newSignUp. If you do send us event parameters with spaces, you’ll need to use special notation to reference events: Without spaces: {{ event.sign_up_date }} With spaces: {{ event["sign up date"] }} Event names can include special characters. However, when searching for events—like when you want to create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions.—you may want to escape *, +, or | characters using a \ character to treat them as literals. Otherwise, these characters act as a wild card, additive search, and subtractive search respectively unless you escape them. Anonymous Events You can send anonymous events to Customer.io containing an anonymous_id. When you set the anonymous_id on a person, we associate those anonymous events with a person. This lets you log events before people log in, sign up, or are otherwise identified, and then attribute those events to people after you identify them. Send test events through the UI You can send a custom event through the UI to test event-triggered campaigns, segments, and liquid conditions in your messages. Try sending an event representing you, or a designated test account, to make sure that your campaign and messages behave the way you expect them to. To send a custom event: Go to one of the following places and click Send Event: The Trigger page when setting up a new campaign. Your Campaign Overview page (available after you start an event-triggered campaign) A person’s profile—click Options > Send Event Data Index > Events > select an event. In Activity Logs, select an Event activity and click Resend. Use the search box to select the person you want to send this custom event to. Enter the EVENT NAME as it appears in your trigger, segment, etc. Enter EVENT DATA. These may be items you reference in messages with liquid or criteria for campaigns and segments. Click Send event. Resend an event When you update event-triggered campaigns or segments, you may want to resend events to test your changes. In the Activity Log and Data Index, you can click events to see Recent Activity for a particular event. Select an entry and click Resend to resend an event associated with that person. Deduplicating events You can provide an id with your events, to deduplicate events—if there’s a possibility that your integration might send duplicate events. The id must be a ULID. If two events contain the same id, we won’t process the event multiple times. Deduplicating events helps you accurately represent people’s activity; can prevent people from accidentally entering or leaving campaigns based on the number of times that a person performed an event; and prevents duplicate events from impacting your workspace’s performance. { "name": "my event", "id": "01BX5ZZKBKACTAV9WEVGEMMVRY", "data": { "prop1": "value" } } Events in the Activity Log The Activity Log has two types of dates: Timestamp and Processed At. Timestamp represents the date and time listed on an event. If you don’t set a timestamp, we use the date-time when we receive the event. Processed at is the date and time when we process an event. Anonymous events are not processed, and therefore do not have a “processed at” time. If there is a significant difference between the two, it could be for one of these reasons: An anonymous event was merged to a profile. Anonymous events are not processed until they’re associated with a person, so an anonymous event may be timestamped well before you identify a person and the event is associated with the person. You may have manually set a timestamp on an event. This typically happens when you backdate an event, or want to log the exact date-time that an event occurred and you don’t immediately send the event to Customer.io. Customer.io experienced a processing delay. A note on multiple occurrences of an event: We track each occurrence of an event, and for message-related events, we show both the first time and actual time the event was performed. For instance, we track each time a person opens an email. The timestamp of the opened attribute always reflects the first time the event happened. The timestamp inline with the event name shows the timestamp of the event you’re viewing. If this is the only time the event has been performed for this specific message, both timestamps would match. Data-out for events You can export event data for a person from their profile. Select Options > Export profile data in the top right. You will receive an email with the export or you can find it on the Exports page (under Configure data, click More). The export includes message events (clicked, opened, etc), attribute changes, and custom events (like “Order Completed”). You can also export high-level event data from the data index, like the number of campaigns that use an event and when an event was last received by Customer.io. Go to Data Index > Events. Then click Export to CSV. You can programmatically send message events (clicked, opened, etc) and subscription preferences to destinations outside Customer.io using reporting webhooks. Go to Integrations > Reporting Webhooks to get started. This does not include custom events that were sent to Customer.io. You can also send events to destinations outside of Customer.io using our Pipelines API and most of our integration libraries. --- ## Import people or events via CSV URL: https://docs.customer.io/journeys/uploading-people/ You can import a CSV to add new people, update existing people, or both. How it works You can upload CSVs or Google Sheets to add people, update people, and send events to Customer.io outside your normal integration path. When you upload a CSV of people or events, each row in your CSV represents a person or an event respectively; each column in your CSV represents an attribute or event property respectively. When you go to upload a CSV, you’ll select People or Events. The option you choose determines the format of the CSV you upload. People: You can add new people and update existing people. Each column in the sheet represents an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you want to set on your audience. Events: You can upload events for existing people, but you cannot add new people when you upload events. Each column in your sheet represents an event property. To import relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., check out Add/update objects via CSV. Import a Google Sheet If you want to import Google Sheets, you must login to your Google account and allow us access to your sheets. You’ll see this includes the ability for us to read, edit, create, and delete the specific files that you share with us. However, we will only ever read files; we don’t write changes to your documents. After you grant access, you can select the individual sheets that you want to share with Customer.io. Import people from a CSV file Before you import people, make sure that your CSV file is ready to import. It must contain a column that maps to id or email. If you want to import a Google sheet, you must grant Customer.io access to sheets in your Google account. Go to People. Click Add People then scroll to the bottom of the modal and click CSV or Google Sheets. Select People. See import events from a CSV file if you want to upload events for people in your workspace. Select the file you want to import. If your CSV does not validate, it may not meet our CSV requirements. Set up your import and then click Next. Set a Name and Description for your import, helping you identify your CSV on the Imports page. Select whether add new people or not. Select whether to update existing people or not. If you update existing people, determine whether to update people by id, email, or cio_id. Use cio_id if you want to update people’s id or email values. Determine how to handle empty values—ignore them or nullify existing attribute values.  Looking for the email option? If you want to identify people by email but you don’t see that option, you can enable email as an identifier in your workspace settings. Map fields from your CSV to attributes in Customer.io and click Next. You must map id or email attributes to a column if your sheet did not include columns labeled id or email; all other columns are optional.  Creating new attributes If you import a column, but you don’t map it to an existing attribute, we create a new attribute using the column title. Review your import for errors and warnings. Check out our section about import for errors and warnings for help reviewing your CSV. (Optional) Click Preview Import to download a CSV file that reflects your final import, including all data mappings, skipped attributes, etc. Make sure that your import is correct. You cannot stop the import process after you click Import. (Optional) Add people to a new or existing manual segmentA segment of people you maintain manually. You must explicitly add people to, or remove people from, the segment..  Each row in your CSV can trigger a campaign Customer.io processes imports row-by-row. Segment-triggered campaigns may fire as we create new people or change attributes, so review your import carefully! Learn about when backfilled people data can trigger campaigns. Click Complete import to begin importing people. The import process takes approximately one minute per 20-30 thousand rows. You can navigate away from the page, and we will send you an email when your import is complete. Under Configure data, click More > Imports to revisit this import or see your previous imports. On the table, you’ll see how many rows were imported. Hover over the count to check how many people were updated or created. Map CSV headers to attributes When you import a CSV, you match the headers in your CSV to attributes for people in Customer.io, at least one of which represents the identifier for the people you want to update (ID or email). If your column names match attributes in Customer.io, we try to map them automatically. Otherwise, you can re-map columns in your attribute to existing attributes or create new attributes. We sample the first three rows of your data for each column, helping you understand the data that you’re mapping to each attribute. Use Import as: to select the attribute that you want to map a column to. If you do not want to import the values in a column, select Skip. Customer.io has some reserved attributes: id, email, created_at, and unsubscribed. These attributes have a defined purpose in Customer.io and expect certain types of values. You can create your own attributes for similar, non-reserved purposes. For example, if you want to import a column representing an email address, but you don’t want to map that to the reserved Customer.io email attribute, you can change the column name and import it as a different attribute. You cannot have multiple columns called email, nor can an email cell contain multiple values.  Mapping warnings You may see some warnings when mapping attributes. These generally represent best practices and recommendations, and are not issues that you need to fix. For example, we may recommend that you remove spaces from your column names because spaces in attributes can make things difficult when using Liquid in your messages. People CSV Requirements You can upload a CSV directly to Customer. io, or link us to a Google Sheet. Take a look at an example CSV. Each row in your CSV represents a person you want to add to, or update in, your workspace, and each column represents an attribute that you want to assign to that person. Your file must: be in CSV format OR a Google Sheet not exceed 100 MB in size not contain more than 100 columns contain at least one column that maps to an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. in your workspace—id, email, or cio_id (when updating people). This requirement changes based on the identifiers used in your workspace and whether you want to add or update people. See the section below for more information. To share Google Sheets, you must login to your google account and grant Customer.io access to your sheets. Identifiers and Required Columns in your CSV Your CSV generally needs to include at least one identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. However, the identifiers your sheet can include depend on whether you want to add or update people and your workspace settings—whether you identify people by id, email, or both. For Email and ID workspaces, your CSV must include at least one of id or email but may include both. When you update people: if a person does not already have an id or an email attribute, your CSV can update these values for that person. For example, if you add a person by email and then want to assign an id later, you can do that. If ID or email values are not empty, you cannot change them unless you identify people by cio_id. Attempting to change identifiers without using cio_id results in a Failed Attribute Change error. For ID only (classic) workspaces: When you add people: your sheet must include an id column (or a column that you map to id). When you update people: you can update people by email (without an id column). But, if multiple people in your workspace have the same email address, the row produces an error; in this case, we don’t know which person you want to update.  Use cio_id to update people’s identifiers If you want to update a person’s email or ID, you must identify them by cio_id—an identifier assigned by Customer.io. See the section below for more information. Workspace type Add Update Required in CSV Notes ID-only/Classic ✅ id ✅ id or email email is not a unique identifier. If you update by email, and multiple people have the same email address, the row produces an error. ✅ ✅ id email or ID ✅ at least one of id or email ✅ at least one of id or email Your request cannot change existing id or email values ✅ ✅ at least one of id or email Your request cannot change existing id or email values  There are other reserved attributes See our Attributes page for a description of attributes with specific uses in Customer.io. Updating people identifiers (email or ID) If you want to update a person’s email or ID, and those identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. are already set, your sheet must identify people by their cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. Otherwise, trying to change a person’s email or ID value (after it was already set) results in an Failed Attribute Change error. You can find people’s cio_id values by performing an export, or on the People page. When you import people, select the option to Update people, and use cio_id as the identifier. Identifying people by their canonical, CIO ID lets you change their other identifiers. You cannot add people when you identify people by cio_id; this value is reserved and assigned by Customer.io automatically when you add someone using an email or ID. Upload subscription preferences via CSV If you use our subscription center feature, you can set or backfill subscription preferences when you upload people. You can set some or all subscription topic preferences for people by importing a CSV in the People tab. Set one or more topic preferences for a person You can use this method to update any and all subscription preferences for people without overwriting preferences for topics not specified in the CSV. Add each subscription center topic name you want to set as its own column header. Upon upload, you’ll map each header to the attribute cio_subscription_preferences.topics.topic_<topic ID> where the topic ID corresponds to the topic name. You can find this on your subscription center landing page or by retrieving subscription center topics in our App API. After you complete the import, you’ll see that only the topic preferences you specified in the CSV show changes on the person’s profile. Before uploading, another option is to add a column header that already matches the JSON dot notation above. When you go to map fields, the correct attribute name automatically populates. Set all topic preferences for a person You can use this method to update ALL topic preferences per person, not a selection of topic preferences.  Include every topic and value per person If you do not include all topic preferences for each person using this method, the person’s preferences that are not specified in the import will be overwritten to match the default opt-in/out status of the topic. You can import subscription preferences for a person by adding a column header cio_subscription_preferences and including the same JSON structure that our API expects. The contents of this column are topics objects, as follows: email,first_name,cio_subscription_preferences person@example.com,person,{"topics":{"topic_1":true,"topic_2":false}} another.person@example.com,another,{"topics":{"topic_1":true,"topic_2":true}} See this spreadsheet for an example. Upload people via the API You can upload a CSV of people via our App API by providing the URL of your CSV file. CSVs that you import this way conform to the same rules as a CSV that you upload via the UI. The main difference is that we’ll automatically map columns to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., so you should make sure that your CSV’s column names match attribute names in your workspace before you upload a CSV. This means that your CSV must contain a column titled id or email—an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. we can use to add or update people. We recommend that you host your CSV from a short-lived URLs. Ideally, your URLs will expire 2 hours after you initiate imports. We have to be able to reach your files, but you probably don’t want files containing your customers’ information to remain publicly available after you’ve uploaded them to us. The response from a CSV upload contains a result.id. You can use this value with a companion endpoint to return the results of your import—including whether the import is complete and how many rows we were able to import. curl --request POST \ --url https://api.customer.io/v1/imports \ --header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \ --header 'content-type: application/json' \ --data-raw '{ "import": { "name":"my upload", "data_file_url":"https://www.example.com/myfile.csv", "type":"people", "identifier":"id", "data_to_process": "all", "description":"uploading people" } }' data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:people data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:event data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:relationship data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:object Import events from a CSV When you upload a list of people, you can upload events rather than attributes! You might do this if you need to backfill events that your audience performed outside of your normal integration path—things your audience did before you integrate with Customer.io. Before you import people, make sure that your CSV file is ready to import. The first column must be _cio_name and contain the event name; the second column must be _cio_customer_id. See Event CSV requirements for more information. Under Configure data, click More > Imports and click Import. Select Events. See import people from a CSV file if you want to add or update people. Select the file you want to import. If your CSV does not validate, it may not meet our CSV requirements. Set up your import and then click Next. Set a Name and Description for your import, helping you identify your CSV on the Imports page. Select the type of identifier in your CSV’s _cio_customer_id field: id or email. In the Preview step, check that event properties are mapped properly. Click any row in the preview to see the JSON representation of your event. Review your import. Check out our section about import for errors and warnings for help reviewing your CSV. (Optional) Click Preview Import to download a CSV file that reflects your final import, including all data mappings, skipped attributes, etc. Make sure that your import is correct. You cannot stop the import process after you click Import.  Events can trigger campaigns! If your sheet doesn’t contain a _cio_timestamp field, or your timestamps are within the past 72 hours, your events can trigger campaigns. Make sure that you understand the impact of your events before you finish your upload. Learn about when backfilled event data can trigger campaigns. Click Complete import to import your events. The import process takes approximately one minute per 20-30 thousand rows. You can navigate away from the page. We’ll send you an email when your import is complete. Under Configure data, click More > Imports to revisit this import or see your previous imports. On the table, you’ll see how many rows were imported. Hover over the count to check how many events were added and how many people were updated or created. Import events via the API You can upload a CSV of events via our App API by providing the URL of your CSV file. CSVs that you import this way conform to the same rules as a CSV that you upload via the UI. The main difference is that we’ll automatically map columns to event data properties, so you should make sure that your CSV column names match event properties that you might use in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other assets. Your CSV must contain a column titled _cio_customer_id—the id or email address of your audience, so we can identify who performed each event. You must use the same identifier for everybody in the sheet. We recommend that you host your CSV from a short-lived URLs. Ideally, your URLs will expire 2 hours after you initiate imports. We have to be able to reach your files, but you probably don’t want files containing your customers’ information to remain publicly available after you’ve uploaded them to us. The response from a CSV upload contains a result.id. You can use this value with a companion endpoint to return the results of your import—including whether the import is complete and how many rows we were able to import. curl --request POST \ --url https://api.customer.io/v1/imports \ --header 'Authorization: Bearer YOUR_APP_API_KEY' \ --header 'content-type: application/json' \ --data-raw \ '{ "import": { "name":"my event upload", "data_file_url":"https://www.example.com/events.csv", "type":"event", "identifier":"id", "description":"uploading people" } }' data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:people data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:event data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:relationship data_file_url string Required The URL or path to the CSV file you want to import. type string Required The type of import.Accepted values:object Event CSV requirements The first two columns in your CSV must be _cio_name, _cio_customer_id. Take a look at an example CSV. You can also include a third _cio_timestamp column representing the date-time when the event occurred if you want to back-date your event. If you don’t include this column, we’ll use time that we process the event as the event’s timestamp. Additional columns are event data attributes that you want to associate with the person. Event data provided via CSV (columns 4 and greater) is flattened and cannot contain nested JSON. For example, a column called property.subproperty will simply become a stringified key called "property.subproperty". col col name required description 1 _cio_name The name of the event. 2 _cio_customer_id The identifier for a person. Depending on your workspace settings, this can be the person's id or email. You must use the same identifier for the entire CSV—you cannot mix IDs and email addresses. 3 _cio_timestamp The date-time when the event occurred. 4+ Subsequent columns contain additional attributes for the data object in the event; one column per event property. These are properties you'll use in 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}}.—i.e. {{event.column_name}}. These columns can have any name, though you may want to make sure your column names don't contain spaces or special characters that might make them hard to use in Customer.io Review import errors and warnings On the final Review step, we validate your import and return errors and warnings for rows in your CSV file. Rows with errors will not be imported, but rows with warnings will. Depending on the size of your CSV file, it may take a moment for us to validate your import. If there are no errors or warnings, you can continue importing your file as normal. If there are issues, you may want to correct your CSV file so you import all items. Errors are issues that prevent us from importing a row: The row is missing a value in the “id” column. “object_id” is empty. The specified Person does not exist. The specified Object does not exist. Warnings are issues that do not prevent us from importing a row, but that you may want to address to make sure your data is well formed and consistent: Same “id” paired with multiple “email” values. Multiple rows have the same “email” value. Click Preview Import to see which rows would successfully import. You can also click Export errors file or Export warnings file to download a CSV containing the issues found. Each file contains the rows from your original CSV file that resulted in errors or warnings respectively, including 2 new columns: _row: contains the row number from your original file that contained an error or warning. _errors or _warnings: lists errors/warnings for a row.  Re-import your error CSV You can import users directly from an error or warning CSV file after you correct the errors. Make sure you remove the “_row”, “_errors” and/or “_warnings” columns. Export files from an import You can export files that you uploaded, or were otherwise generated as a part of the upload process (like errors and warnings), for up to 30 days after your import. After 30 days, these files expire and are no longer available to download. --- ## Searching and filtering people URL: https://docs.customer.io/journeys/filter-search/ You may want to isolate a specific person, or a set of people by attributes. There are a few different ways to do this from the **People** page. By email If you’d like to find a person by their email address, you can do so via the input box at the top of the page: If you remember what the user’s email starts with, enter it here and then click to search. Searching for tweety, for example, would return results like: tweety@acmecorp.com tweety.bird@acmecorp.com tweety@acme.com tweetyandsylvester@gmail.com  This will not return email addresses that contain your search term somewhere in the middle. So the above search would not return the email address bird.tweety@acmecorp.com …or domain You can also search for all people whose email ends with a specific domain, for example, “acmecorp.com” by searching for @acmecorp.com which would return results like: tweety@acmecorp.com tweety.bird@acmecorp.com bugs.bunny@acmecorp.com theroadrunner@acmecorp.com By conditions If you’d like to view or export a list of people who match specific conditions, click conditions: Then, add your conditions as needed: Segment condition: whether users are in or not in particular segments Attribute condition: whether or not an attribute exists or is equal to a particular value Newsletter condition: whether or not a user has received a particular newsletter Message condition: whether specific types of messages have or have not been delivered, opened, etc Event condition: whether a specific event has or has not been performed If you’d like to export your results you can click the Export to CSV button to receive a CSV of your search results. --- ## Filter Activity Logs URL: https://docs.customer.io/journeys/filtering-logs/ Your workspace **Activity Log** tracks changes to your people and objects. You might use this to identify who recently performed a specific event or viewed a specific page and troubleshoot issues. In the Activity Log, you can track which activities people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. have performed. This can help you ensure you’re receiving certain events, troubleshoot people across a similar activity, and more.  The Activity Log shows activities processed within the last 30 days. This article is about your workspace-wide activity logs. You can also find activity logs for each of your customers within their profile page. To track changes made by your teammates, go to your Audit Logs. Filter activities To find specific activities: Decide whether you want to filter by Identified or Anonymous users. Filter by Activity Type. Some examples include Failed Event, Person Deleted, Bounced Email, or Skipped Update. Decide a date range. We don’t show activities older than 30 days. Click to view the payload of the activity. Or click the person or object to go to the profile page for more info. Timestamp vs Processed At The Activity Log has two types of dates: Timestamp and Processed At. Timestamp represents the date and time listed on an event. If you don’t set a timestamp, we use the date-time when we receive the event. Processed at is the date and time when we process an event. Anonymous events are not processed, and therefore do not have a “processed at” time. If there is a significant difference between the two, it could be for one of these reasons: An anonymous event was merged to a profile. Anonymous events are not processed until they’re associated with a person, so an anonymous event may be timestamped well before you identify a person and the event is associated with the person. You may have manually set a timestamp on an event. This typically happens when you backdate an event, or want to log the exact date-time that an event occurred and you don’t immediately send the event to Customer.io. Customer.io experienced a processing delay. --- ## Using your Data Index URL: https://docs.customer.io/journeys/using-data-index/ The **Data Index** provides information about the events and attributes stored on people and objects in your workspace. It can help you understand what data you're using and where you use it—segments, campaigns, and more. Why use the Data Index The Data Index is useful for things like: Troubleshooting: Are your campaigns behaving strangely? Check the Data Index to see if a particular event is being sent multiple times or if it’s being used by multiple campaigns. Data clean-up: Data gets messy sometimes. Check if your attributes or events are similarly named or near-duplicates and clean your data accordingly. Visibility and clarity: Determine if a piece of data you need for a campaign is already being sent to Customer.io or if you need to start sending it to us. Add tags to your data to help you organize and keep track of related data. Marking attributes as sensitive: On certain plans, account admins and workspace admins can locate and mark attributes as sensitive in the Data Index to ensure data privacy across team members. How to use the Data Index On the left-hand navigation, go to Data Index. All roles can view the index, but only account and workspace admins can edit descriptions or export data. At the top, you can switch between Attributes and Events. Attributes are data stored on your customers or objects. Customers are referred to as “people” in Customer.io, and each person has a profile where we store attributes. Objects are data that help you relate people to entities like accounts they belong to or courses they’re enrolled in. You can switch between people’s profile data and objects data with the dropdown filter. Profile Attributes show any attribute stored on people’s profiles. Custom Object Attributes show attributes saved across any object in an object typeAn object type is a group of objects. An object type could be Online Classes while an object within the type could be English 101. Customer.io generates a unique, immutable object_type_id. (what you see in the left-hand navigation of your workspace), like Online Classes Attributes. Custom Object Relationship Attributes show attributes saved on relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between people and any object in an object type, like Online Classes Relationship Attributes. Events are actions your people perform in your mobile app, website, or wherever you track them. You can filter your data further by Usage. Click Attributes in use to filter out unused data. An attribute is in use if it’s used in a campaign, newsletter, segment, email template and/or snippet. Tag your data In your Data Index, you can manage tags for your attributes and events. Tags you create here are available across all automations as well as segments. You can also assign tags from the Data Index table or from individual attribute pages. Our AI-powered segment builder takes these tags into account to generate more relevant segment conditions too! Attribute list By default, the Attributes tab lists all the peopleAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. attributes in your workspace in alphabetical order. Use the dropdown menu to switch to objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. or relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. attributes. Use the search bar to find a specific attribute in the list. For event data, switch to the Events tab. View attribute usage and details Click an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to see where it’s used and recent activity: Usage reflects the campaigns, segments, newsletters, templates, snippets, and layouts that use the attribute. Templates are emails that reference the attribute. If a campaign uses the attribute, it could mean it’s used in a filter, trigger condition, action condition, goal, or other conditions. Click a campaign name to see details.  Usage does not track object or relationship attributes referenced by liquid While we track when liquid references profile attributes and events, we don’t track this for objects or relationships currently. This means templates, snippets, and layouts always show as unused for these attributes. Activity reflects the five most recent attribute changes. You can also perform a few actions from this page: Edit the name, description, or tags. Mark people’s attributes as sensitive to ensure data privacy across team members Use the attribute in a new segment, campaign, etc. Click Use in to see your options. View sources to see which integrations or actions set the attribute. Filter for people with this attribute. Describe an attribute You can add descriptions to attributes in the Data Index so all of your teammates are on the same page. Our AI-powered segment builder takes these descriptions into account to generate more relevant segment conditions too! In the Data Index, click an attribute then click Edit next to Description to get started. You can click in the description field to generate a description with AI. Review AI-generated descriptions for accuracy before you save them. Mark attributes as sensitive Premium This feature is available for Premium plans. Account admins and workspace admins can mark profile attributes as sensitive in the Data Index. This redacts values but not attributes names from the workspace and helps ensure data privacy across team members. If you have a custom role that includes the Edit permission for the Data Index, you can also mark attributes as sensitive. You can also mark event attributes as sensitive independently. In the Events tab, select an event to find its attributes and mark them as sensitive. Profile and event attributes are separate—marking a profile attribute as sensitive doesn’t automatically redact event attributes with the same name. In the Attributes tab, click an attribute. Click Edit in the panel. Click “Make sensitive.” To unhide sensitive attributes, select the box to uncheck it.  Not seeing Make sensitive? Check that you’re an Account Admin or Workspace Admin in Team Members. If you are, then check whether you’re on a Premium or Enterprise plan or reach out to someone with billing access. Otherwise, you’ll have to upgrade for access. Click Save. Next, assign “Hide sensitive attributes” to team members. How you hide sensitive data depends on the type of workspace-level role you’re assigning to team members. For standard roles, you’ll choose “Hide sensitive attributes” when assigning the role of Author or Viewer. For custom roles, you’ll choose “Hide sensitive attributes” when creating the role. Export attributes On the Attributes tab, click Export to CSV in the top right and choose the type of export you want. You can export information for people, object, or relationship attributes. Each attribute type has two export options. Profile attributes The Attributes export is an overview of where you use profile attributes: Name: the attribute name Campaigns: the number of campaigns where it’s used as a filter, trigger, action condition, goal, and more Segments: the number of segments it’s used in Newsletters: the number of newsletters it’s used in as a recipient filter People: the number of people in your workspace who have the attribute Last Updated: the timestamp of when the attribute was last modified across your people  The Last Updated date might show January 1, 1970 We store activities like attribute updates for 30 days. If you haven’t updated an attribute in 30 days, the Last Updated timestamp changes to zero (0) which is equal to January 1, 1970. The Attribute Usage export is a breakdown of where you use profile attributes: Name: the attribute name Used In: indicates “Segment,” “Campaign,” or “Newsletter” Usage Name: the name of the segment, campaign, or newsletter that the attribute is used in Link: a link to the segment, campaign, or newsletter that the attribute is used in Custom object attributes The Custom Object Attributes export is an overview of how many objects use each attribute in the object type exported: Name: the attribute name Object Type: the name of the object type exported Objects Count: the number of objects that have the attribute The Custom Object Attribute Usage export is a breakdown of where each object attribute is used: Name: the attribute name Object Type: the name of the object type exported Used In: indicates “Segment,” “Campaign,” or “Newsletter” Usage Name: the name of the segment, campaign, or newsletter that the attribute is used in Link: a link to the segment, campaign, or newsletter that the attribute is used in Custom object relationship attributes The Custom Object Relationship Attributes export is an overview of how many relationships use each attribute in the object type exported: Name: the attribute name Object Type: the name of the object type exported Relationships Count: the number of relationships that have the attribute The Custom Object Relationship Attribute Usage export is a breakdown of where each relationship attribute is used: Name: the attribute name Object Type: the name of the object type exported Used In: indicates “Segment,” “Campaign,” or “Newsletter” Usage Name: the name of the segment, campaign, or newsletter that the attribute is used in Link: a link to the segment, campaign, or newsletter that the attribute is used in Event List The Events tab lists the events that you’ve sent to Customer.io in alphabetical order. Use the search bar to find a specific event in the list. View event usage and details Click an event to see where it’s used and recent activity: Usage reflects the campaigns, segments, newsletters, templates, snippets, and layouts that use the event. Templates are emails that reference the attribute. If a campaign uses the event, it could mean it’s used in a filter, trigger condition, action condition, goal, or other conditions. Click a campaign name to see details. Activity reflects the five most recent attribute changes. You can also create a segment, campaign, etc with the event. Click Use in to see your options. Describe an event You can add descriptions to events in the Data Index so all of your teammates are on the same page. Our AI-powered segment builder takes these descriptions into account to generate more relevant segment conditions too! In the Data Index, click the Events tab, then click an event name. Click Edit next to Description to get started. You can click in the description field to generate a description with AI. Review AI-generated descriptions for accuracy before you save them. Export events You can export Events or Events Usage from the Data Index. On the Events tab, click Export to CSV in the top right and choose the type of export you want. The Events export includes: Name: the name of the event Campaigns: the number of campaigns where it’s used as a filter, trigger condition, action condition, goal, and more Segments: the number of segments it’s used in People: the number of people who have performed the event at least once Last Received: the timestamp of when Customer.io last logged the event Last Data: the data sent with the last received event  The Last Received date might show January 1, 1970 We store activity data for 30 days. If you haven’t received an event in 30 days, the Last Received timestamp changes to zero (0) which is equal to January 1, 1970. The Events Usage export includes: Name: the event name Used In: whether the event is used in a segment, campaign, or newsletter Usage Name: the name of the segment, campaign, or newsletter that the event is used in Link: a link to the segment, campaign, or newsletter that the event is used in Edit attributes details in bulk From the Data Index, you can modify multiple attributes or events at once. You can filter or search for the data you want to edit, then check the box next to each item and you’ll see an Actions dropdown. In the Actions dropdown, you have three options: Edit: Edit the descriptions or tags of multiple attributes or events at once Generate descriptions: Generate descriptions for multiple attributes or events using AI Delete: Delete the attributes from your index. This is only possible for attributes not used in segments, campaigns, or elsewhere in your workspace. Learn more about deleting attributes from your workspace.  Generating descriptions with AI overwrites existing descriptions After you confirm you want to generate descriptions, we overwrite any existing descriptions. While you can edit them afterwards, you can’t revert the change to a previous description. View the sources of your data You can see where your attributes and events come from in your workspace’s Data Index under Sources. We started tracking sources on August 25, 2025; you can’t view source data from before then. In the index, you can preview and filter by sources. Hover over the source to view the name of the integration or action that changed the data. Click the source to view the integration in your workspace. Click Filters to select one or more sources to filter your data index. If the source is empty for an attribute or event, the data was last added or changed before August 25, 2025. Source options This is a list of some of our sources. If you have questions about others, send us feedback by clicking the thumbs down icon and typing your request! Source Description Attribute update action A “Create or update person” action in a campaign or API-triggered broadcast Action Any action that can update attributes in a campaign or API-triggered broadcast, like “Send and receive data” or “Batch update”. This excludes “Create or update person” actions (see above). Customer.io Journeys API OR Integration: Customer.io API Integrations that uses our Pipelines API Customer.io Track API Integrations that uses our Track API List unsubscribe A customer unsubscribed through your workspace’s subscription page. Updated in dashboard A team member manually updated the attribute in your workspace. --- ## How do I know what data I have available? URL: https://docs.customer.io/journeys/data-availability/ If you're not sure what data (events and attributes specifically) you're sending to Customer.io or how your data is used, you can find that information in the **Data Index**. You'll find the Data Index in the left-hand menu, just below *Activity Logs*. Here, you’ll find all of the attributes and events flowing into your Customer.io account via your chosen integration method. You can see: All of the events and attributes you have Where they’re being used (segments, campaigns) How they’re being used (as a trigger, filter, or Action Condition) For events, the data associated with each and when it was last seen For attributes, we list all of the attributes you have for your users, and when each was last changed or added Head over to the Data Index introduction for more details on how to navigate; otherwise, feel free to explore! --- ## Anonymous people URL: https://docs.customer.io/journeys/anonymous-people/ When people first visit your website or use your app without logging in, they're anonymous. You know they exist and can see their activity, but you don't know who they are. You want to identify these people and convert them into members of your audience, users of your service, or customers. How it works Most of the things you do in Customer.io revolve around people you know: they log into your service, make purchases, sign up for your newsletters, and so on. And, because you know who they are—you have their email addresses, names, and other information—you can send them messages. But what about people you don’t know? Plenty of people visit your website or open your app without logging in or otherwise providing identifying information. In Customer.io, we typically call these Anonymous People. Sometimes we refer to them as Leads or Prospects: everybody who visits your website or app is someone who could potentially become a customer! In Customer.io, you can track and see anonymized activity, like the pages people visit, the actions they take, and so on. But you’re limited in how you communicate with them because you don’t have their email address, phone number, and so on. When you identify these people—by having them sign up for your service, make a purchase, or otherwise provide you with their email address—we’ll associate their anonymous activity with their new profile in Customer.io, and you can use their previously anonymous activity to add them to segments, send targeted messages, and personalize their experiences. flowchart LR a(person visits website) a-->b{Are they alreadyidentified?} b-....->|yes|c(Activityisn't anonymous) b-->|no|d(Activity is anonymous) d-->e subgraph e[Anonymous activity] direction LR f(Person visits page) g(Person adds item to cart) h(Person starts video) end e-->i{Is person identified within 30 days?} i-->|yes, anonymous activity associated with profile|c i-.->|no|j(Anonymous activity is deleted) Some integrations handle anonymous data automatically Our JavaScript libraries and some of our mobile SDKs automatically assign people an anonymous ID and track page or screen views. If you use these libraries, you get some anonymous data for free, without doing anything special. Our server-side libraries and APIs don’t automatically track anonymous data. If you want to track anonymous data with these libraries, you’ll need to assign an anonymous ID to people and send events manually. Some of our mobile SDKs don’t support anonymous data yet. We’re adding support in future versions, but you won’t be able to track anonymous screenviews with our Android, React, or Flutter SDKs in the meantime. Platform SDK Supports anonymous data anonymous ID anonymous page/screen tracking Web JavaScript Client ✅ Auto Auto Web Classic JavaScript (Track API) ✅ Auto Auto Mobile iOS SDK v3 and later ✅ Auto Auto Mobile iOS SDK v2 and earlier ❌ N/A N/A Mobile Android SDK v4 and later ✅ Auto Auto Mobile Android SDK v3 and earlier ❌ N/A N/A Mobile React Native SDK ❌ ❌ ❌ Mobile Flutter SDK ❌ N/A N/A Server Node.js ✅ Manual Manual Server Python ✅ Manual Manual Server Go ✅ Manual Manual Your goal is to identify anonymous people You can’t do much with an anonymous person. But if you can identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them—if you can learn their name, their email address, and so on—you can send personalized messages, improve their experience in your app, and more. Identifying people is the first step in building a relationship with them. You can identify people when they provide you their email address through a web form, create an account with you, or log in to your service. There are patterns that encourage people to identify themselves (or become customers) on almost every website or app: offering someone a coupon on their first purchase encouraging people to sign up for a newsletter offering a free trial of your service These are all ways to identify new people and start nurturing them to become customers, users, or members of your audience! Send in-app messages to anonymous people Premium This feature is available for Premium plans. You can send in-app messages to anonymous people who use your website or mobile app if you use our JavaScript client or mobile SDKs. These messages can encourage people to sign up for your service, make a purchase, or otherwise identify themselves. When you create an anonymous message, you’ll set criteria determining where your message appears. People will see your message if: They haven’t been identifiedThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. They’re on a page or screen that matches your Page Rules They haven’t already seen your message too many times—determined by the Frequency setting in your in-app messages Anonymous people don’t generate profiles Anonymous data in Customer.io isn’t associated with a profile. Anonymous people don’t cost you anything. But that’s also because the data they generate is limited and not very useful until you identify them. What do I do with anonymous data? Anonymous data isn’t very useful until you identify a person, but you can still: Use anonymous page views to trigger in-app messages. These messages aren’t personalized, but they might do things like encourage people to sign up for your service or make a purchase. Use anonymous responses to in-app messages to figure out conversion rates and other metrics for your anonymous in-app messages. Send anonymous events to downstream destinations, like your analytics platforms or data warehouses to track traffic and other metrics. Anonymous data really shines after you identify people who were previously anonymous. When you identify people, we’ll associate their anonymous activity their profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io—their anonymous data stops being anonymous. This lets you respond to the things people did before you identified them. You can use this data to trigger campaigns, add people to segments, and personalize their experiences. --- ## Anonymous activity URL: https://docs.customer.io/journeys/anonymous-activity/ People you don't know visit your website and use your app. By tracking the things anonymous people do, you can better understand people's behavior before they sign up or login. When you identify anonymous people, we'll associate their anonymous activity with the identified person, so you have a complete history of their activity—triggering campaigns, adding people to segments, and so on. How it works When a person visits your site or app, they get an anonymous_id. You can track events against this ID—the pages they visit, things they click, and so on. For the most part, this activity isn’t very useful by itself. But when you identify people, we’ll merge people’s anonymous activity with their profiles, so you can respond to the things people did before you identified them! That’s where anonymous activity really shines. You can use anonymous activity to: Trigger anonymous in-app messages encouraging people to sign up, make a purchase, log in, etc Personalize and trigger messages after you identify people—like onboarding campaigns based on things people did before they signed up for your service Keep metrics on things like page and screen views via destinationsAn integration that sends data out of Customer.io—your data’s ultimate destination. like Mixpanel, Amplitude, etc  Our JavaScript library sends some anonymous events automatically If you use our client-side JavaScript, we automatically assign users an anonymous_id and capture pageviews. Anonymous pageviews The page and screen methods represent pages that people view on your website or in your app. Page and screen events aren’t very specific; they simply track traffic, but they can be useful for understanding how people move through your site or app. You can send these events to analytics platforms like Mixpanel, Amplitude, and so on to measure traffic. When you identify people, you might use their anonymous pageviews to invite them to buy a product or sign up for a class based on the pages they viewed. Anonymous pageviews can also trigger in-app messages! You can use page rules to show anonymous visitors in-app messages when they visit specific pages in your app. JavaScript client SDKs: we’ll automatically capture page events for each page the visitor views Mobile SDKs: you can automatically capture screen events—like pageviews, but for screens in your mobile app Server-side libraries (Node.JS, Python, and Go): you’ll need to capture page events yourself. Anonymous events Where page events are specialized for pageviews specifically, you can also track custom events. These events can be whatever you want. Where a page event might tell you when a person visits a product page, you could also track when a person adds that product to their cart—along with the price of the item, the quantity, department, and so on. Unlike page and screen views, custom events aren’t captured automatically, and are less meaningful until you identify a person. But, because custom events typically carry more specific information about things people do, these events can help you trigger much more powerful and personalized campaigns when you eventually identify a person. Anonymous events in the Activity log The activity log shows Identified and Anonymous events. Identified events are events associated with a person in your workspace. Anonymous events are associated with an unknown (or anonymous) person. While we capture anonymous events, these events don’t do anything until you associate (or merge) them with a person. When events are merged to a person’s profile, they become “identified” and appear in the Identified tab. Events with a icon next to the person, were previously anonymous events that are now associated with a person. We’ll also report an Anonymous merge activity in the log when we merge anonymous events with a person. --- ## Merging anonymous activity URL: https://docs.customer.io/journeys/merge-anonymous/ When a person visits your website or uses your app, you can track their activity with an `anonymous_id`. Later, when you identify them, we'll automatically merge their anonymous activity with their profile. How it works When you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. a person, Customer.io will associate anonymous events with the person. You can use a person’s previously anonymous activity to add them to segments, trigger campaigns, and personalize their experiences. If you use our JavaScript SDK, or any of our mobile SDKs that support anonymous activity, we’ll do this automatically! If you use our server-side libraries or our APIs, you’ll have to set a person’s anonymous_id and send events manually. When you identify a person, you’ll need to pass their anonymous_id so we know which anonymous events to associate with the person you identified. flowchart LR a(Person visits website) a-->b{Are they alreadyidentified?} b-....->|Yes|c(Activityisn't anonymous) b-->|No|d(Activity is anonymous) d-->e subgraph e[Anonymous activity] direction LR f(Person visits page) g(Person adds item to cart) h(Person starts video) end e-->i{Is person identified within 30 days?} i-->|Yes, anonymous activity associated with profile|c i-.->|No|j(Anonymous activity is deleted) Change anonymous event merging settings Anonymous event merging is enabled by default, and we recommend that you leave it enabled. If you disable anonymous event merging, you won’t be able to associate anonymous events with people you identify; anonymous events will remain anonymous and they won’t be very useful to you. If you created your workspace before July 2021, you must enable Anonymous event merging to associate anonymous activity with people you identify. Go to Settings > Workspace Settings and click Merge Options. Toggle the Anonymous event merge setting. Using anonymous events as campaign triggers Customer.io retains anonymous events for 30 days. These events don’t do anything while they’re anonymous—they cannot trigger campaigns or segment membership—until they are associated with a person. They appear in the activity log in the Anonymous tab, but they’re otherwise unusable until you associate them with a person by anonymous_id. When an anonymous event is associated with a person, it can trigger a campaign if it occurred within the past 72 hours. While an older event can’t trigger a campaign, you can still use it to add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., which can also trigger campaigns. flowchart LR a(identify a person) a-->c{Did anonymous event happen within the past 72 hours?} c-->|yes|d(Event triggers campaign) c-.->|no|e(Event does not trigger campaign) Anonymous event merge in the activity log The Activity Log reports an Anonymous Data Merged activity for a person when we merge anonymous events to an identified person’s profileAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}.. This activity records the number of anonymous events merged with a profile, the unix timestamp for the oldest event, and the anonymous ID for the events. Merging anonymous events with JavaScript SDKs Using either of our JavaScript SDKs, you can send anonymous events with the track method. We’ll automatically associate these events with people when you identify them (as long as you haven’t disabled anonymous event merging). So, for example, if a person adds items to their cart before they log in, the snippet automatically associates that event with a person when they log in. JavaScript client (Recommended) JavaScript client (Recommended) // Send an anonymous event cioanalytics.track('addedToCart', { product: "shoes", price: 39.95, qty: 1, size: 9 }); // Identify the person // The addedToCart event is now associated with this person cioanalytics.identify('coolperson1234', { email: 'cool.person@example.com', name: 'Cool Person', email: 'cool.person@example.com', totalSpent: 0 }); Journeys JavaScript Snippet Journeys JavaScript Snippet // Send an anonymous event _cio.track("addedToCart", { product: "shoes", price: 39.95, qty: 1, size: 9 }); // Identify the person // The addedToCart event is now associated with this person _cio.identify({ id: 'coolperson1234', email: 'cool.person@example.com', name: 'Cool Person', email: 'cool.person@example.com', totalSpent: 0 }); Server-side API implementation If you send events to us using the API, you need to set an anonymous ID manually—both in your anonymous track events and in your identify calls. Anonymous event IDs must be unique and are not reusable. flowchart LR A(Unidentified person)-->|performs activity| B[/Event with anonymous ID/] B-->|person logs in| C[/identify call w/anonymous ID/] D-->|no|G(Events belong to someone else) C-->D{Does anonymous_id match?}-->|yes|F(Events merged w/ Identified Person) F-->H[/Events use ID or email/] F-->|Person logs out| A The example event below has an event with an anonymous ID. The corresponding identify request contains the same anonymous ID as the event, so we’ll associate the even with the person identified by the email address person@example.com. We’ll use our Node.js server-side libraries for this example, but the same principles apply for any of our server-side SDKs. Node.js (Recommended) Node.js (Recommended) // Send a track event with an anonymous ID cioanalytics.track({ anonymousId: '019mr8mf4r', event: 'addedToCart', properties: { product: "shoes", price: 39.95, qty: 1, size: 9 } }); // Send an identify call with the same anonymous ID cioanalytics.identify({ userId: 'coolperson1234', traits: { name: 'Cool Person', email: 'cool.person@example.com', totalSpent: 0 } }); Journeys Node.JS Journeys Node.JS // Send a track event with an anonymous ID cio.track('019mr8mf4r', { name: "addedToCart", data: { product: "shoes", price: 39.95, qty: 1, size: 9 }, }); // Send an identify call with the same anonymous ID cio.identify('coolperson1234', { anonymous_id: '019mr8mf4r', email: 'cool.person@example.com', name: 'Cool Person', email: 'cool.person@example.com', totalSpent: 0 }); Sending anonymous events after identifying someone After you identify someone, you can send events with the anonymous ID for up to 5 minutes, and we’ll associate them with the person you identified. You cannot reuse an anonymous ID after you have assigned it to a person. If you send an event with an anonymous ID that you already assigned to a person more than five minutes ago, the event will not be associated with the identified person; it will remain anonymous. --- ## Anonymous events URL: https://docs.customer.io/journeys/anonymous-events/ Start logging events before people sign up or log into your site, app, or service. When people sign up or log in, you can associate those events with people. Anonymous events help you do things like personalize your onboarding campaigns based on things people did before they signed up. For example, if a customer viewed a pricing plan before signing up for a trial, your onboarding campaign might cover features relevant to the plan they viewed. An anonymous event is an event associated with a person you don’t recognize. The event bears an anonymous_id—a value representing the unknown person, like a cookie. We store these events for up to 30 days. The Anonymous event merge feature lets you associate these events with a person after you identify them. You can enable anonymous event merging in your workspace settings. If you use the JavaScript snippet (client-side): we automatically log anonymous events and associate them with people you identify. If you use the API (server-side): When you log anonymous events, you’ll set an anonymous_id on the event. When you identify people, you can set an anonymous_id attribute, and we’ll associate any events with the matching anonymous_id that we received up to five minutes after your identify call. If you identify a person then try to merge anonymous events more than five minutes after the identify call, the anonymous events will not merge into the profile. If you want to merge anonymous events more than 5 minutes after the initial identify call, you must resend the events by updating the anonymous id of the event then sending another identify call where you update the anonymous id of the identified profile to the new anonymous event id. Turn on anonymous event merging  When you create a new workspace, anonymous event merging is on by default If you created your workspace before July 2021, you must enable Anonymous event merging to associate anonymous events with people you identify. Anonymous event merging is the act of associating anonymous events—events with an anonymous_id—with a person. When you assign an anonymous_id attribute to a person, any anonymous events with that same ID are associated with the person. Go to Settings > Workspace Settings and click Merge Options. Turn on the Anonymous event merge setting. Set up an integration that sends events to Customer.io. While we have plenty of integrations, our JavaScript snippet makes this easy! Now, any events with an anonymous_id are merged with people bearing the same anonymous_id attribute. JavaScript implementation (client-side) You’ll use the track method to send anonymous events using our JavaScript snippet (recommended) or our classic JavaScript SDK. If you’ve enabled Anonymous event merging, the JavaScript snippet automatically associates events (that occurred in the past 30 days) with people when you identify them. So, for example, if a person adds items to their cart before they log in, we’ll automatically associate relevant anonymous addedToCart events with a person when they log in. JavaScript Source (Recommended) JavaScript Source (Recommended) // track an anonymous event cioanalytics.track("addedToCart", { product: "cool-shoes" }); // identify the person // if "anonymous event merging" is enabled, we'll associate the // event with the person cioanalytics.identify('userId', { // additional attributes go here first_name: 'cool', last_name: 'person', shipping_address: { city: 'San Francisco', state: 'CA', country: 'USA' } }); Classic JavaScript SDK Classic JavaScript SDK // track an anonymous event _cio.track("addedToCart", { product: "cool-shoes" }); // identify the person // if "anonymous event merging" is enabled, we'll associate the // event with the person _cio.identify({ id: 'YOUR_USER_ID_HERE', }); Server-side API implementation If you send anonymous events to us using the API or our server-side integrations, you’ll set a person’s anonymous ID and send that ID whenever a person performs an event. When you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. a person, you can pass a person’s anonymous ID attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. This tells us to associate events with the matching anonymous ID with the person you identify. This covers the anonymous events you sent before you identified the person and up to five minutes after that. After you identify a person, you should send events using their known identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. until they log out (or otherwise become unidentifiable), and not the anonymous ID that you previously used. graph LR A[Unidentified person]-->|performs activity| B[/Event with anonymous ID/] B-->|person logs in| C[/identify call w/anonymous ID/] D-->G[No] C-->D{Does anonymous_id match?}-->E[Yes] E-->F[Events merged w/ Identified Person] F-->H[/Events use ID or email/] F-->|Person logs out| A You cannot reuse an anonymous ID after you have assigned it to a person. If you send an event with an anonymous ID that you already assigned to a person more than five minutes ago, the event will not be associated with the identified person; it will remain anonymous. If you send events with a new anonymous ID for a person you identified more than five minutes ago, you’ll need to send a new identify request with the new anonymous ID to associate those events with a person. The example event below has an anonymousId of abc123. Anonymous event IDs must be unique and are not reusable. The corresponding identify request will associate events with the anonymousId of abc123 with the person identified by the email address person@example.com. Here’s an example using our Node.JS integration: // track the anonymous event cioanalytics.track({ anonymousId: '48d213bb-95c3-4f8d-af97-86b2b404dcfe', event: 'added_to_cart', properties: { product: "shoes", revenue: 39.95, qty: 1, size: 9 } }); // identify the person // The anonymousId matches so the anonymous event // is associated with the identified person cioanalytics.identify({ userId: '019mr8mf4r', anonymousId: '48d213bb-95c3-4f8d-af97-86b2b404dcfe', traits: { first_name: 'Cool', last_name: 'Person', email: 'cool.person@example.com', purchases: 0 } }); Anonymous event retention and campaign triggers Customer.io retains anonymous events for 30 days. These events don’t do anything—they cannot trigger campaigns or segment membership—until they are associated with a person. They appear in the activity log in the Anonymous tab, but they’re otherwise unusable until you associate them with a person by anonymous_id. When an anonymous event is associated with a person, it can trigger a campaign if it occurred within the past 72 hours. Anonymous event merge in the activity log The Activity Log reports an Anonymous Data Merged activity for a person when anonymous events are merged to an identified person’s profileAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}.. This activity records the number of anonymous events merged with a profile, the unix timestamp for the oldest event, and the anonymous ID for the events. Third-party support for anonymous events The following third-parties support anonymous events: Segment Destination Actions Segment Destination (Device mode only) To request support for anonymous events from other third-party platforms, email the third party directly and CC product@customer.io. Anonymous events in the Activity log The activity log shows Identified and Anonymous events. Identified events are events associated with a person in your workspace. Anonymous events are associated with an unknown person. While we capture anonymous events, these events don’t do anything until you associate (or merge) them with a person. When events are merged to a person’s profile, they become “identified” and appear in the Identified tab. Events with a icon next to the person, were previously anonymous events that are now associated with a person. --- ## How do I create multiple subscription types? URL: https://docs.customer.io/journeys/multiple-subscription-types/ Before the subscription center existed, we recommended the following options to those who wanted to track preferences at a more granular level than our global unsubscribe and to segment people based on these preferences. We recommend using our [subscription center](/journeys/subscription-center/) moving forward.  Customer.io now has a subscription center! Our subscription center lets you manage multiple subscription topics and set subscription-based audiences for campaigns and broadcasts. If, however, the options in this article better serve your use case, please let us know so we can continue to improve our product. Here are three tracking subscription preferences: Use our subscription center! Custom unsubscribe links: This method lets you segment based on people who have clicked a link that represents unsubscribing. Form-based, custom subscription center: This method lets you map a custom form to profile attributes to capture your customers’ subscription preferences. Custom unsubscribe links Let’s say you have multiple webinars that you run. You want to send updates to people about a particular webinar but also give them a way to opt out. This method works great for discrete events that you don’t really want to keep around forever as a preference on people’s profiles.  Custom unsubscribe links must adhere to new Google and Yahoo standards If you use custom unsubscribe links, you’ll also need to do a bit of development work to support the new RFC 8058 before June 1, 2024. See custom unsubscribe links (RFC 8058) for more information. 1. Create landing pages Set up different links/landing pages, three in this example, and make sure link tracking is enabled. The content on the pages can be anything you want, but make sure the person landing there knows they’ve been unsubscribed. e.g. example.com/unsub/webinarupdates#02-20-2017 example.com/unsub/webinarupdates#02-27-2017 example.com/unsub/webinarupdates#03-05-2017 Note: This custom implementation does not give your customers a way to resubscribe to this information. 2. Change the unsubscribe link in the layout In your layout, replace Customer.io’s global unsubscribe link {% unsubscribe_url %} with the link to one of your landing pages. In our webinar example, you would either need to have a different layout for each webinar preference or move the link into the body of your message. 3. Create data-driven segments to avoid sending to unsubscribed people Give your new segment the following rule: “Email [any email] has ever been clicked on tracked link matching [your page URL]”, like this: 4. Target these segments to message the right people You can then use these segments to ensure you don’t send messages to people who are unsubscribed. You can also use these segments to trigger campaigns to update people’s attributes using the Create or update person action. Creating your own subscription center For this method, you will need to build a subscription center form and use Customer.io’s forms integration to connect your custom form to your workspace. Overall, you’ll need to: Create your form with the various subscription options you need. Map your form to their respective profile attributes in your workspace. When you set up your form in Customer.io, you will map your form’s subscription options to the profile attributes that you want to use to capture users’ subscription preferences.  Always include a global unsubscribe option Be sure to include a global unsubscribe option on your form and map it to the reserved unsubscribed attribute. If a person wants to unsubscribe from all of your messages, your form will set this attribute to true, making sure that you respect their unsubscribe request. When someone submits your form, their subscription attributes will update according to the mapping you set. Then you can set up segments to include or exclude people who have various subscription attributes for future messages. Report unsubscribe metrics for custom subscription centers When you use our default unsubscribe link and page, we attribute unsubscribe requests to a particular message, helping you understand how your audience receives your messages. If you use a custom subscription center, you need to attribute unsubscribes to a particular message if you want to capture unsubscribe metrics for your messages. You can do this using our custom unsubscribe API. This endpoint attributes each unsubscription to a specific message and sets the person’s unsubscribed attribute to true. If you use a custom subscription center and manage subscriptions with other attributes, the unsubscribed attribute may not represent your audience’s subscription preferences. The unsubscribed attribute represents a global unsubscription in Customer.io. A person whose unsubscribed attribute is equal to true won’t receive any of your messages unless you specify this in settings for campaigns and broadcast and settings for your messages in your workflows. So, if you use a custom attribute to represent your audience’s subscription preferences, and you report unsubscription metrics using the unsubscribe reporting API, you may need to set your campaigns and broadcasts to All subscribed and unsubscribed so that your audience continues receiving messages in accordance with their custom subscription preferences. Your audience can still unsubscribe if you use a custom subscription center and do not report unsubscribe metrics back to Customer.io. You just won’t be able to attribute your audience’s unsubscribe requests to a particular message. --- ## Shortcuts to external services URL: https://docs.customer.io/journeys/people-shortcuts/ You can add **shortcuts** from people in Customer.io to external services, making it easy to go from a person in Customer.io to that person's account in your CRM tool, analytics platform, or any other service. You can even set up shortcut webhooks to call an external API from a person's page. You’ll find the Shortcuts button on the upper-right corner of any person’s page in your workspace. Go to People and select a person to add or use shortcuts. Add a shortcut  Customize links with liquid Though you add a shortcut from an individual person’s page in Customer.io, your shortcut will appear on every person’s page in your workspace. You should use 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}}. in your link to personalize the shortcut for everybody in your workspace. Go to People and select a person in your workspace. Click Shortcuts and select Add a shortcut. Select whether to add a Link or a Webhook. If you’re not familiar with webhooks, you probably want to add a link. Set a Name for your shortcut. This is how you’ll recognize your shortcut in Customer.io. Provide the Value for your shortcut. This is the URL you want to link to. Remember, you can include 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}}. in your URL. (Optional) If you selected Webhook in earlier steps, set your HTTP verb, request headers, and body (where applicable). For more information, see Webhook shortcuts below. Click Add shortcut.  Test your shortcut If you don’t want to perform the action defined by your shortcut, you can click Shortcuts > > Copy link to copy the link for your shortcut. Paste it into a text editor to make sure that it works as expected. Personalize shortcuts with liquid You add a shortcut on an individual person’s page, but shortcuts apply to all of your people. The shortcut that you add on one person’s page will also appear on everybody else’s page in Customer.io. You can template shortcuts using 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 produce URLs that apply to everybody in your workspace. While you can use conditionals and other complex liquid in your shortcut URLs, variables are limited to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and Customer.io “user” variables. Attributes are items you reference in liquid using customer—like customer.id to reference an ID or customer.email to get a person’s email address. This means that you should set the variables that you want to reference outside Customer.io as attributes on people in your workspace. User properties are the user.email, user.id, and user.role of the team member who uses the shortcut. These properties help you reference and capture information about the person using the shortcut, so you can audit who used the shortcut or assign the shortcut user as a support representative. You can find a person’s email address, ID, and role under Settings > Account Settings > Team Members. For example, imagine that you want to set up a webhook shortcut that assigns the person who clicks the shortcut as a support representative for a person. Your payload could reference both the “person” (customer) and Customer.io team member (user). { "needs_support": { "id": "{{customer.id}}", "email": "{{customer.email}}" }, "support_rep": "{{user.email}}" } Webhook shortcuts You can add a webhook as a shortcut. This provides a quick, low-code solution to call an external API. You could use shortcut webhooks to suspend or reactivate accounts, pull information about people, and trigger other actions in third-party services. When you set up a webhook shortcut, you’ll pick an HTTP verb, set request headers, and populate a request body where applicable. Like your request URL, request headers and bodies support 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}}., letting you populate your webhook request with information for each individual person. When you use a webhook shortcut, we’ll show you the response. While you can perform GET requests with a webhook, you cannot save or manipulate data from a response. If you want to save data from an external service and associate it with a person, you should create a campaign with a webhook action.  Webhook shortcuts do not retry Webhook shortcuts expect a response within 25 seconds and will not retry. If we don’t receive a response within 25 seconds, we’ll consider the request timed-out. Add/remove person in a manual segment Instead of uploading a CSV to adjust the people in a manual segment, you can add people to or remove them from a manual segment using a shortcut! Go to People and select a person in your workspace. Click Shortcuts and select Add a shortcut. Select Webhook. Add a Name to the shortcut like “Add to Segment_Name.” Select POST as the method. Add one of our Track API endpoints: To add a person, add this URL and replace the segment ID: https://track.customer.io/api/v1/segments/{segment_id}/add_customers. To remove a person, add this URL and replace the segment ID: https://track.customer.io/api/v1/segments/{segment_id}/remove_customers. Add a Header for Authorization. The value should be: Basic <siteID:APIkey>, where your credentials and the colon are base64 encoded. Add this body: {"ids":["{{customer.id}}"]}. Save changes. Click Shortcuts then select the shortcut you just made. You’ll receive a 200 if it’s successful: Go to your manual segment, and you’ll see the audience now includes or excludes the person! Test your shortcut After you set up your shortcut, you should test it to make sure that your liquid variables and logic render correctly. If there’s a liquid error in your shortcut—a property doesn’t exist, or the logic in your shortcut doesn’t render, you won’t be able to use the shortcut. You can hover over the shortcut to see more information about the error. You can copy a link URL and paste it into a text editor to test that your logic and variables render the way you expect. On a person’s page, click Shortcuts > > Copy link to copy any of your shortcuts. If there’s an error in your liquid, we’ll render the liquid in the URL itself. For example, if you added an extra angle bracket on customer.id, your shortcut URL will show {{customer.id}}} when you copy the link. Edit a shortcut Go to People and select a person in your workspace. Click Shortcuts, select for the shortcut you want to modify, and then click Edit. When you’re done with your edits, click Save changes. Delete a shortcut Go to People and select a person in your workspace. Click Shortcuts, select for the shortcut you want to modify, and then click Delete. --- ## Overview: Objects vs Collections URL: https://docs.customer.io/journeys/getting-started-objects-collections/ When you identify people or send events, you associate data with a person. But what if you want to manage data for a company, a series of educational courses, or other entities? That's what *objects* and *collections* are for in Customer.io: they provide ways to manage non-people data and associate it with people! How it works Customer.io revolves around people. You identify people, set their attributes, and send events representing the things that they do on your websites or in your apps. But you might want to maintain some data independently of people and associate this data with people later. Maybe you manage multiple accounts, and people can belong to one or more accounts; or people in your environment can enroll in educational courses; or you might manage recreational sports leagues that people can join. This is where Objects and Collections come in. But how do you decide which one to use?  When deciding whether to use objects or collections, consider: how many groups you have how often you will update them how you want to relate them to people Objects are easier to manipulate than collections - you can modify a single object unlike a collection where you’d have to replace the whole collection to make a change to a single item. Use objects when you have more then a couple thousand groups or when you’re going to update the items a couple times a day. Job Postings, for instance, would be a good fit for objects if you plan on creating or updating them on a daily basis. On the other hand, collections are free to Premium and Enterprise customers, easy to sync from a Google sheet, and work well for data you’d otherwise store in a spreadsheet. For example, imagine that you run a school where you manage a few hundred courses every year, a group of teachers, and classroom facilities. You might store your facilities and teachers as collections because there are fewer of them and they change infrequently. Then you might store courses as objects because it’s easy to change course data and relate courses to your audience of students. You should also consider how objects and collections relate to people. You can relate people to objects directly by adding relationships to their profiles. Relationships persist until you delete them. Relationships to collections do not persist beyond the workflow, though. After people exit a campaign, they’re no longer related to a collection. (You could, however, use a Create/Update Person action to add the collection data to a profile.) You also cannot relate collection data to people outside of a workflow. Rather, you must use a Query Collection action in a campaign to retrieve the relevant data and relate it to people while in the campaign. Objects Collections Relate to people Independently of workflows Only inside workflows Frequency of updates You can update an individual object at any time. You have to replace the entire collection to update a single item, so we recommend infrequent updates. Pricing Tiered Free (available on Premium and Enterprise plans) Objects An Object is a grouping mechanism in Customer.io—a way to associate people with an account, online courses, or recreational sports leagues. You’ll set up an object with its own attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., and then you can set relationships between people and the object. Your object can both supplement people data in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., 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}}., and other aspects of Customer.io. Objects are a bit like people in that they can contain attributes, and changes to them can trigger messages, but they can’t receive messages or perform events themselves. Rather, when you target an object, you’re really associating data or sending messages to people who are “related to” an object—like messaging a group of people when an online course changes or when they have new charges associated with their account. flowchart LR subgraph Object direction LR a[create object]-->|set relationship|b[person 1] a-->|set relationship|c[person 2] a-->|set relationship|d[person 3] end Object-->e[object triggers campaign] e-->f[person 1 enters campaign] e-->g[person 2 enters campaign] e-->h[person 3 enters campaign] Collections A Collection is data that exists independently of people or objects. You can query a collection as a part of a workflowA series of actions (messages, attribute updates, etc) that people progress through as a part of a campaign or broadcast., and use the data alongside people in the workflow. But, when the person exits the workflow, they’re no longer associated with the data. flowchart LR a[person triggers campaign]-->c subgraph Campaign c[query collection]-->|use collection data in message|d[send message] d-->e[person matches conversion criteria] end e-->|collection data no longer applies|f[person exits campaign] --- ## Objects: how they work URL: https://docs.customer.io/journeys/objects-start/ Not to be confused with JSON objects, an *Object* is a way to group people, like an *account* that people belong to, online courses that people enroll in, or recreational leagues that people can join. This page covers object types and objects. Object types are the kinds of non-people things that you want to track in Customer.io—like companies, online classes, accounts, etc. An object is an individual thing—a single company, online class, or account—that you want to relate to one or more people.  Check out our plans and how we bill to learn more about object pricing. How it works Objects let you group and relate data to people in your workspace, like accounts that people belong to, flights they’ve booked, or online courses they’ve enrolled in. Objects show up in the menu under People. Each object has its own attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you can use with 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 personalize messages. For example, you might message administrators of accounts to notify them when their accounts upgrade their plans. Without objects, you’d need to set attributes or send an event for every individual member of the account. But with objects, you can handle this one-to-many relationship the easy way. flowchart LR subgraph Account direction LR b[Admin Angela] c[Admin Oscar] d[Staff Kevin] end Account-->|plan is equal to premium|e[Send a campaign to admins of accounts] e-->f[Send message to Angela] e-->g[Send message to Oscar] Quick Start If you’re ready to try things out right away, here are the high points: Go to Custom Objects in the side menu to get started. Create an object type. Create one or more objects. Relate your objects to people. Trigger campaigns based on changes to objects or relationships. Check out how to reference objects in liquid. You can also segment users by their relationships to objects. --- ## Objects: video tutorials URL: https://docs.customer.io/journeys/objects-video-tutorials/ Introduction to objects Overview of objects in your workspace Using objects for SaaS/B2B use cases In this example, our Product Manager Cam shows you how to message admins when a new member joins their accounts. Cam created a campaign that triggers each time a person joins an account (i.e. for each new relationship). The person who joins the account is different from the people who should receive the campaign, which applies to many other use cases, like: messaging teachers when students are added to their roster messaging real estate agents when someone submits an application to buy a house  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s the example data we’re working off of. Technically speaking, this is a campaign with a trigger based on adding a relationship to an account where the audience is people with a specific relationship attribute. To understand the finer details, check out object and relationship campaigns. Using objects for your marketplace In this example, our Product Manager Cam shows you how to message people when the houses they’re interested in go on sale. But this set up applies to more than real estate! For instance, you could also use this kind of campaign: to notify admins of an upgrade to their account to message students when there’s a new homework assignment for their class  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s the example data we’re working off of. Technically speaking, this is a campaign with a trigger based on a change to an object where the audience is people with a specific relationship attribute. To understand the finer details, check out object and relationship campaigns. --- ## Object types URL: https://docs.customer.io/journeys/object-types/ An object type defines the kind of one-to-many relationship you want to set, like Accounts, Employers, or Job Postings. The first three types appear in the side nav under People. If you have more than three, we’ll nest all of your object types under Custom Objects. Depending on your plan type, you can create 2 or more object types. In our API, we identify object types as integers starting at 1. In the example above, the object type is Companies, which has 6 objects/individual companies. Each object type gets its own object_type_id, and you’ll set an object_id for each object. An object_id must be unique within a type, not across types. For example, object types 1 and 2 could each have an object identifier of a. Define a custom object type Remember, an object type helps you differentiate groups of objects. Customer.io generates a unique, immutable object_type_id. You can find this value on the Custom objects page in Workspace Settings. While we define the object_type_id, you define the object_id. You’ll use this value to reference objects belonging to your new type.  Creating objects and types with our API If you send an API request that includes an object_type_id that doesn’t exist in your workspace, we’ll automatically create a new one for you up to 15 times. We’ll do the same for any object_id that does not exist as well. Go to Settings > Workspace Settings, and select Custom Objects. Click Create Custom Object. Choose your object type on the left or create your own on the right. Give your object a Name and set the Singular form. In general, we expect that you’ll give objects a plural name (like “Accounts”); we simply use the Singular form to make better sense of your object name in prompts.  Liquid identifiers for objects When you create a new object type, we automatically set the identifier for liquid. This is how you’ll reference objects of this type when personalizing messages with 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}}.. For example, if your object type is called Accounts, you’d reference objects in the format {{objects.accounts}}. See Use objects in liquid for more information. Click Save. API calls create objects and types if they don’t exist If you send an API request that includes an object_type_id or object_id that doesn’t already exist, we’ll automatically create it for you. Object types auto-created through our API are disabled by default, but you can enable them if you’re within your plan’s allotment. If you add an object_type_id or don’t provide one via API, we name it a type of animal. You can set a new Name and liquid tag under Settings > Workspace Settings > Custom Objects. flowchart LR a[API request]-->b{Does object type exist?} b-->|yes|d{does object ID exist} b-.->|no|c[Create new animal object type]-.->d d-->|yes|e[update object & relationships] d-.->|no|f[create new object] c-.->g[Go to UI to change name and liquid tag] Edit or rename an object type You can change the name and liquid tag for object types. Changes to the Name and Singular Form affect what you see in Customer.io. Changing the liquid tag could affect your messages.  Is your liquid tag in use? Messages that reference your liquid tag in active journeys will fail to send after you change your liquid identifier, unless you have a fallback. Before you change your object’s liquid tag, check the Usage field for the object type within Settings > Workspace Settings > Custom Objects. We advise against changing a liquid identifier that’s in use because drafted messages or those triggered between changing the identifier and changing the reference will fail to send. Go to Settings > Workspace Settings > Custom Objects. Click Object Name. Update the Object Type Name, Singular Form and/or Liquid Identifier. Then click Save. Delete an object type You can delete an object type through the UI, but not the API at this time.  Deleting an object type also deletes relationships Deleting an object type permanently deletes all associated objects and relationships to people. Go to Settings > Workspace Settings > Custom Objects. Check the Usage of the object type. You can delete a type when it is no longer in use in segments, campaigns, broadcasts, snippets, and email layouts. If needed, adjust usage of your object type. Check the box beside each object type you want to delete. Select Delete at the top of the table. Confirm your selection.  You cannot re-use an object type id after it’s deleted. If you create a new object type in the UI, it is automatically assigned a new integer. If you are creating object types programmatically, you must use a new id; we will not create a new object type if the id was previously used/deleted. Enable/disable an object type Your plan determines the number of enabled object types you can have. An enabled object type is one you can successfully reference in messaging and segments. You can view all enabled and disabled object types in the side menu. You can create more than the number of object types allotted by your plan, but you can only enable the count that your plan allows. This grants you the flexibility to create objects and relationships while the object type is disabled so you can enable it shortly after disabling one you no longer need. Object types created over your plan allotment are disabled by default.  You can only disable a type if it is not referenced in any segments or messages. Go to Settings > Workspace Settings > Custom Objects. Check the Usage of the object type. You can disable a type when it is no longer in use in segments, campaigns, broadcasts, snippets, and email layouts. You can enable a type as long as you have not surpassed the number of object types allowed in your plan. If needed, adjust usage of your object type in segments and messages. Toggle Enable/Disable for the object type. Object types auto-created through our API - those without an id or with an id that doesn’t exist yet - are disabled by default, but you can enable them if you’re within your plan’s allotment. --- ## Objects URL: https://docs.customer.io/journeys/objects-create/ Learn how to create, edit, or delete objects.  Before you can create an object, you must create an object type. Create an object Remember that an object type is the kind of non-people grouping that you want to track in Customer.io—like Companies, Online classes, or Accounts. An object is an individual thing—a single company, online class, or account—that you want to relate to one or more people. You can create objects programmatically by integrating your systems with your Customer.io workspace or manually through your workspace’s interface. Programmatically create objects: via API via Pipelines API via reverse ETL integrations via Segment group calls Manually create objects: via your Object page via CSV import You can set relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between objects and people programmatically, using the same methods above, as well as through our UI. We don’t currently offer object-to-object relationships. Reserved attributes Customer.io has reserved these object and relationship attributes to support core functionality in the platform: Attribute Purpose Required Data Format cio_object_id A unique, immutable identifier for objects provided by Customer.io. If this does not yet exist in your workspace, we create a new object. When importing by cio_object_id object_id A unique identifier for objects. If the object_id does not yet exist, we create a new object. When importing by object_id Our default id limit is set to 150 characters. All valid UTF characters are allowed. objectId String An analog for object_id in some Customer.io integrations. relationship Used to reference relationships to objects. Cannot be used as the name of an object attribute. To reference relationships in liquid _relationship Used in relationship-triggered campaigns to reference audience members who did not trigger the campaign. Cannot be used as the name of a customer attribute. To reference relationships in liquid created_at Unix timestamp when the object was first created. Used when listing objects in the UI, for example. No Unix timestamp timezone The user’s time zone. Used for sending localized messages. No Region Format Create an object in the UI In the UI, you can create objects by going to your objects-type pages in the left hand menu under People. Select your object type under People in the left hand nav. Set the object_id: this is the value you use when you set relationships between objects and people. Set a name and any other attributes you want the object to contain. In general, we suggest that you set a name to help you recognize objects in the UI. Other attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. are values that you want to associate with people related to the object.  You cannot use relationship as an object attribute name. relationship is reserved for referencing relationship attributes in Liquid. Click Save Changes to finish creating your object. Now you can set relationships between your new object and people. Create an object in the API Creating an object works just like identifying a person: you can send an identify request to create a new object or update an existing object’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. Any request that passes a new object ID will automatically create that object. In general, it makes sense to create your custom object type first. But you can create a new type when you create a new object, simply by passing an object_type_id that doesn’t already exist. If you’re not sure if a type exists, check in your workspace under People—if you see the object type you want to create, then pass that id. You can create objects using our SDKs which are mostly built on the Pipelines API and have group methods to help you create objects and relate them to people. If you use our older JavaScript snippet (which references track.customer.io), you’ll use our Track V1 API. We also have a Track v2 API that supports batching.  You cannot use relationship as an object attribute name. relationship is reserved for referencing relationship attributes in Liquid. Pipelines API (Recommended) Pipelines API (Recommended) curl --request POST \ --url https://cdp.customer.io/v1/group \ -u api_key: \ -H 'content-type: application/json' \ --data-raw ' { "userId": "42", "groupId": "acme", "traits": { "objectTypeId": 1, "name": "ACME, Inc.", "bill_day": 15, "monthly_due": 100.00, "relationshipAttributes": { "role": "primary_contact" } } }' Track API v2 Track API v2 In the v2 API, you’ll specify a type and an action. The object type lets you modify objects, and the identify action lets you create an object, set its attributes, and relate it to multiple people. You can create objects and set relationships with other action types, but you can only set attributes through our API with the identify action. (You can also update object attributes in the UI.) curl --request POST \ --url https://track.customer.io/api/v2/entity \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data-raw ' { "type": "object", "identifiers": { "object_type_id": "1", "object_id": "acme" }, "action": "identify", "attributes": { "name": "acme", "bill_day": 15, "monthly_due": 100.00, }, "cio_relationships": [ { "identifiers": { "id": "42" } }, { "identifiers": { "email": "billing-contact@example.com" } } ] }' Track API v1 Track API v1 In our original Track API, you can set cio_relationships to relate a person to one or more objects. If an object_type_id or object_id doesn’t exist, we’ll create it. curl --request POST \ --url https://track.customer.io/api/v1/customer/billing-contact@example.com \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data-raw ' { "plan": "basic", "role": "accountant" "cio_relationships": [ { "identifiers": { "object_type_id": "1", "object_id": "acme" } }, { "identifiers": { "object_type_id": "1", "object_id": "globex" } } ] }'  An object’s attributes cannot exceed 100kb You can keep setting relationships on an object, but the total attributes for an object can’t take up more than 100kb. Import objects and relationships You can import objects and relationships through: Most of our integration libraries Reverse ETL integrations Segment group calls CSV imports Our integration libraries use group calls to help you import objects and relationships. You can sync objects (like companies, accounts, or online classes) and relate them to people as a part of our Reverse ETL database integrations - MySQL, BigQuery, Snowflake, and so on. This helps you add objects and relationships on a regular interval based on business logic and data that you store outside of Customer.io. If you use Segment as a CDP, import objects and relationships through Segment group calls. You can import objects and relationships via CSV, as well. This is a good option if you’re just getting started and want to quickly add objects or relationships to your workspace. You can also import via CSV if your data isn’t available through an integration. Edit or remove object attributes You can set attributes for an object through our UI or you can update them using the same API calls that you used to create objects. Go here to learn how to edit object attributes through Segment group calls. In our Track v2 API, use attributes to modify an object’s data. Set an attribute value to null or an empty string to remove attributes.  You cannot use relationship as an object attribute name. relationship is reserved for referencing relationship attributes in Liquid. To update object attributes in our UI: Select your object type from the left hand nav under People. Then click the object you want to modify. Click Manage under Attributes. Set attributes for the object or click the trash can icon to remove attributes. Then click Save. Delete objects You can delete objects in the UI, API or through Segment group calls. Deleting an object permanently removes the relationship from people. To delete objects in the UI: Select your object type from the left hand nav. If applicable, filter by object attributes to locate the ones you want to remove. Check the box next to each object you want to delete. Select Delete at the top of the table, and confirm your selection. To delete, you can also select the object name to navigate to its landing page. Then in the top right, select Options > Delete and confirm your selection. --- ## Relationships URL: https://docs.customer.io/journeys/relationships/ Create relationships between objects and people to segment users based on the groups they belong to and to personalize messages with object data. You can create and remove relationships between objects and people through: object and people pages in the UI our Track API our web sdk reverse ETL integrations our Segment integration While you can relate objects to people, you can’t relate objects to objects. If this is functionality you need, please share your use case with us. Set relationships in the UI You can relate objects and people through our UI individually and in bulk. To remove relationships, visit an object or person page. You can track the addition and removal of relationships in your Activity Log. Set relationships in bulk From the People page, you can relate multiple people to multiple objects within a single type. Go to the top right dropdown and select Add Relationships. Check the box next to each relevant person then continue to the next step. Check each object you want to relate to the people you checked. (Optional) Add relationship attributes—information that defines how the person is related to the object. Review and confirm. You can also accomplish this from an Object Type landing page. Set relationships individually From a person’s page, you can add relationships to multiple objects. Go to People in the left hand nav. Select a person. Select Options > Add relationships in the top right. Select the Object Type you want to target. Check each object you want to set a relationship to. Review and confirm. From an Object page, you can do the opposite - relate this one object to multiple people. Delete relationships in the UI You can delete relationships from the Relationships tab of a person or object page. To delete relationships for a person: Go to a person’s page. Select the Relationships tab. Check the box beside each relationship you want to remove. Select Delete at the top of the table and confirm your action. Set/delete relationships programmatically You can relate objects to people in the same ways you can create objects through our API, our Web SDK, our reverse ETL integrations, and our Segment integration. You can set relationships when you create an object (the identify action) or later on, like when people enroll in a class or leave a company, with the add_relationships or delete_relationships actions. You can also set relationships using our v1 API, but the examples below use our v2 API, which is more flexible and provides the ability to send multiple requests in the same call. You can set relationships on an object or a person—determined by the type parameter. Specify an object if you want to set relationships with multiple people; specify a person if you want to relate them to multiple objects. Object to multiple people Object to multiple people curl --request POST \ --url https://track.customer.io/api/v2/entity \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data-raw ' { "type": "object", "identifiers": { "object_type_id": "1", "object_id": "acme" }, "action": "add_relationships", "cio_relationships": [ { "identifiers": { "id": "42" } }, { "identifiers": { "email": "billing-contact@example.com" } } ] }' Person to multiple objects Person to multiple objects curl --request POST \ --url https://track.customer.io/api/v2/entity \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data-raw ' { "type": "person", "identifiers": { "email": "billing-contact@example.com" }, "action": "add_relationships", "cio_relationships": [ { "identifiers": { "object_type_id": "1", "object_id": "acme" } }, { "identifiers": { "object_type_id": "1", "object_id": "globex" } } ] }' Person to objects (JS) Person to objects (JS) If you use our Web SDK, you can set relationships using the identify function. Your JavaScript snippet must include the line t.setAttribute('data-use-array-params', 'true');, or you’ll receive an error when you attempt to set relationships this way. _cio.identify({ id: 'userid_34', email: 'customer@example.com', cio_relationships: { action: "add_relationships", relationships: [ { identifiers: { object_type_id: "1", object_id: "acme" } } ] } });  Send fewer requests with the batch endpoint You can use our /v2/batch endpoint to send multiple requests in the same payload. Set relationship attributes with the Pipelines API You can set relationship attributes using the Pipelines API and most of our integration libraries. You can set relationship attributes in either a group call or an identify call. In both cases, you’ll put relationship attributes in the traits.relationshipTraits object. Group Group { "userId": "student-1234", "groupId": "math101-2024", "traits": { "object_type_id": "2", "relationshipTraits": { "dues_paid": true, "class_complete": true, "grade": "A-" } } } Identify Identify { "userId": "student-1234", "traits": { "object_type_id": "2", "objectId": "math101-2024", "relationshipTraits": { "dues_paid": true, "class_complete": true, "grade": "A-" } } }  We default object_type_id to 1 You don’t need to define the object_type_id, but we default to 1—your first object type—if you don’t include it. Update relationship attributes Just like you can add attributes to objects and people, you can also add attributes to relationships between objects and people. For instance, if someone is related to three account objects, you could set an attribute like role on their relationships to describe how they’re related to each account—like an owner on one account and a member of the others. To set relationship attributes between a person and an object: On a person’s profile, select the Relationships tab. Click on the attribute count. Click Create New Attribute on the right hand panel. Enter the attribute name and value for the person. Click Save at the bottom of the panel. You can also accomplish the above through the Relationships tab of an object’s page. Use our Track v1 or v2 APIs to set relationship attributes programmatically: v1: Add or update a customer v2: Make a single request v2: Send multiple requests Now, you can create a data-driven segment or trigger a campaign based on relationship attributes. Reserved attributes Customer.io has reserved these object and relationship attributes to support core functionality in the platform: Attribute Purpose Required Data Format cio_object_id A unique, immutable identifier for objects provided by Customer.io. If this does not yet exist in your workspace, we create a new object. When importing by cio_object_id object_id A unique identifier for objects. If the object_id does not yet exist, we create a new object. When importing by object_id Our default id limit is set to 150 characters. All valid UTF characters are allowed. objectId String An analog for object_id in some Customer.io integrations. relationship Used to reference relationships to objects. Cannot be used as the name of an object attribute. To reference relationships in liquid _relationship Used in relationship-triggered campaigns to reference audience members who did not trigger the campaign. Cannot be used as the name of a customer attribute. To reference relationships in liquid created_at Unix timestamp when the object was first created. Used when listing objects in the UI, for example. No Unix timestamp timezone The user’s time zone. Used for sending localized messages. No Region Format Find object relationships You can find relationships from the Relationships tab of an object or person page. To see who is related to an object: Select an Object Type under People in the side menu. Select an Object. Click the Relationships tab to view a list of related people. Filter for objects without relationships Filtering for objects without relationships can help you determine if you have unused objects. Since objects count towards billing, it’s best practice to review them monthly and delete the objects you don’t need. Select your object type in the left-hand navigation, and then click Objects without relationships to review them. Learn more about how billing works and how to reduce billing overages. --- ## Import objects or relationships via CSV URL: https://docs.customer.io/journeys/import-objects/ Add or update objects and relationships via CSV to add or update data in Customer.io. This is a good option if you're just getting started and want to quickly add objects or relationships to your workspace. You can also import if your data isn't available through an integration. How it works You can upload CSVs or Google Sheets to add or update objects and relationships outside your normal integration path. When you upload a CSV of objects, each row in your CSV represents an object, and each column represents an object attribute. When you upload a CSV of relationships, each row is a relationship between an object and person, and each column (besides object_type_id, object_id, and the identifier of the person) is an attribute on the relationship. You can upload a CSV or Google Sheet of objects or relationships from the Imports page. Under Configure data, click More to find it. You can also import from People or an Object Type page. Import a CSV of objects Before you import objects, create your CSV. An object CSV only requires an object_id, but you can also specify object attributes like name, created_at, and anything else relevant to your objects. You can import objects for a single object type via CSV or Google Sheets. To import objects via CSV: Under Configure data, click More > Imports, then click Import in the top right. Select the radial next to the object type you want to import. You can also import through People > Add People > Import a CSV in the top right. Or click the object type in the left hand menu > Add an object > then Import a CSV. Click Choose File at the bottom of the import list. Make sure your file meets our CSV requirements. Set up your import and then click Next. Set a Name and Description for your import, helping you identify your CSV on the Imports page. Select whether to add new objects or not. Select whether to update existing objects or not. Determine how to handle empty values — ignore them or delete existing attribute values. Map fields from your CSV to attributes in Customer.io and click Next. At the bottom of each field, you’ll see “Map to attribute.” We map each field in your CSV to an attribute in your workspace or create a new attribute. You must map a field to object_id if your file does not include a column labeled such; all other columns are optional.  Creating new attributes If you import a column, but you don’t map it to an existing attribute, we create a new attribute using the column title. Review your import for errors and warnings. Review our section on import errors and warnings for help. (Optional) Click Preview Import to download a CSV file that reflects your final import, including all data mappings, skipped attributes, etc. Make sure that your import is correct. You cannot stop the import process after you click Complete Import.  Each row in your CSV can trigger a campaign Customer.io processes imports row-by-row. Object-triggered campaigns may fire as we create new objects, so review your import carefully! Click Complete Import to begin importing objects. The import process takes approximately one minute per 20-30 thousand rows. You can leave the page, and we will email you when your import is complete. Under Configure data, click More > Imports to revisit this import or see your previous imports. On the table, you’ll see how many rows were imported. Hover over the count to check how many objects were updated or created. Import a CSV of relationships Before you import relationships, create your CSV. You can import relationships for one or more object types via CSV or Google Sheets. A relationship CSV requires both object_type_id and object_id to identify the object as well as an identifier - id, email or cio_id - for the person in the relationship. You can optionally specify relationship attributes like role, created_at, and anything else relevant to these relationships.  Add objects and people before importing relationships You cannot create people or objects when you import relationships; the objects and people referenced in the CSV must already exist in your workspace. Check out Add/update people and events and Import objects for more info. To import relationships via CSV: Under Configure data, click More > Imports, then click Import in the top right. Select the radial next to Relationships. You can also import through People > Add People > Import a CSV in the top right. Or click the object type in the left hand menu > Add an object > then Import a CSV. Click Choose File at the bottom of the import list. Make sure your file meets our CSV requirements. Set up your import and then click Next. Set a Name and Description for your import, helping you identify your CSV on the Imports page. Select how to identify people - by id, email, or cio_id. Select whether to add new relationships or not. Select whether to update existing relationships or not. Determine how to handle empty values — ignore them or delete existing attribute values. Map fields from your CSV to attributes in Customer.io and click Next. At the bottom of each field, you’ll see “Map to attribute.” We map each field in your CSV to an attribute in your workspace or create a new attribute. You must map a field to object_type_id and object_id if your file does not include columns labeled such. You also need to specify an identifier for a person - id, email, or cio_id. All other fields are optional.  Creating new attributes If you import a column, but you don’t map it to an existing attribute, we create a new attribute using the column title. Review your import for errors and warnings. Review our section on import errors and warnings for help. (Optional) Click Preview Import to download a CSV file that reflects your final import, including all data mappings, skipped attributes, etc. Make sure that your import is correct. You cannot stop the import process after you click Complete Import.  Each row in your CSV can trigger a campaign Customer.io processes imports row-by-row. Relationship-triggered campaigns may fire as we create relationships or update their attributes, so review your import carefully! Click Complete Import to begin importing relationships. The import process takes approximately one minute per 20-30 thousand rows. You can leave the page, and we will email you when your import is complete. Under Configure data, click More > Imports to revisit this import or see your previous imports. On the table, you’ll see how many rows were imported. Hover over the count to check how many relationships were updated or created. Import a Google Sheet If you want to import Google Sheets, you must login to your Google account and allow us access to your sheets. You’ll see this includes the ability for us to read, edit, create, and delete the specific files that you share with us. However, we will only ever read files; we don’t write changes to your documents. After you grant access, you can select the individual sheets that you want to share with Customer.io. CSV requirements To upload an object CSV, it must: contain a column object_id that maps to each object not exceed 100MB in size not contain more than 100 columns be in CSV format OR a Google Sheet To share Google Sheets, you must log in to your Google account and grant Customer.io access to your sheets. To upload a relationship CSV, it must: contain a column for object_type_id that maps to the object type of each object in your relationships contain a column object_id that maps to each object in the relationship contain a column for the identifier of the person in the relationship - id, email or cio_id not exceed 100MB in size not contain more than 100 columns be in CSV format OR a Google Sheet To share Google Sheets, you must log in to your Google account and grant Customer.io access to your sheets. Review errors and warnings On the final Review step, we validate your import and return errors and warnings for rows in your CSV file. Rows with errors will not be imported, but rows with warnings will. Depending on the size of your CSV file, it may take a moment for us to validate your import. If there are no errors or warnings, you can continue importing your file as normal. If there are issues, you may want to correct your CSV file so you import all items. Errors are issues that prevent us from importing a row: The row is missing a value in the “id” column. “object_id” is empty. The specified Person does not exist. The specified Object does not exist. Warnings are issues that do not prevent us from importing a row, but that you may want to address to make sure your data is well formed and consistent: Same “id” paired with multiple “email” values. Multiple rows have the same “email” value. Click Preview Import to see which rows would successfully import. You can also click Export errors file or Export warnings file to download a CSV containing the issues found. Each file contains the rows from your original CSV file that resulted in errors or warnings respectively, including 2 new columns: _row: contains the row number from your original file that contained an error or warning. _errors or _warnings: lists errors/warnings for a row.  Re-import your error CSV You can import users directly from an error or warning CSV file after you correct the errors. Make sure you remove the “_row”, “_errors” and/or “_warnings” columns. --- ## Export objects or relationships via CSV URL: https://docs.customer.io/journeys/objects-export-many/ Export data about objects and relationships to CSV files. Export a single object and its relationships You can export data for a single objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. from an object’s detail page or the object list page. Select an object type from the left hand navigation. Click the name of the object you want to export. Click Options in the top right then choose “Export Object data.” Confirm your action. You will receive an email to download the export. You can also find it on the Exports page (under Configure data, click More). The export will include two JSON files - one with the object’s attributes and the other with relationships to the object: attributes.json contains all current attribute values for the object. relationships.json contains all people related to the object. People data includes identifiers like cio_id, email, and id. The object is identified by its cio_object_id. Both contain cio_object_id so you can reconcile the files. Export many objects and relationships You can export objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. for one object type at a time. For example, you can export your Online Classes or Schools, but you can’t generate a single export with both Online Classes and Schools. Select an object type from the left hand navigation. Click Export to CSV in the top right then choose what you want to export: All attributes exports all object attribute data. Displayed attributes exports the attributes displayed on the table. By default, this option exports object_id, created_at, and name. If you change the displayed attributes through Edit Display Attributes, it will export those. Choose attributes exports only the attributes you select. (Optional) Check the box if you would like to export relationship data for each object. Confirm your action. You will receive an email to download the export. You can also find it on the Exports page (under Configure data, click More). You cannot export more than 2 million relationships. If you attempt to, the export will fail. In this case, you can select fewer objects to export and try again. The export will be a CSV file. If you chose to export relationships, you’ll receive a separate CSV. You can reconcile the files with cio_object_id. Export objects that meet filter conditions You can also narrow down the objects you want to export by filtering by an object attribute. You can pair this with exporting all attributes for each object, displayed attributes on the table, or a set of attributes after selecting Export to CSV. Expiry After 60 days, the export expires, and we delete it from your workspace. If you need the export again, you can always generate a new one! --- ## Objects and relationships in campaigns URL: https://docs.customer.io/journeys/object-and-relationship-campaigns/ You can trigger campaigns based on changes to objects and relationships. You can target trigger data in messages and specify who should enter a campaign based on the relationship they have to objects. Trigger campaigns based on objects or relationships You can trigger campaigns based on changes to objects or relationships. You can trigger a campaign when: an object is updated (like an Account’s name was changed). a relationship between an object and person is added. a relationship is changed (like a person is no longer associated with an Account). You can also narrow in on the audience for these campaigns. For both object and relationship-triggered campaigns, you can choose to send to: every person in the object or certain people in the object For relationship-triggered campaigns, you can also choose to send only to the person that triggered the campaign.  An object or relationship can’t fan out to more than 1,000 people. If an update triggers journeys for more than 1,000 related people, none of them start a journey. You’ll see “Failed Journeys for Object/Relationship Campaign” in your activity log. This limit is per trigger event, not per campaign—your campaign can have more than 1,000 total recipients as long as each individual trigger doesn’t fan out to more than 1,000 people. Learn more about the fan-out limit. Visit Campaign Triggers for more on object-triggered campaigns or relationship-triggered campaigns. Per-trigger fan-out limit of 1,000 When an object or relationship changes, the campaign runs for every person related to that object. A single trigger event can’t fan out to more than 1,000 people. If a trigger includes more than 1,000 people, none of them start a journey. This limit is per trigger event, not per campaign. Your campaign can have far more than 1,000 total recipients—the limit only applies to how many people a single object or relationship update can reach at once. Under the limit Under the limit You have 10,000 company objects, each related to 5 people (50,000 people total). When any company’s properties change, the campaign triggers for the 5 people related to that company. Every person can go through the campaign because each trigger event fans out to only 5 people. Over the limit Over the limit You run an auction site with a single auction object related to 50,000 interested people. When the auction’s properties change, the trigger tries to fan out to all 50,000 people at once—but the 1,000-person limit blocks the entire batch, and nobody starts a journey. Why the limit exists A single object update can trigger messages for every related person simultaneously. Without a cap, one property change on a popular object could kick off thousands of simultaneous journeys, slowing down your workspace. The 1,000-person limit is a backstop to prevent this kind of problem. Working within the limit If an object or relationship update would fan out to more than 1,000 people, you might want to: Narrow your audience. Switch from “Every person in the object” to “Certain people in the object” and add conditions to reduce the total number of people per trigger. Restructure your objects. Break large objects into smaller ones with fewer relationships. For example, split a single “auction” object into regional auction objects so each update affects fewer people. Performance considerations Imagine you have a few Account objects with 10 people related to each of them. Each Account has a lifetime_value attribute, and you have five segments and five campaigns that send messages based on an Account’s lifetime_value. Each time an Account’s lifetime_value gets updated, our system checks each of the 10 people related to that account to see if they should still belong to any of the segments or campaigns. One little change - to a single Account’s attribute, in this case - caused us to check 100 things (5 segments x 10 people, 5 campaigns x 10 people). At this scale, that’s no problem. But imagine you have hundreds of thousands of Accounts and most of them change every day. That can pretty quickly become hundreds of millions, even billions, of things for us to process. We know how to handle this - it’s our job! But if you ever get to an extreme enough scale, we’ll send you warning messages so you know well in advance if your account could be impacted. In very extreme situations, we might need to throttle the data we process from you - but again, we’ll always warn you long before we get to that. Bottom line, the more segments and campaigns you have based on objects, the more you should make sure you’re targeting and updating the correct objects and relationships. For instance, instead of sending us your entire database every hour, send us only the object data we need. Of course, if you have any concerns, reach out to us and we’ll be happy to help! Workflow actions for objects and relationships You can use object or relationship data in any workflow item, including Create or update person, Send event, Send and receive data, Wait Until blocks, and Batch update. How else would you like to manipulate object and relationship data in your campaigns? Let us know at product@customer.io. We’d love to hear your use cases! Random Cohort Branch While random cohorts typically send people down different paths, you can use the Cohort by setting to send people related to the same custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. down the same path. This ensures that people related to the same object all have the same campaign experience. True/false branch In true/false branches, you can send people down different paths based on the their relationships to objects. All campaigns, except those with webhook triggers Drag a true/false branch onto your workflow. Click the branch, then click Add condition > Relationship to define a branch based on relationships to objects in your workspace. For instance, you could trigger a campaign when people open a page explaining a certain feature (event-triggered campaigns). Then you could branch them based on whether they’re related to any premium account to personalize messaging. You can add other conditions to refine the path further - conditions are only joined by AND statements, not OR. Object or relationship-triggered campaigns In object or relationship-triggered campaigns, you can also refine true/false branches by the trigger object. Drag a true/false branch onto your workflow. Click the branch then click Add condition > Object-type-name (Trigger) to define a branch based on the object triggering the campaign. For instance, imagine you trigger an onboarding campaign when a person is added as an admin to any account. The onboarding flow for accounts on legacy plans is different from those on newer plans. So you add a true/false branch to your workflow and split users based on whether the account they were added to has a legacy plan type. Or maybe you trigger a campaign when an account’s plan type is upgraded to a modern one. This impacts admins of your accounts differently from all other roles so you add a true/false branch based on whether their relationship attribute role is equal to admin. For this, you’ll click Add condition > Relationship. Multi-split branch You can send people down different paths based on object and relationship conditions in multi-split branches. Unlike true/false branches, multi-split branches allow you to split people into more than two paths. All campaigns, except those with webhook triggers Drag a multi-split branch onto your workflow. Click the branch then click Data Type > Relationship. You can define people related to any object in your workspace. For instance, imagine you want to encourage people to use your app after being inactive for 30 days. You could trigger a campaign when they join a segment based on inactivity then branch these users based on their plan type to personalize messaging. Object or relationship-triggered campaigns With object and relationship-campaigns, you can also refine branch conditions based on the trigger object. Drag a multi-split branch onto your workflow. Click the branch then click Data Type > Object-type-name (Trigger). For instance, imagine you trigger an onboarding campaign when a person is added as an admin to any account. The onboarding flow for accounts on legacy plans is different from those on newer plans. So you add a multi-split branch to your workflow and split users based on plan_type. You create different messaging for those on a legacy plan, premium plan, and all others. Or maybe you trigger a campaign when an account’s plan type is upgraded to Enterprise. This impacts people differently based on their role on the account, so you add a multi-split branch based on the relationship attribute role. You create different messaging for admins, managers, and anyone without those roles. For this, you’ll click Data Type > Relationship. Trigger a campaign with a relationship-based segment People enter segment-triggered campaigns when they join (or leave) a segment. But when you create a relationship-based segment, a person only joins the segment for their FIRST relationship to an object matching your criteria. Their segment membership won’t change on subsequent relationships. This means that a person will only trigger a campaign for the first relationship matching your segment criteria. To trigger a campaign for each relationship change, create a relationship-triggered campaign. For example, imagine that you have a segment of people related to online classes, and you use that segment to trigger a campaign: When a person signs up for their first online class, they’ll join the segment and enter the campaign. When they sign up for another online class, they won’t trigger the campaign again because their segment membership didn’t change! In this example, a person would only re-join the segment if they unenrolled from all their classes, causing them to leave the segment, and then re-enrolled in a class later. flowchart LR a(add relationship to person)-->b{is this the first relationship?} b-->|yes|c(person joins segment)-->e(Person enters campaign) b-.->|no|d(person does not enter campaign) Exit Conditions follow these same principles: a person won’t leave a segment (causing them to exit a campaign) until they’re no longer related to any objects matching your segment criteria. flowchart LR a(remove relationship from person)-->b{is this the only relationship?} b-->|yes|c(person leaves segment)-->e(Person exits campaign) b-.->|no|d(person remains in campaign) Use an object in a segment You can create object-oriented segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. on the Segments page, or when you create campaigns and newsletters. Learn more about how relationship-based segments work in campaigns. Go to Segments and click Create Segment. Give your segment a Name and a Description, and click Create Data-Driven Segment. Under Add condition or group, select Relationship and set a condition to Relationship to your object type exists. (Optional) you can refine your selection by clicking Refine and selecting an object-based attribute condition. For example, you might want to limit a newsletter segment based on a country or plan_name attribute. When you’re done, click Save Changes. Now you can use your segment in newsletters or to trigger campaigns. You cannot backfill relationships We don’t have a concept of historical relationships in Customer.io—at least not yet. For example, a person might have joined a company several years ago, but it will appear that they only recently joined the company when you set their relationship in Customer.io. Because you can’t backfill relationships, you should be careful when using relationship-based segments to trigger your campaigns. For example, if you want to send a welcome campaign for new employees of a company, and you relate people to the company after you set up your campaign, you may inadvertently trigger a welcome campaign for those people. --- ## Use objects in liquid URL: https://docs.customer.io/journeys/objects-in-liquid/ Learn how to reference objects and relationships using liquid. Reference trigger data You can trigger campaigns based on changes to objects and relationships, so we made it possible for you to use liquid that targets the trigger object or relationship data. You can reference trigger data for object and relationship-triggered campaigns using {{trigger.<object_type>.<attribute_name>}}. For instance, if you have an object type of “Online Classes” and set up a campaign to trigger when you update a class, you could craft an email like the following: Liquid Syntax Rendered Liquid Hi {{customer.first_name}}! We just added a syllabus to {{trigger.online_class.name}}. Hi Jeff! We just added a syllabus to Calculus 1A. See the liquid examples below for more use cases.  New to JSON? Objects in Customer.io use JSON dot notation; if you’re not familiar with JSON, we have a quick primer to help you understand how it works. Relationship-triggered campaigns You can use the following liquid when a relationship triggers a campaign: {{trigger.relationship.<attribute_name>}} {{trigger.<object_type>.<attribute_name>}} {{trigger.customer.<attribute_name>}} Recall that a relationship-triggered campaign can have an audience that is different from the person with the relationship that triggered a campaign. You have the ability to reference both people using liquid. For instance: Liquid Syntax Rendered Liquid Hi {{customer.first_name}}! {{trigger.customer.first_name}} just joined your {{trigger.online_class.name}} class. Hi Jeff! Ali just joined your Calculus 1A class. {{customer.first_name}} refers to the audience/recipient of the message, and {{trigger.customer.first_name}} refers to the person with the relationship that triggered the campaign. Object-triggered campaigns When you set up campaigns that trigger on object changes, you can pull data directly from the trigger object. This lets you create personalized messages based on whatever just changed, using liquid. The following liquid lets you pull in attribute data from the object trigger: {{trigger.<object_type>.<attribute_name>}} Exception to trigger data Customer.io uses customer and relationship as keys in liquid tags to access data stored on your people and relationship data associated with your objects, respectively. For this reason, we recommend you NOT name your object types “Customers” or “Relationships.” However, if you already named your objects “Customers” or “Relationships” and want to reference trigger data, you’ll need to add an underscore to the key, like {{trigger._customer.<attribute-name>}} or {{trigger._relationship.<attribute-name>}} Otherwise, referencing the keys relationship or customer without the underscore will pull in object data, not trigger data, like {{trigger.relationship.<attribute_name>}} or {{trigger.customer.<attribute_name>}}. Reference non-trigger data If you’ve read the above, you may be wondering how to reference object data when an object or relationship is NOT the trigger for a campaign. For instance, you may want to: check if someone is related to an object list values for the same attribute across multiple objects Let’s imagine you have an object type of online classes and these classes have an attribute of name. You could reference non-trigger data for classes with the following liquid: {{objects.online_classes}} {{objects.online_classes[#]}} {{objects.online_classes[#].name}} You could reference relationships to these classes, as well. Below, we reference the relationship attributes role and name. {{objects.online_classes[#].relationship.role}} {{customer._relationship.name}} When using this liquid, just remember to replace the object type and attribute names to match your data! You can use most of this liquid across any of our automation workflows - campaigns, newsletters, API-triggered broadcasts, and transactional messages. This liquid {{customer._relationship.<attribute_name>}}, however, is only available in object and relationship-triggered campaigns (see below). We’ll get into examples below, but make sure you understand the following limitations before using non-trigger liquid in your campaigns. Before you begin  Before you use non-trigger liquid for objects and relationships, consider that: Referencing objects is relative Referencing an object is relative to: the number of objects a person is related to in your object type and the order the objects were created in This means that {{objects.online_classes[0]}} could refer to an English class for one person and a Math class for another; you can’t reference a specific object or relationship across all people in your campaign. [0] corresponds to the most recently created object that a person is related to and [9] corresponds to the oldest object that a person is related to. You cannot reference more than 10 objects of the same type If a person is related to more than 10 objects of the same type, like 11 classes, then only the 10 most recently created could render in a message. For example, {{objects.online_classes}} renders based on the date each object was created, NOT when a person was related to the object. This means that if you just related the oldest online class to a person with relationships to 10 other classes, then this class would not render in the list because the 10 other classes were created more recently. We know this isn’t the easiest to use - thanks for bearing with us as we improve this experience! The following examples imagine that you relate people to online classes. Object attribute If you wanted to reference just an attribute, you’d use {{objects.online_classes[0].<attribute_name>}}. Keep in mind: {{objects.online_classes[0].name}} is not necessarily the same class across all people who receive a message. That is, if Matthew were related to English Literature only and Clarice were related to Calculus 1A only, then {{objects.online_classes[0].name}} would render the data for each class name respectively. Relationship attribute If you wanted to reference relationship data for an object, you’d use {{objects.online_classes[#].relationship.<attribute_name>}}. Keep in mind: {{objects.online_classes[#].relationship.<attribute_name>}} is not necessarily the same class across all people who receive a message. That is, if Matthew were related to English Literature only and Clarice were related to Calculus 1A only, then {{objects.online_classes[0].relationship.<attribute_name>}} would render the data for each class respectively. Audience attribute In object or relationship-triggered campaigns, you choose an audience - the people who start a journey. For instance, if you triggered a campaign based on a person being added to an account, you could specify that you want people who are account admins to enter the campaign and receive messages. You’d reference the audience using {{customer._relationship.<attribute_name>}} in messages. (To target the person who triggered the campaign, you’d use trigger liquid). Here’s an example of how to message admins when people subscribe to their account: Create a campaign triggered by a person added to an account. Set the relationship trigger condition to role is equal to subscriber. Set the audience to “certain people in the account,” choose relationship from the dropdown, then enter role is equal to admin. Create a message to inform the admin(s) that a person was added to the account: Hi {{customer._relationship.admin}}! {{trigger.customer.name}} at {{trigger.account.name}} started using {{trigger.relationship.feature}}. Examples Check if a person is related to objects You can assign a variable to check if a person is related to objects: {% assign classes_count = objects.online_classes | size %} {% if classes_count == 0 %} Time to sign up for a class! Review our catalog. {% else %} Check out related courses! {% endif %} You use the size filter with dot notation to check for relationships: {% if objects.online_classes.size > 0 %} Check out related courses! {% else %} Time to sign up for a class! Review our catalog. {% endif %} This second example intentionally uses > 0 instead of == 0. You can’t compare keys with .size notation using the equals operator. Learn more about the size filter in our liquid reference guide. List objects related to a person {% for class in objects.online_classes %} {% if class.relationship.status == "not_started" %} - {{class.name}} {% endif %} {% endfor %} Message an audience member who did not trigger the journey This is only useable in object or relationship-triggered campaigns when the audience is “Certain people related to the object!” Check out Audience attribute for an example of a campaign you could use this in. Hi {{customer._relationship.admin}}! {{trigger.customer.name}} at {{trigger.account.name}} started using {{trigger.relationship.feature}}. Show all object data for up to 10 objects Use {{objects.<object_type>}} to show all attributes related to each object a person is related to. For instance, if a person were related to two classes, then {{objects.online_classes}} could render the following: { "_created_in_customerio_at"=>1685030419, "cio_object_id"=>"oba886080104", "created_at"=>1685030405, "name"=>"English Literature", "object_id"=>"ae3000-145" } { "_created_in_customerio_at"=>1681491863, "cio_object_id"=>"oba886080102", "created_at"=>1681491859, "name"=>"Calculus 1A", "object_id"=>1 } You likely don’t want to surface all object data to your customers, especially in this format! Consider using a for loop instead. Keep in mind, you cannot reference more than 10 objects related to a person of a single object type. Show all object data for an object Imagine that you created two classes as objects. First you added English Literature then you added Calculus 1A. If a person enrolled in both classes, which related them to the objects, you’d reference the person’s Calculus enrollment with {{objects.online_classes[0]}} and the person’s English class with {{objects.online_classes[1]}}. {{objects.online_classes[0]}} would show all of the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for the Calculus class! For example: { "_created_in_customerio_at"=>1685030419, "cio_object_id"=>"oba886080104", "created_at"=>1685030405, "name"=>"Calculus 1A", "object_id"=>"ae3000-145" } Keep in mind: {{objects.online_classes[0]}} is not necessarily the same class across all people who receive a message. That is, if Matthew were related to English Literature only and Clarice were related to Calculus 1A only, then {{objects.online_classes[0].name}} would render the data for each class respectively. If you want more control over the format of the object data, reference attributes like this instead: {{objects.online_classes[0].name}}. Object liquid in workflow actions You can use object and relationship liquid in any campaign type to set attributes through workflow actions like Send event, Create or update person, Send and receive data, and Batch update. If the campaign is triggered by an object or relationship, you can also set attributes equal to trigger data like {{trigger.account.name}} or {{trigger.relationship.status}}. Create or update person Use Create or update person actions to update people’s attributes. For instance, let’s say you schedule appointments and each appointment is an object. You could loop through all of a customer’s upcoming appointments and set next_appointment to the earliest date. {% assign next_appointment = appt %} {% for appt in objects.appointments %} {% if next_appointment == blank or next_appointment < appt.date %} {% assign next_appointment = appt.date %} {% endif %} {% endfor %} {{next_appointment}} Send event Use Send event actions to create events that trigger campaigns. In many cases, you may not need a Send event workflow action; you could create an object or relationship-triggered campaign instead to accomplish the same goal. But perhaps you want to separate actions into distinct campaigns to track multiple goals or simply separate parts of a campaign for ease of maintenance or understanding. Then a Send event can help. Send and receive data Use Send and receive data actions to trigger webhooks with object or relationship data. For instance, perhaps you need to send an account’s ID to reconcile billing in Stripe; you’d use this action to send your object’s data. You could also pass object data to your analytics platform like Mixpanel. Batch update Use Batch update actions to send events or update people in bulk. In object or relationship triggered campaigns, under “Which attributes do you want to add, change, or remove?” you can choose “Trigger object attribute” or “Trigger relationship attribute” from the dropdown as we populate the available attributes so you don’t have to write liquid. If this doesn’t fit your use case, you can also specify a profile attribute, choose “Liquid,” then include object or relationship liquid. --- ## Upsell: monetize power users URL: https://docs.customer.io/journeys/upsell-users/ In this object recipe, we'll create a broadcast to upsell power users on a higher tier subscription. The recipients are related to accounts that might upgrade. Business-to-business (B2B) companies often need to track and drive growth across both individual users of their product and the account or company they work for. In Customer.io, objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. help you do this. For this recipe, imagine you work at a design platform, Sketcher.io, which offers graphic design and product design tools. Each power user belongs to an Account, a type of object in your workspace that represents the company they work for. You want to identify Accounts that are getting a lot of value from Sketcher.io and convince them to upgrade to a higher tier plan. To accomplish this, let’s build a segment around admins of accounts that are active and on a free plan. Then we’ll send a newsletter to this segment of users encouraging them to upgrade. Prerequisites Access to create segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. and broadcastsA message sent to a group of people at the same time. Unlike campaigns, where individuals can enter campaigns and receive messages on their own time, you’ll trigger a broadcast for everybody meeting your criteria at once. When you send a broadcast, you can personalize broadcasted messages for individual members of your audience; everybody doesn’t have to get exactly the same message. Integrate non-people data as objects and relationships in your workspace Set up objects and relationships In this example, the workspace includes Account objects where some people are members of the account and others are admins. These roles, and other data like are saved as relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between people and objects. If you’re not yet integrated but want to test this out, you can import objects and relationships via CSV.  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s a spreadsheet with sample data - we’re working off of the Accounts and AccountsRelationships tabs. Create a segment of power users Go to Segments and click Create Segment. Add a name like “Admins for Accounts that might Upgrade,” and click Create Data-driven Segment. Click the dropdown “Add condition or group” and choose Relationship. Specify that the relationship between a person and Accounts exists with the following conditions or other conditions that fit your needs: In this example, Sketcher.io tracks the role a person has on the relationship to the account, so the first condition is targeting the relationship attribute role to find admins. Sketcher.io also tracks the plan type on the account, so we’ll set this account attribute to free. Finally, we’ll check the last time each person comments (last_commented) and shares (last_shared) in each account to find the most active users. Consider what attributes in your own data would help create this segment of users! Click Save Changes. Now you’re ready to create a newsletter to upsell these power users! Create a newsletter to upsell power users Go to Broadcasts and click Create Broadcast. Enter a title and choose Newsletter, like “Upsell for Recent Power Users.” Then click Create Broadcast. On the Recipients step, click People matching conditions. Add a segment condition that targets people in “Admins for Accounts that might Upgrade,” the segment you made earlier. Click Save & Next. On the Goal step, mark people as converted when they upgrade their plan. You’ll want to track this as an eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.. In this example, we’ll mark them as converted after performing the event plan_activated only when the plan_name of the event data is Premium or Enterprise. NOTE: this assumes that the person receiving the newsletter is also the person responsible for upgrading the account plan. We’ll mark this as a conversion if accomplished within 1 week of being sent this newsletter, but you can change that depending on the time you think your customers or sales team needs. Click Save & Next. On the Content step, click Email and either start a message from scratch or build from an existing email. Here’s an example of an email you could make! It includes the benefits of upgrading their plan. Click Save then Back to Content. Click Next to review your broadcast. On the Review step, check that your recipients and other settings are correct, then click Send Newsletter at the bottom or click Schedule to send it later. We made a broadcast because it’s the simplest option for messaging everyone in a segment at once. You could also build a segment-triggered campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. if you want to automatically reach out to people as they become power users (people who meet the segment criteria in the future). --- ## Adoption: drive feature usage URL: https://docs.customer.io/journeys/feature-adoption/ In this object recipe, we'll create a campaign to remind people they can share their work to collaborate with team members. The audience is related to accounts who have not shared recently, but have signed in recently. Business-to-business (B2B) companies often need to track and drive growth across both individual users of their product and the account or company they work for. In Customer.io, objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. help you do this. For this recipe, imagine you work at a design platform, Sketcher.io, which offers graphic design and product design tools. Each user belongs to an Account, a type of object in your workspace that represents the company they work for. You want to drive product usage by encouraging people to collaborate by sharing their work. To accomplish this, let’s build a segment of people who have not shared their work recently, but have signed in recently. Then we’ll create a campaign triggered by people in this segment and encourage them to share. Prerequisites Access to create segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. and campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. Integrate non-people data as objects and relationships in your workspace Set up objects and relationships In this example, the workspace includes Account objects and we track people’s feature usage on their relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. to these Accounts. If you’re not yet integrated but want to test this out, you can import objects and relationships via CSV.  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s a spreadsheet with sample data - we’re working off of the Accounts and AccountsRelationships tabs. Create a segment to target active users that have not used a feature recently Go to Segments and click Create Segment. Add a name like “Recent Users That Have Not Recently Shared,” and click Create Data-driven Segment. Click the dropdown “Add condition or group” and choose Relationship. Specify that the relationship between a person and Accounts exists with the following conditions or other conditions that fit your needs: In this example, Sketcher.io tracks the last time each person shares designs (last_shared) on the relationship to the account, so we’ll use this to determine if people have used the share feature recently. Consider what attributes in your own data would help filter for users that haven’t used a feature! Click the dropdown “Add condition or group” and choose Event. Choose the event that represents signing in to your platform, in this case, it’s sign_in. Then specify they should have signed in recently - at least once within the past 30 days. Click Save Changes. Now you’re ready to create a campaign to drive feature adoption! Create a campaign to drive feature adoption Go to Campaigns and click Create Campaign. Enter a name, like “Remind People to Share.” If helpful, add a description so other team members can understand the campaign at a glance. Click Manage under Messages. Set the Subscription preference to determine who will get your messages. Click Choose trigger and select Segment change. Choose the segment you made to target active users that have not used a feature recently. Save your changes. Click the trigger block again then click Frequency to expand the settings. Set the frequency of entry. We want this campaign to reach out to people once a month, so let’s change the frequency to “every re-match” after a minimum of 30 days. This means people will re-enter the campaign every time they re-enter the segment after at least 30 days. Next, drag an Email into your Workflow. Click the email then Add Content. You can create an email from scratch or from an existing email. Here’s an example of an email you could make! It encourages the audience of your campaign to log in and share their designs. Click Save then Back to Workflow. Click the email again, click Settings, and change the sending behavior from Queue Draft to Send automatically. Next, define what should cause people to convert. Click the title menu in the top left, and click Set goal. In this example, we’ll say people convert when they perform the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. share_with_colleague within 1 week of being sent any delivery from this campaign. Click Start Campaign to review your workflow and activate your campaign! --- ## Awareness: announce events URL: https://docs.customer.io/journeys/awareness-announce-events/ In this object recipe, we'll create a campaign to notify people they can register for an event that they want to attend. The audience has signed up for a waitlist that related them to an event object. Business-to-business (B2B) companies often need to track and drive growth across both individual users of their product and non-people entities like the company they work for or webinars they want to attend. In Customer.io, objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. help you do this. For this recipe, imagine you work at a design platform, Sketcher.io, which offers graphic design and product design tools. We track when users sign up to be notified of event registration and which events they’ve attended. Specifically, they can sign up for Webinars, which will be a type of object in the workspace. You want to drive registration for webinars by announcing upcoming webinars that expand on webinars they’ve already attended. To accomplish this, let’s build a segment around people who attended beginner webinars. Then we’ll send a newsletter to this segment of users encouraging them to take the intermediate-level webinars. Prerequisites Access to create segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. and broadcastsA message sent to a group of people at the same time. Unlike campaigns, where individuals can enter campaigns and receive messages on their own time, you’ll trigger a broadcast for everybody meeting your criteria at once. When you send a broadcast, you can personalize broadcasted messages for individual members of your audience; everybody doesn’t have to get exactly the same message. Integrate non-people data as objects and relationships in your workspace Set up objects and relationships In this example, the workspace includes Webinar objects and we track people’s registration status on their relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. to these webinars. If you’re not yet integrated but want to test this out, you can import objects and relationships via CSV.  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s a spreadsheet with sample data - we’re working off of the Webinars and WebinarsRelationships tabs. Create a segment of people who have attended a beginner event Go to Segments and click Create Segment. Add a name like “Recent Attendees of Beginner Webinars,” and click Create Data-driven Segment. Click the dropdown “Add condition or group” and choose Relationship. Specify that the relationship between a person and Webinars exist with the following conditions (or other conditions that fit your needs): In this example, Sketcher.io tracks when each webinar was scheduled (scheduled_date) and the level of the webinar (level), so we’ll include these attributes in conditions to filter for the right webinars. We also track when people attended the event (attended) on the relationship between the webinar and person, so we’ll set that to true. Consider what attributes in your own data would help filter for these users! Click Save Changes. Now you’re ready to create a newsletter to announce registration for the next level of webinar! Create a newsletter to announce upcoming, intermediate events Go to Broadcasts and click Create Broadcast. Enter a title and choose Newsletter, like “Upcoming Intermediate Webinars Announcement.” Then click Create Broadcast. On the Recipients step, click People matching conditions. Add a segment condition that targets people in “Recent Attendees of Beginner Webinars,” the segment you made earlier. Click Save & Next. On the Goal step, mark people as converted when they register for an intermediate webinar. You’ll want to track this as an eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.. In this example, we’ll mark them as converted after performing the event webinar_registration where the webinar has a level of intermediate. We’ll mark this as a conversion if accomplished within 1 week of being sent this newsletter, but you can change that depending on the time you think your customers or sales team needs. Click Save & Next. On the Content step, click Email and either start a message from scratch or build from an existing email. Here’s an example of one you could make! It includes the upcoming, available intermediate webinars. Click Save then Back to Content. Click Next to review your broadcast. On the Review step, check that your recipients and other settings are correct, then click Send Newsletter at the bottom or click Schedule to send it later. --- ## Conversion: activate people on your event waitlist URL: https://docs.customer.io/journeys/conversion-activate-waitlist/ In this object recipe, we'll create a broadcast to annoucnce a series of events that people may want to attend based on other events they've attended. Business-to-business (B2B) companies often need to track and drive growth across both individual users of their product and non-people entities like the company they work for or webinars they want to attend. In Customer.io, objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. help you do this. For this recipe, imagine you work at a design platform, Sketcher.io, which offers graphic design and product design tools. Users can sign up for events so that they’re notified when registration opens. Specifically, they can sign up for Webinars, which will be a type of object in the workspace. You want to drive registration for webinars by capturing a waitlist of people interested in the event and letting them know when registration opens. To accomplish this, we’ll create a campaign triggered by a webinar becoming available for registration. And the audience will be people who acquired a relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. to this webinar by signing up for the waitlist. Prerequisites Access to create campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. Integrate non-people data as objects and relationships in your workspace Set up objects and relationships In this example, the workspace includes Webinar objects and we track people’s registration status on their relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. to these webinars. If you’re not yet integrated but want to test this out, you can import objects and relationships via CSV.  You can import fake data to play around with but delete it once you’re done. You can import test data to follow along, but you might want to import to a new workspace so you don’t clutter your live data. Also remember to delete any unnecessary data after you’re done because people and objects count towards billing.  Here’s a spreadsheet with sample data - we’re working off of the Webinars and WebinarsRelationships tabs. Create a campaign to announce registration for events Go to Campaigns and click Create Campaign. Enter a name, like “Webinar is Ready for Registration.” If helpful, add a description so other team members can understand the campaign at a glance. Click Manage under Messages. Set the Subscription preference to determine the audience of your messages. Click Choose trigger, select the Webinars filter, then choose Webinar updated. On our webinar objects, we saved several attributes, including allow_registration. This indicates whether people will be able to sign up for the webinar. Set this equal to true for the trigger. Save your changes. Click the trigger block then click Audience settings. Select Certain people in the webinar. Then specify that you want people with the relationship attribute waiting_list equal to true. This is a relationship attribute we imported with our CSVs too. Next, drag an Email into your Workflow. Click the email then Add Content. You can create an email from scratch or from an existing email. Here’s an example of one you could make! It encourages people to register for the webinar they wanted to be notified about and reminds them of other webinars. And here’s the liquid that powers this! You can copy the text and liquid below! Hey {{customer.first_name}}, Great news! The webinar you’ve been waiting for, {{trigger.webinar.name}}, is now open for registration. It will be held {{trigger.webinar.scheduled_date | date: "%A, %B %d at %H:%M"}} —secure your spot today before it fills up, we'll be talking all about {{trigger.webinar.description | downcase}} {% if objects.webinars.size > 1 %}You're also already on the waiting list for these upcoming webinars!:{% for webinar in objects.webinars %} {{webinar.name}} - {{webinar.description}} - {{webinar.scheduled_date | date: " %b %d, %H:%M"}} {%endfor%}{%endif%} See you there, fellow Sketcher! Thank You for Choosing Sketcher.io We’re so grateful to have you as part of the Sketcher.io community. Stay tuned for more exciting updates coming soon! You could also create an in-app message to promote the same event on your website! Click Save then Back to Workflow. Click the email again and set the email to Send automatically. Next, define what should cause people to convert. Click the title menu in the top left, and click Set goal. In this example, we’ll say people convert when they perform the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. webinar_registration within 1 week of being sent any delivery from this campaign. Click Start Campaign to review your workflow and activate your campaign! --- ## Collections URL: https://docs.customer.io/journeys/collections/ Collections provide a way to store data in your workspace that you can use in campaigns with Liquid. What’s a Collection? A collection is a new type of data in Customer.io that’s separate from people or events in your account. They represent any set of “things” that exist in your business. For instance, a Collection could be a list of upcoming events, promotions/coupons, or available classes. You should store this data in Customer.io when it’s helpful to be able to sort through the Collection to identify a specific set of data that you want to include in a message, tailored to the data you know about each person. Let’s take the example of an online learning service. By storing all the courses offered in a Collection, you could retrieve the specific set of classes that are relevant for each individual person and list them in a campaign email. Collections are flexible. They’re simply lists of JSON objects that do not require specific schemas or indexes. However, your collection cannot be larger than 10 MB, and rows in your collection are limited to 10 KB.  You cannot use collections with Newsletters For now, you can only take advantage of collections in Campaigns. Create a Collection You can upload your collection as JSON, a CSV file, or by sharing a Google Sheet with us. If you upload a CSV file or share a Google Sheet, we expect your file to be a simple table with headers—each column is an attribute of your collection items. To share a Google Sheet with us, you must grant Customer.io access to specific files in your Google Drive. For example, if your collection contains a list of courses, your CSV might look like this: name,level,description,duration,instructor Introduction to Python,beginner,Learn the basics of Python programming,4 weeks,Jane Smith Data Analysis with R,intermediate,Analyze datasets using R,6 weeks,John Doe Advanced Machine Learning,advanced,Deep dive into ML algorithms,8 weeks,Alice Johnson Each row represents an item in your collection, and each column header becomes an attribute that you can reference in your messages. Collections must be smaller than 10 MB and you cannot have more than 50 collections in your workspace. To add a collection: Under Configure data, click More > Collections and click Add Collection. Give your collection a name and description. Select your collection file: JSON, CSV, or Google Sheet. Click Add Collection. Use Collection data in your messages Now that you’ve added a Collection, the next step is to use them in your campaigns. The power of Collections is that you can query the Collection as part of a campaign workflow, meaning that you can retrieve only the Collection items relevant to that particular person and campaign. Add a Query Collection action When a person reaches this action, the campaign retrieves collection data and stores info based on your parameter. Then you can use this data in subsequent messages. Build a query to retrieve relevant info Select the query collection action, give it a name, and click Add Query to get started. Let’s build a query that retrieves course recommendations based on a person’s skill level. Choose a collection to query Select the Collection you want to query. Click Add query condition to filter for the courses you want to send a message about. For example, you could filter for all courses where the collection attribute level is equal to the person’s attribute skill_level. Decide how to store the results Under Store the query results, decide whether you want to use this information outside of the campaign or not: Choose Customer Attribute to store data on the person’s profile, accessible outside of the campaign. Choose Journey Attribute to store data temporarily during the journey; this data expires when the person exits the campaign. Keep the following limits in mind if you’re setting journey attributes: Overall limit: 100 journey attributes per journey. You can create or change up to 100 journey attributes during a single journey. Updates beyond this limit fail, and the person moves forward without the update. Attribute name limit: 128 bytes Attribute value limit: 100 KiB Then choose or create an attribute, like course_recommendations. We store data on this attribute as JSON. Preview the results Choose a person from the Sample data panel to preview the query results. Go to the Preview tab to see what the Collection rows would return for the sample user. Reference collection data with liquid Now that you’ve set up your query, you can use the collection data in a message! Reference profile attributes For collection data stored as Customer Attributes, reference them using the customer object: {% for item in customer.course_recommendations %} {{ item.name }} {% endfor %} If the data is an array, you can use liquid filters, such as first, last, and map, to get specific items or attributes. {% assign first_course = customer.course_recommendations | first %} If you limited your query to a single result, then the attribute value is an object, not an array. You can instead directly access the name and course description: {{ customer.promo_course.name }} {{ customer.promo_course.description }} Reference journey attributes For collection data stored as Journey Attributes, reference them using the journey object: {% for item in journey.course_recommendations %} {{ item.name }} {% endfor %} If the data is an array, you can use liquid filters, such as first, last, and map, to get specific items or attributes. {% assign first_course = journey.course_recommendations | first %} If you limited your query to a single result, then the attribute value is an object, not an array. You can instead directly access the name and course description: {{ journey.promo_course.name }} {{ journey.promo_course.description }} Collections API The endpoints for Collections are available via our App API: US region: https://api.customer.io/v1/ EU region: https://api-eu.customer.io/v1/ Creating a new Collection: POST:/v1/api/collections Create a new collection by providing both a name and either an array of JSON objects as data, or a url to download CSV or JSON data from. { "name": "collection name", "data": [ { row }, { row } ], } If you provide a URL, the data can either be JSON (array or newline delimited) or a CSV file. Ensure that you provide a Content-Type definition so that we know how to parse the data. If you don’t provide a Content-Type, we use the file extension. If you don’t include a content type or file extension, we assume the URL contains JSON. You cannot share a Google Sheet with us using this API. Once created, a Collection has the following JSON definition: "bytes": bytes_of_data_in_collection, "created_at": 1589471110, "updated_at": 1589471132 "id": 1, "name": collection_name "rows": number_of_rows_in_collection, "schema": ["name1", "name2", "name3" ], The schema will have a list of all attributes used by any items in the Collection, but remember that we don’t enforce that all items have this set of attributes. Retrieve info about a Collection you’ve already created GET:/v1/api/collections/:id Get the details of a collection with the given id. This does not include the content. Retrieve the content of a Collection you’ve already created GET:/v1/api/collections/:id/content Returns the entire content of the collection with the given id. Retrieve a list of all your Collections GET:/v1/api/collections Returns the details (but not content) for all of your Collections: {"collections": ["collection1", "collection2"]} Update the content of a Collection PUT:/v1/api/collections/:id/content Provide the content of a Collection as JSON-encoded text or CSV-encoded data. This will replace all the content for the Collection. For example: [ { "name":"Steak", "price":20, "quantity":10, "can_deliver":false } ] Update the name and content of a Collection PUT:/v1/api/collections/:id Update an existing collection. If name or data or url aren’t provided, the associated data is not updated. This will replace all content for the Collection. { "name": "collection name", "data": [ {"name":"Hot Dog","price":2,"quantity":1,"can_deliver":true}, {"name":"Pizza","price":10,"quantity":2,"can_deliver":true}, {"name":"Steak","price":20,"quantity":10,"can_deliver":false} ] } OR { "name": "collection name", "url": "https://example.com/sheet.csv" } Delete a Collection DELETE:/v1/api/collections/:id Deletes the collection with the given id. --- ## How segments work URL: https://docs.customer.io/journeys/segments/ Segments are groups of people that you organize by shared characteristics or behaviors. You can add people to segments automatically when they achieve certain criteria, or you can create manual, static segments. Overview Here’s an overview of segments in Customer.io: And here’s a more interactive look at creating a manual segment and data-driven segment: Segments are named groups of people who share characteristics or behaviors. A Segment can have many people and a person can belong to many segments. You can use segments as recipient lists, campaign triggers, filters, conversion criteria and more. Whenever you need to reference a subset of the people in your workspace, you’ll use a segment. For example, here is the criteria for one of the pre-made segments called, “Have not logged in recently”. If your integration sends us page viewEvents that track when people view pages on your website. You can track page view events using our JavaScript snippet or by sending requests directly to our API. events for people who have logged into your site, this segment contains people who have been in your workspace for at least 30 days but have not logged in recently. We’ll know they haven’t logged in because they haven’t logged in and viewed any pages in the past month. You can use a segment like this as a campaign trigger to send messages that incentivize people to return to your site! Types of segments You can create two different kinds of segments in Customer.io. The way people enter and exit your segments depends on the type of segment you create. Data-driven Segments: People enter and exit data-driven segments automatically when they match and stop matching the conditions that you set for the segment. These segments help you take action based on your audience’s real-time actions. You should use data-driven segments when you want Customer.io to move people in and out of your segments automatically based on the data you send us for each person.  Leverage AI to generate segments faster While you can create data-driven segments manually, you can also describe your segment or build a segment suggested by the agent to get up and running quicker or find missing conditions to improve your membership. Manual Segments: People enter and exit manual segments when you explicitly add them to, or remove them from, the segment. You might take advantage of manual segments when you want to move people in and out of segments based on business logic outside of Customer.io. You can add people to manual segments through a CSV upload, as part of a campaign workflow, or using our API. You can remove people from manual segments using the “Clear Segment” option, a campaign workflow action, or using our API. When to create a data-driven vs manual segment Customer.io shines best when you have an active data integration between your system and ours. This allows you to utilize attributes and behaviors of your end-users to set up automated messaging from our app. That is the power of Data-driven Segments, and for the most part this is the type of Segment you should be using. Manual Segments are available in instances where you need more hands-on control of the Segment, you are expecting limited changes or a data integration is not available to you. How we calculate segment membership We calculate segment membership when someone matches the segment conditions. For instance, let’s say you created a segment “Signed Up” with the condition “created_at is a timestamp.” After someone gains a created_at timestamp, they will become a member of “Signed Up.” The opposite is true, as well. That is, we calculate when someone is NOT in a segment when they do not meet the segment conditions. For instance, if you create a segment “Signed Up” with the condition “created_at is a timestamp” then create a person without a created_at timestamp, they will not belong to “Signed Up.” This means, we calculate “not in” segment membership when someone has left a segment AND when someone has never belonged to a segment. Phrased another way, a person can match trigger conditions, filter conditions, and wait until conditions based on “NOT IN: segment” criteria without having belonged to that segment in the past. So if you want someone to enter a campaign when they used to belong to a segment but no longer do, you’ll have to use more than “not in” segment conditions. The Segments page Go to the Segments page to manage your segments. By default, you’ll see a list of active segments, which means segments that you haven’t manually archived. You can see where and if your segments are in use under Usage. Filter for segments On the Segments page, you can use the search box to filter for text in the name or description of your segment or the id. Click Filters to filter by other data: Segment type: data-driven vs manual Usage: where the segment is used Object type: if the segment conditions reference custom objects Tag: if the segment uses one or more tags View archived segments You can archive a segment instead of deleting it to keep a history of the segment or just to make sure that nobody uses it temporarily. On the Segments page, click Archived segments. Then you can filter to find specific ones. View an individual segment From Segments, click a segment to manage its settings: Overview: For data-driven segments, you’ll see a collapsed view of the Segment’s conditions. For manual segments, you’ll see a list of CSV imports (if any). For all segments, you’ll see how many people are in the segment and a truncated view from the other tabs. People: Displays a count of segment membership over time Usage: Links to campaigns, newsletters, and other assets that use the segment. Alternately, you can create assets based on the segment from here. Ad Audiences A list of Ad Audiences that sync with this segment. Tag segments You can find segments by tag. Tags provide a way of grouping not just segments, but campaigns, broadcasts, and transactional messages together. Create, assign, edit, and delete as many tags as you need in your workspace. From the Segments page, click to open a segment. Edit the name to add or remove tags. Export a segment When a segment has one or more members, you can export a CSV file containing a list of the people in the segment. Go to Segments. Click the segment you want to export a list from. Click Export and choose Export People List or Export Change in Membership. You can find and download the CSV on the Exports page (under Configure data, click More). Archive or unarchive a segment You can archive a segment instead of deleting it to keep a history of the segment or just to make sure that nobody uses it temporarily. You cannot archive a segment that is currently in use. Check your segment’s Usage tab to see if it’s still used in your workspace. Then from your segment’s page, you can click and select Archive Segment. To unarchive a segment, find the segment in Archived segments. Then click and select Unarchive Segment. You can also archive or unarchive multiple segments from the Segments page by checking the box next to each segment then clicking Archive or Unarchive. Troubleshooting segments These are some common issues with segments, and tips to help you solve them. My attribute value is a collection. How do I create a Segment based on an item in that collection? Attribute values in Customer.io are only simple strings. For convenience, we do, however, make them available in message templates as JSON objects (if it is valid JSON) or as arrays (if it is a valid array) but for segmentation purposes, we do not currently distinguish them as being different data types such as arrays or dictionaries, etc. I edited my Segment conditions but People who match the new conditions did not enter a campaign that is triggered by my Segment. Learn more about editing live campaigns. I deleted a Segment but all the People are still in my account. Deleting a Segment will NOT delete the members of that Segment. Try recreating the Segment and then viewing the People who are in the Segment by clicking on the link in the People section of the Segment’s detail page where it says, “This segment contains # people”. You will be able to delete the People from there. I am trying to match values that contain a *, |, or + but I am not getting the results I expect. *, | and + are special operators that allow for simple, regex-like matching. When searching specifically for strings containing these characters, try placing a backslash (\) before the special operators that you want to find so that they will not be interpreted as regex operators. I am using regular expressions in my Segment Condition but it’s not working. Segment Conditions do not currently support the full set of regular expression rules. Try limiting your use to the few special operators we allow for string comparison (*, | and +). I cannot add a Person to my Manual Segment because they do not exist in my account. People must exist in your account before they can be added to a Manual Segment. Try adding that person to Customer.io and then you will be able to add them to a Manual Segment. --- ## Build segments with AI URL: https://docs.customer.io/journeys/segment-builder/ Building segment conditions to target the right audience can be hard, so we've made it possible for you to generate segments by leveraging AI! How it works Our agent uses publicly available data about your company and data from your workspace to suggest conditions for targeting specific audiences. Provide a brief prompt to generate segments that align with your goals. You can build a segment from any page using the agent. Click to get started. We prioritize your data privacy Our AI-powered segment suggestions are based solely on attribute names from your workspace, such as profile and event attributes, but never the values. We do not use any personally identifiable information (PII) to suggest or generate segments. For example, if you have a first_name attribute, we know the attribute exists but do not pass actual names through AI, a large language model (LLM), or any other service to help generate segments. Our system only processes attribute names so your audience’s personal information remains protected. Suggested conditions improve as you populate your workspace As your workspace grows and becomes more populated with data, the relevance of our AI-generated suggestions will increase. More data means suggestions will be more tailored and useful to your business. You can also improve the conditions generated by AI by adding descriptions to your attributes and events in the Data Index. Our segment builder takes these descriptions into account to better understand your data and provide more relevant segment conditions. Build a segment with AI To leverage AI to build a segment, use the agent: Click to get started from any page. From a segment page, click Describe segment, and then you can prompt the agent. Here are the steps for using the agent from a segment page: Go to Segments. Click Create Segment > Data-driven. Click Describe segment on the Overview tab. This opens the agent, which shows a few suggestions of segments that might be revelant to you. Describe your use case or ask the agent to create one of the suggested segments.  Add descriptions and tags to your attributes and events in the Data Index to improve segment generation! Providing additional context on your data helps our segment builder better understand your data and provide more relevant results. Review your conditions. Do they target attributes that are relevant to your goals? You can modify the conditions directly or ask the agent to revise them. If you’re not happy with the results, you can always start over with a new prompt. If you’re creating a segment outside of a segment page, the agent will share the link to the new segment so you can review it. Review our insights—People and Activity, to confirm you’re grouping the people you expect. Enter a segment name, if you haven’t already. Click Save Segment. Review our insights As you build your segment, we generate insights to help you understand the people in your segment. This can help you determine if you need to refine your conditions further. You’ll see two charts under People after you save your segment. People Under People, you’ll see the number of people who would join this segment once you create it and what percent of your workspace they represent. You can preview Sample members and click on them to see their data. After you save your segment, you can preview total membership and membership changes over a period of time. By default, we show the past 30 days: The Members chart shows total membership. Click Change to switch to viewing membership changes—count of people who entered or left the segment over time. Activity The subscribed metric shows the percent of people in this segment currently subscribed to your messages. If you use our subscription center, it’ll show the percent of people subscribed to at least one topic. Under Activity, you can review how people have engaged your messages in the past seven days and whether they’ve interacted with your product or marketing site. How we determine activity We determine whether people have viewed your product or marketing site based on page and screen events. If you link people to a website containing our JavaScript snippet, or a mobile app using one of our SDKs, we typically capture page and screen events automatically. Otherwise, you can send these events using a number of other integrations. --- ## Data-driven segments URL: https://docs.customer.io/journeys/data-driven-segments/ Data-driven segments are groups of people that you automatically populate when they meet certain conditions. People enter the segment when they match the conditions and leave the segment when they stop matching the conditions. These kinds of segments help you group people according to real-time activities. Create a data-driven segment This process explains how to create a segment on the Segments page, but can also create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. when you create a campaign (using the They meet conditions option). The same basic steps apply there as well. If you create a new segment when working on a campaign, you may want to check your segment before you start the campaign just to make sure that your conditions match the right people for your campaign. Go to Segments. Click Create Segment. Click Data-driven. Click Untitled at the top to enter a Name and Description for the segment. These help you find the segment—both in the list of segments, and when selecting the segment in a campaign, broadcast, etc. Leverage AI to build your segment or manually add the conditions that people must meet to join the segment. Click Save Changes. When you save a data-driven segment, it takes a bit of time to build and determine the initial membership in the segment. The more people in your workspace and the more complex your segment conditions, the longer it takes to build the initial segment membership. You can monitor the build process by clicking in the upper-right corner and going to your workspace’s Tasks list. Segment conditions When you create Data-driven Segments, you’ll build a set of conditions determining membership in the segment—like whether a person has received a specific message or has a certain attribute. People matching the conditions enter the segment, and they leave the segment when they no longer match those same conditions. Data you can use in segment criteria You can add segment conditions that include people’s data or messaging data. People data: AttributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. Events Relationships Forms that people have filled out Page EventsEvents that track when people view pages on your website. You can track page view events using our JavaScript snippet or by sending requests directly to our API. Devices Screens Segments Messaging data: Emails Slack messages Twilio SMS Webhooks In-App messages How to join conditions When you create a segment, you start by specifying how to join conditions—All or At least one. All represents a boolean and—where all conditions must be true for a person to enter the segment. At least one is a boolean or, requiring only one of your conditions to be true to enter the segment. You can also nest conditions using the Group option. This lets you create complex segmentation logic like condition1 AND (condition2 OR condition3). There is a handle on the left side of each condition or group that you can use to drag and rearrange conditions. After you create your segmentation conditions, use the handle to move conditions into nested groups that fit your specific logic. Using AND and OR conditions in a segment When you create a segment, you determine whether a person needs to match ALL conditions or At least one condition. These reflect AND and OR logic respectively. You can use the Group condition to add additional layers of AND and OR logic within your segment: items within the group use the same AND/OR logic. Here’s a walkthrough of using these conditions in a segment: While you select the logic when you add a group, new groups add another drop-down that you can use to change logic within the group. Note that groups only display their logic (the “AND” and “OR” bubbles to the left of the group) after you add at least two conditions, because you’ll need at least two conditions before there’s any logical difference between AND and OR. For example, the segment below contains people who have performed the purchase event, and are either new customers (created within the last 30 days) or spent a minimum dollar amount. Using JSON arrays in segments Attributes and events can contain complex JSON—arrays, objects, and arrays of objects. You can use JSON notation to access JSON in your segment. However, when you access an array, you can provide an index to access a specific item in the array or leave the index blank to match any item in the array. For example, if you store a person’s shipping addresses in an array of objects call shipping_addresses, you could match the city in the first shipping address using shipping_addresses[0].city. If you want to match a city value against any object in the array, you could use shipping_addresses[].city. If you want to match a value against any property in any value or object in the array, you could simply use shipping_addresses[]. When referencing an array of values—not a property belonging to an object in an array, but the array itself (array[])—the equals evaluator works like a contains operator. For example, if you have an attribute called favorite_foods containing the values pizza, fries, burgers, trying to create a segment where favorite_foods[] equals pizza would evaluate to true. Segments with special characters Event names and other data you may want to create segments from can include special characters. In most cases, we’ll treat special characters as literal characters: & is &. However, the following special characters perform Regular Expression (RegEx) functions that look for patterns of characters. To treat them as characters, not search functions, you can escape them with \ (for example, \+1). Otherwise, this is how they work: Special character RegEx function * This is a wild card: it represents any character. For example, s*n matches event names “sink”, “sun”, and “lesson”. If you don’t place any characters after *, it’ll match the string up to *. For example, si* matches “reside” and “sink”. + This represents “and”: it matches both values in a statement. You can use it with attribute conditions and event conditions, but not event names. For example, if you wanted to find a person whose email address contains person and gmail, you could write email contains person+gmail. A few emails this would match on include person@gmail.com, person+test@gmail.com, or person2@gmail.com because these addresses contain both person and gmail. | This represents “or”: it lets a person who has performed (or not performed) either event to enter the segment. For example, a segment based on hide|seek events lets a person who has performed either the hide or seek events to enter the segment.  Use the conditions with contain to use RegEx To use regular expressions, you need to use the “contains” or “does not contain” conditions. Using “equals” or “does not equal” will yield no results. Segments based on object relationships You can relate people to objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.—things like companies, accounts, or online courses. Then you can base segments on these relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins.. This provides a way to trigger a campaign for everybody at a company or in certain courses. When you create a segment, use the Relationship option to target people based on their relationship to one or more objects. Then you can refine the condition with object or relationship attributes. Image 1: Passed any calculus class In the image above, people join the segment when they have at least one relationship to a calculus course they’ve passed. If someone took three calculus courses and passed one but failed the others, they are a member of this segment. Now if you added two separate relationship conditions, rather than refining the one, we’d evaluate the segment differently. Image 2: In any calculus class AND passed any course In this example, people join the segment when they’re related to any calculus course AND related to any course they’ve passed. If someone passed an english class but failed a calculus class, they’ll still join the segment.  If you want to evaluate object and relationship attributes for a single object, refine the relationship, don’t separate the conditions.  To trigger a campaign based on every relationship change, create relationship-triggered campaigns instead. Relationship-based segments trigger a campaign for the first relationship that matches, NOT all relationships that match. If that does not suit your use case, consider setting up a relationship-triggered campaign instead. Segments based on messages You can create a segment of people who have engaged with emails, Slack messages, Twilio SMS, webhooks, and in-app messages. When you create your segment, you’ll select the message type then define your criteria. For instance, if you chose Email, you can then include any email in your workspace or search for a specific email to use. Then you define the behavior you want to target. Here it’s “Onboarding email 1 has ever been opened by a human.” Note that last part: opened by a human. We track human and machine metrics for email opens and clicks. When creating a segment that includes this data, you choose whether to target any data (human and machine) or only data attributed to people.  Segmenting on link clicks containing specific URL data If you segment on a Message with Has ever been clicked on tracked link containing someUrlData, your segment will only contain profiles that clicked that link after August 9, 2025. This is when we began capturing URL data from links people click. Bounce-based conditions The has ever bounced condition for segments includes both hard and soft bounces: Hard bounces: Permanent delivery failures (invalid email addresses, non-existent domains) Soft bounces: Temporary delivery failures (full mailbox, server issues, message too large) If you see people included in your segment that you didn’t expect, they may have had a soft bounce at some point, which still qualifies them for the has ever bounced condition. Segments based on segments When creating a segment, you can include other segments as conditions. This helps you reuse the same conditions across multiple segments to save time building them. For instance, consider who your foundational audience is, like people who are subscribed and active customers. You can create a segment for this group and add it, instead of rewriting all the conditions, to another segment. When creating a new segment, click the Overview tab. Under People, click Segment and decide whether people should be IN or NOT IN a segment you specify. When editing a segment, click Edit then click Add condition or group. Choose Segment from the menu and decide whether people should be IN or NOT IN a segment you specify.  Not seeing the option to add segment conditions? You can only add segments as conditions if the segment you want to use as a condition is not: Referenced in another segment. For instance, if you already added the segment “Signed up” to another segment’s conditions, then you can’t add segment conditions to “Signed up.” Used as a trigger for a legacy segment-triggered campaign. Learn more about identifying this trigger type. Using timestamps You can create data-driven segments based on timestamps, but we evaluate match time for segments with timestamps in a different manner from other segments. A match time is when someone enters a segment. For most segment conditions, this is when someone gains an attribute, loses an attribute, performs an event, and so on. But for segments with timestamp conditions, we can actually calculate the match time based on a date-time, so the match time might not always be, for instance, when someone gains a timestamp attribute. To help explain this, you could have a segment condition where a profile attribute birthday exists. People would join this segment as soon as you recorded their birthday attribute. But if your segment is birthday is a timestamp 15 days from now, people will only join your segment when they meet that condition. Here’s a brief walkthrough of using timestamps in segments: For more information on implementing timestamp conditions in a segment, go to Timestamp Conditions. Segment-triggered campaigns with past timestamps  Customer.io typically does not trigger campaigns based on historical data Customer.io, as a practice, does not trigger campaigns based on backfilled or historical data. (The exception is with backfilled events.) For segment-triggered campaigns, this means you can create a profile with a past timestamp that causes the person to join a segment, but that person will not trigger a campaign based on that same segment. For example, assume you created a data-driven segment with the following condition: last_online is a timestamp before 30 days ago. You use this segment to trigger a new campaign. On March 30, 2023, you add a new person with the attribute last_online: 1669965166 (Dec 2, 2022 at 12:12:46 AM). This profile would enter the segment, but the match time would be Jan 1, 2023 at 12:12:46 AM, 30 days after the person’s timestamp, which is the first day the segment condition is true. While this person would enter the segment, they would not trigger a campaign on March 30, 2023, since the match time is in the past. For more information on implementing past timestamp conditions in a segment, go to Timestamp Conditions. Segment-triggered campaigns with future timestamps  Try out date-triggered campaigns instead If you trigger campaigns using a timestamp-based segment, you may want to try date-triggered campaigns instead. With date-triggered campaigns, people won’t trigger a campaign until the time specified in the trigger conditions. This makes it easier to predict when people will start and finish journeys. Remember that, for segments with a condition that starts with a timestamp, the match time is the time when the condition is true, not necessarily when the person joined the segment. This creates some caveats for segment-triggered campaigns based on future timestamps. In this scenario, a person will trigger the campaign, but not start a journey, until the timestamp. This ensures that people don’t inadvertently experience the journey before it’s relevant to them. For example, imagine that you have a segment called “Signed up” with a single condition: joined_club is a timestamp. On January 1, 2023, you add a person whose joined_club value is a timestamp for March 17, 2028. This person joins the “Signed up” segment because their joined_club attribute is a timestamp. If you use the “Signed up” segment as a campaign trigger, this person will generate a campaign journey, but they won’t start the journey until March 17, 2028—the date and time value for their joined_club attribute—because that’s when the person officially joins the club. You can find journeys with future start-times on the Campaign’s Journeys tab or under an individual person’s Journeys tab with a Started time in the future. If you want people to start their journey based on future timestamp values, you may want to try out date-triggered campaigns instead. But, if you need to use a segment-triggered campaign based on a future date, you can set the campaign’s trigger conditions to include dates and times within a range of the present (the is a timestamp between or after conditions). This lets people with a timestamp (like our joined_club example) start their journey even when they have a future value. Edit a segment You can edit the name, description, and conditions for your segment. Changing a data-driven segment’s conditions will cause the segment to rebuild and may affect active campaigns. Go to Segments. Click the segment you want to update. Click the pencil icon to edit the name or description of the segment. Click Describe changes to leverage AI to improve the segment criteria. Click Edit to manually edit the conditions driving membership in the segment. Delete a segment Go to Segments. Click the segment that you want to delete. Click and choose Delete Segment. You can also select one or more segments on the Segments page then click Delete. If one of your workflows depends on the segment, we won’t let you delete the segment so your campaigns and other actions don’t break. Find where your segment’s referenced under Usage and make sure it’s removed from all locations: A campaign, unsent newsletter, or API-triggered broadcast: your segment could be part of the trigger, recipient conditions, or other workflow conditions A form A SQL import Another active segment (Active means it’s not archived.) Duplicate a segment You might want to copy a data-driven segment as a starting place for the conditions in a new segment. Go to Segments. Click the segment that you want to duplicate. Click the options icon , then choose Duplicate Segment and confirm your choice. --- ## Manual Segments URL: https://docs.customer.io/journeys/manual-segments/ Manual segments let you group people according to business logic outside of Customer.io. As the name suggests, you'll [add people](#add-people-to-a-manual-segment) to, and [remove people](#remove-people-from-a-manual-segment) from, these segments manually. Create a manual segment You have the opportunity to create manual segments when you import people by CSV and import people from a database. Otherwise, you can create a manual segment first, and then add people to it later. Go to Segments. Click Create Segment. Enter a Name and Description for the segment. These help you find the segment—both in the list of segments, and when selecting the segment in a campaign, broadcast, etc. Choose Manual. From here, you can add people to your segment. Add people to a manual segment People must be identified in Customer.io before you can add them to a manual segment. You can add people to manual segments via: CSV a workflow action in a campaign our Track API Data-in integrations the segment tab on a person’s profile page a shortcut Via CSV You can upload a CSV to add people to a manual segment from the Segments page. The CSV must contain identifiers (IDs or email addresses) for people you’ve already identified. If a person’s identifier in the CSV doesn’t match an existing profile, we won’t create a new profile and we won’t add them to the segment. You can identify people by uploading a CSV. You may want to do this to make sure people exist before you try to add them to a manual segment. Go to Segments. Click an existing manual segment or add a new manual segment. Click Add by CSV. Determine the identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. you want to use to match people from your CSV to your workspace: By id or By email. Choose the file you want to upload or import a Google sheet. If you import a google sheet, you must grant Customer.io access to sheets in your Google account. Tell us which column holds the ID or email—whichever identifier you selected in earlier steps. If your file contains no errors, click Add People. We validate data when you select your identifiers. If there are errors, you can download an error file (if any) or download the import file. Using a workflow action You can add people to a manual segment as a part of a campaign workflow. Drag a Manual Segment Update action into your workflow. Click the action item and edit the settings on the panel. Update Name - you can update the name of the action visible in your workflow. What should happen when a person reaches this step? - choose whether to add or remove people Manual Segment - select the manual segment you want to change. Conditions - specify any conditions people should meet to be added or removed from the segment. Click Save. From their profile page You can add a single person to a manual segment from their profile page. Go to People. Filter for the person you want to add to a manual segment then click on them. Click the Segments tab. Click the Doesn’t Belong To tab. To the right of any manual segment they don’t belong to, you’ll see + Add to segment. Click this to immediately add them to a specific segment. Accidentally add someone to a manual segment? You can also remove them just as easily. Using a shortcut You can add a person to a manual segment using a shortcut. Go to Webhook shortcuts for more information. Remove people from a manual segment You can remove people from manual segments through: “Clear Segment” on the segment’s page a workflow action in a campaign our Track API a shortcut Clear segment Click your manual segment from the Segments page. Click Options in the top right then Clear Segment from the dropdown. A confirmation modal will appear. If this manual segment is used in campaigns or broadcasts, decide how we should process active journeys: By default, Update campaign journeys is set to No. People with active journeys will show as not belonging to the manual segment, but they will neither leave delays with the condition of “not in: this manual segment” nor trigger campaigns with this condition. They will, however, leave this campaign if the exit condition is “They stop matching the trigger or filter conditions.” For instance, assume Janet is cleared from the manual segment Conference Leads which triggered a campaign that she now has an active journey in. People are set to exit the campaign when they stop matching the trigger condition. If Update campaign journeys was left as No in the confirmation modal, she would still leave the campaign because of the exit condition. She would not trigger a campaign based on “not in: Conference Leads,” though. Toggle Update campaign journeys to Yes if people with active journeys should leave delays with the condition of “not in: this manual segment” or trigger campaigns with this condition. They will continue to follow the exit conditions of the current campaign. For instance, assume Janet is cleared from the manual segment Conference Leads which triggered a campaign that she now has an active journey in. People are set to exit the campaign when they stop matching the trigger condition. If Update campaign journeys was set to Yes in the confirmation modal, she would still leave the campaign because of the exit condition. She could also, for instance, trigger a campaign based on “in: Signed up” and “not in: Conference Leads.” Click Yes, clear segment to confirm your choice. Using a workflow action You can remove people from a manual segment as a part of a campaign workflow. Drag a Manual Segment Update action into your workflow. Click it and edit the settings on the panel. Update Name - you can update the name of the action visible in your workflow. What should happen when a person reaches this step? - choose whether to add or remove people Manual Segment - select the manual segment you want to change. Conditions - specify any conditions people should meet to be added or removed from the segment. Click Save. From their profile page You can remove a single person from a manual segment from their profile page. Go to People. Filter for the person you want to remove from a manual segment then click on them. Click the Segments tab. Click the Belongs To tab. To the right of any manual segment they belong to, you’ll see Remove from segment. Click this to immediately remove them from a specific segment. Accidentally remove someone from a manual segment? You can also add them just as easily. Note, they will not re-enter campaigns triggered by being in this manual segment if the campaigns had a frequency of “one time.” Using a shortcut You can remove a person from a manual segment using a shortcut. Go to Webhook shortcuts for more information. Edit a segment You can edit the name and description of a manual segment from the segment’s details page. You cannot affect membership in the segment except by a CSV import from this page. Go to Segments. Click the segment you want to update. Click to edit the name and/or description for the segment. Delete a segment Go to Segments. Click the segment that you want to delete. Click and choose Delete Segment. You can also select one or more segments on the Segments page then click Delete. If one of your workflows depends on the segment, we won’t let you delete the segment so your campaigns and other actions don’t break. Find where your segment’s referenced under Usage and make sure it’s removed from all locations: A campaign, unsent newsletter, or API-triggered broadcast: your segment could be part of the trigger, recipient conditions, or other workflow conditions A form A SQL import Another active segment (Active means it’s not archived.) --- ## Segment mobile device audiences URL: https://docs.customer.io/journeys/device-segments/ Devices have their own attributes, and you can segment your audience based on their devices' `platform`, `last_used`, and `last_status` attributes to target or exclude specific people from a campaign based on their devices. How it works A person can have multiple devices. Each device has its own attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes to create a segment of people whose devices match your criteria. Remember that your segment matches people. So, if one a person’s two devices matches your segment criteria, that person—and both of their devices—enter your segment. You can limit push notifications to the most recent device when you compose a notification using the Last used device option. Find device attributes To find a person’s devices and their device’s attributes, go to the People page. Select a person, go to the Devices tab, and select the device you want to see attributes for. Segment by platform You can segment people based on their device platform—iOS or Android. You might do this if you want to notify people about an update to your app on a specific platform. Segment by last status The last_status attribute shows the delivery outcome last message—sent, bounced, suppressed, or unused. Unused means that the last status isn’t one of the previous three, but it’s commonly seen when a device is new and hasn’t yet been sent a push notification. You can use this status to target people who’ve been receptive to previous messages or who haven’t received a push notification from you before. Segment by last used The last_used attribute contains the timestamp when the device was last identified. You can use it to find devices within a recent time frame, or between two dates. --- ## Ad Audiences URL: https://docs.customer.io/journeys/ad-audiences-sync/ In ad networks like Google, Facebook, and Instagram Ads, you can target your ads by importing a list of known users. Customer.io lets you do this programmatically by sending segments into ad networks as audiences. This eliminates the manual process of importing custom lists and ensures parity between the people you communicate with in Customer.io and your Ad Network. What are Ad Audiences? In Ad Networks, such as Google, Facebook, and Instagram Ads, you can target your advertising in several ways. One way is to import a list of known users (“known” meaning you have some personally identifiable information on them - such as an email address) and use that list for targeting. Now, you can programmatically send your Customer.io segments into Ad Networks as audiences. This eliminates the historically manual process of importing custom lists - as well as ensure parity in the people you reach in Customer.io and your Ad Network. To learn more about audiences, check out each platform’s specific documentation: Google Ads, Facebook Ads.  We don’t send plain text values to your ad network We hash data (sha256) before we send it to your ad networks. This ensures that we send and save data securely when syncing data with your ad network. Connecting your Ad Accounts to Customer.io To connect an Ad Network, go to the Integrations page within Customer.io and scroll down to the ‘Advertising’ section. Connecting to Google Ads Before you sync segments to Google Ads audiences, you must ensure that your audience has granted consent for you to collect and use their data to personalize ads. See Google Ads Consent for more information. Click Get Started next to Google Ads. Click Sign in and log into a Google account that has either standard or admin access to your Google Ads account. Grant Customer.io Audience Sync access to Manage your Adwords Campaigns. This lets us sync ad audiences to your Google Ads account. You’re done! Now you can sync your segments as audiences to Google Ads. Google Ads Consent As of March 6, 2024, Google Ads requires your audience’s consent to collect user data and personalize ads in conformance with the Digital Markets Act in the European Economic Area (EEA). Google Ads reflects these two types of consent as: User Data: Consent to collect and use user data to personalize ads. Ad Personalization: Consent to personalize ads based on user data. As of March 6, 2024, syncs lacking consent will have a significantly worse match rate within Google. To improve this, your next steps depend on whether you need to follow these regulations. Syncs active before March 6, 2024 For syncs that must meet EEA regulations If your customers are in the EEA or you follow EEA regulations, then your Google Ads syncs should only include users who have confirmed consent for you to collect and use their data. Your audience needs to grant both types of consent before you add them to a segment that syncs to Google Ads. To do this, you may need to collect attributes representing your audience’s consent and use those as conditions in your segments. For instance, you could set an attribute consent equal to true on people’s profiles then add this as a condition on segments that sync with Google Ads. Otherwise, you may need to make it explicit in your terms of service and other declarations that using your service entails Google Ads consent. Then you can proceed to confirm consent on your Google Ads syncs that were active before March 6, 2024, when this new requirement went into effect. For syncs that don’t need to meet EEA regulations If your customers are not in the EEA and you don’t follow EEA regulations, then your Google Ads syncs do not have to follow the Digital Markets Act. If you anticipate you will do business in the EEA, we recommend you follow the steps above. Otherwise, you can continue to confirm consent on your syncs without gathering explicit consent. Confirm consent Click here to see a list of your Google Ads syncs. Click the box in the top-left corner to select all your Google Ad sync segments. Click Bulk confirm consent. Check the box in the modal then click Confirm consent.  We do not remove people from your existing ad audiences while consent is not confirmed. Rather, we prevent your segment from sending new people to Google Ads. You can also confirm consent from a segment’s Ad Audiences tab. Click Confirm consent in the banner message, check the box related to the Digital Markets Act, then confirm your action. New Syncs Moving forward, you must confirm consent for ALL new Google Ads syncs, even if you do not have users in the EEA. By checking the box, you accept the responsibility that (1) you have confirmed consent from all members of your audience or (2) you have confirmed you do not need to gather consent. Connecting to Facebook Ads Click Facebooks Ads from the Integrations catalog. Click Connect your Facebook Account. You will be prompted to log into Facebook. Log in through an account that has access to Facebook Business Manager. Accept Facebook’s Custom Audience Terms of Service. Note that these terms must be accepted per user per business. Once you’ve logged in and accepted the terms of service, you’ll be redirected back to Customer.io and you’re all set! Once you’ve connected any Ad Networks account to Customer.io, you’re ready to sync your segments as audiences to Facebook Ads. Tip: Start with a segment of current customers to use as an exclusion audience! Connecting to Instagram Ads Instagram Ads are enabled in your Facebook Ad Manager. Click the ‘Get Started’ button next to Facebook Ads. Click ‘Sign in‘ and you will be prompted to log into Facebook. Log in through an account that has access to Facebook Business Manager. Once you’ve logged in, you can define where you would like to show your ads. Navigate to ‘Placements’ and click “Edit Placements”. Select ‘Instagram’, and you’re done! Now you can sync your segments as audiences to Instagram Ads. Connecting to YouTube Ads YouTube Ads are enabled in your Google Ad Manager. Click the ‘Get Started’ button next to Google Ads. Click ‘Sign in‘ and follow the Google Authentication prompts. Log into an account that has either standard or admin access to your Google Ads account. Next, you’ll be asked to grant Customer.io Audience Sync access to Manage your Adwords Campaigns. This access will allow audience sync to occur. Once you’ve created a Customer Match audience for YouTube, then you’re done! Now you can sync your segments as audiences to YouTube Ads. Syncing your Segments to Ad Networks To sync a segment to an Ad Network, navigate to the segment, find the Ad Audiences tab and click Create Ad Audience.  No more than one audience per platform You can only sync to one Meta audience and one Google audience at a time. If you try to sync to multiple audiences on the same platform, you’ll get an error that the audience already exists. From there: Choose the Ad Network you want to sync the segment to. Name your ad audience. We recommend adding “Customer.io” in the name to keep your audiences organized in the Ad Network. Create a description for your audience. We recommend including the segment’s URL to easily navigate back from the Ad Network. You’re done! When you click create, Customer.io will create a new audience in the Ad Network, and then add any existing people in the segment to the audience. After that, the audience is automatically synchronized with the segment. We sync at hourly intervals for most ad networks; the Google Ads sync interval is 24 hours. Sync errors If you run into errors, try updating the permissions of your audience within your Ad account. Audience already exists error: This error occurs when you try to sync multiple audiences on the same platform (Meta or Google) from Customer.io. You can only have one active sync per ad platform. To sync to a different audience, you need to delete your current sync for that platform and then create a new one. Facebook permission errors For Facebook, go to Ads Manager > Audiences. If you get permission errors, it’s likely that users in your Facebook Business Manager account haven’t accepted the Terms of Service (ToS) for the business. Each user and each business must accept the ToS at the user, business, and ad account levels: User level: Visit https://www.facebook.com/ads/manage/customaudiences/tos.php to validate that you’ve accepted the rules. Business level: Visit https://business.facebook.com/ads/manage/customaudiences/tos/?business_id={BUSINESS_ID} to validate your acceptance of the rules at the business level. Replace {BUSINESS_ID} with your actual business ID. Ad account level: For each ad account you want to use, you’ll have to make sure that their terms of service are also accepted. To do this, visit the custom audiences URL for your ad account: https://www.facebook.com/ads/manage/customaudiences/tos/?act={ACCOUNT_ID} where {ACCOUNT_ID} is your ad account ID (act_xxxx format). Audience Matching Audience match relies on the email and mobile_ad_id attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. People in your audience only need one of these attributes (email or mobile_ad_id) for the match to work, though adding both increases the chances of success. The mobile_ad_id attribute supports both Apple’s Advertising Identifier (IDFA) and Android’s Advertising ID (AAID). We do not send plain text values to your ad network. We securely hash email or mobile_ad_id values for profiles in the segment before we send data to your Ad Network. These are the only values we send to your ad network. If you want to secure your data yourself, you can hash your customer email and/or mobile_ad_id yourself using the SHA256 algorithm and save them as email_sha256 and mobile_ad_id_sha256, respectively. If profiles have these attributes, we’ll use them over the hashes we generate from the emailand mobile_ad_id profile attributes. Initial Processing Time On initially syncing a segment to an Ad Network, Google and Facebook both state a 24 hour processing period before the audience is ready to be used. In those platforms, you will see the audience created shortly after enabling the sync. Be aware, that even when the “Populating…” callout goes away, the Ad Network might still be processing - please give an extra hour to confirm the size of the audience created. Using Audiences in Ad Networks Once the audience has been created in the Ad Network, you can use it in new or existing ad campaigns. Do not delete the audience in the Ad Network - as that will stop the sync for that audience from running going forward. You can update the name, description, and settings of the audience in the Ad Network’s platform without affecting the sync. In Google, you can use this audience on the Search network, Youtube network, or Gmail network (due to limits set by Google, you cannot use this kind of audience on the Display Network). Managing your Ad Audiences Once you’ve connected an Ad Account, you’ll see an Ad Audiences column on the Segments page indicating any synchronized segments. Select the Ad Audiences dropdown to filter your segments by Ad Networks and/or sync statuses. To view more detail about a synchronized segment, click on the segment and navigate to the Ad Audiences tab. For any Ad Audience, you’ll see the name, description, the number of people synced, when the segment was last synced, and the sync status. Here’s a breakdown of all sync statuses: Synced: The segment successfully synced to the Ad Network. Actively syncing: The segment is actively syncing to the Ad Network. Queued to sync: The segment is queued to sync and will be synced to the Ad Network soon. Paused sync: The sync is paused and the segment will not automatically sync to the Ad Network. Failed sync: The segment failed to sync to the Ad Network. In some cases, the segment will have an error syncing and will re-try to sync in an hour. In other cases, when the audience is deleted in the Ad Network for example, the segment will fail to sync to the ad network. In those cases, a new Ad Audience will have to be created to re-sync the segment. You can pause, resume, and delete any Ad Audiences. From the Ad Audience tab, click the ‘Manage‘ button for an Ad Audience. From there, you can: Pause your Ad Audience, which will stop it from syncing to the Ad Network until you resume the sync. Resume your Ad Audience, which will enable automatic syncing to the Ad Network. Delete your Ad Audience, which will permanently delete it from Customer.io. The audience will remain in the Ad Network, but it will no longer be synchronized. You can always re-sync this segment later by creating a new Ad Audience. Why Your Segment Size in Customer.io Will Be Different Than Your Audience Size in the Ad Network When syncing segments to audiences in Ad Networks, the Ad Networks try to match a given key (listed above) with the same key in their systems. Unfortunately, the Ad Network will not be able to match every single record it receives - in our internal testing, we’ve seen about a 50% match rate on Google and on Facebook. For Google, here is more information about custom match rates and list size. For Facebook, here’s more info on custom audiences from lists. Google is doing away with cookie-based ad targeting. How will this affect Ad Audience Sync? Ad Audience Sync will not be affected by changes to Google’s third-party cookie policy. Ad Audience Sync uses Google’s Customer Match product. Customer Match lets advertisers target their known users based on personally identifiable information. As Google removes support for ad targeting based on cookies, Customer Match will be the only option for advertisers who want to target their known users. Customer.io’s Ad Audience Sync helps you avoid the manual work of uploading CSVs directly into Google’s Customer Match product by automatically creating and updating audiences in your ad account. Disconnecting and Reconnecting to your Ad Network When you need to reconnect your Ad Network, please perform all of the following steps: Disconnect the ad network account using the Disconnect button on the network’s integration settings page Reconnect the ad network account Sync your ad audiences again Wrap up Are there Ad Networks you’d like to see? For example, we’re considering Twitter, Snapchat, Linkedin, Quora. If any of these or other Ad Networks would be valuable to you, please let us know! That’s it! As always, if you have any questions or if this documentation could have been made any clearer, please let us know. --- ## Timestamp Conditions URL: https://docs.customer.io/journeys/segmentation-and-timestamp-rules/ Segments include a variety of timestamp-based rules. This page describes those rules and helps you take advantage of them in segments. When you created your Customer.io account, you likely saw a default segment called “Signed up” that uses the rule created_at is a timestamp. When you build your own segment, we have a variety of timestamp-based rules: This document will give some guidelines for how and when to use each. What does “is a timestamp” even mean? is a timestamp will evaluate to true as soon as the value is a valid UNIX timestamp. However, when you use a segment with an is a timestamp condition to trigger a campaign, a person won’t begin the campaign until the timestamp’s date and time. This means a person can be a member of the segment, but not enter a campaign. This only applies to setting an attribute equal to is a timestamp, not relative conditions like is a timestamp before 3 days from now. Timestamp-based rules are best used when you want to send a message on a certain date or X days after a certain date (by using delays). Example If the segment condition is created_at is a timestamp, then someone will enter the segment when you set their created_at attribute to a timestamp, like 1917049881 (Tue Oct 01, 2030 01:51:21pm GMT+0000). This person would not start a campaign triggered by membership to this segment until Tue Oct 01, 2030 01:51:21pm GMT+0000, the date and time of their created_at attribute; though they will log a journey. Why are all signed-up users in this segment? A user who enters a segment using only the rule created_at is a timestamp rule will never exit, so the segment itself contains all the users who matched in the past. This just checks that the value is a timestamp. Example I want my campaign to send messages to all users 7 days after they sign up, as long as they’re under 25 and they like pop music. Here’s how that might work: Trigger segment: created_at is a timestamp Filter segments: segment with the rule age is less than 25 AND segment with the rule favorite_music_type is equal to pop Delay: 7 days Messages How this works When users sign up, they are added to the campaign and the clock starts ticking. Once the delay passes, people are checked against the filter criteria (Is the user age under 25? Is pop their favorite music type?). If the filters match, the users belong to those two segments 7 days after signing up, and the message goes out. If not, the users skip the message or leave the campaign. Why not just add all the rules in one segment? Each rule inside a segment has a particular matching time and the overall matching time of the segment is dictated by the last rule matching. If we’d made the above example a trigger segment: created_at is a timestamp AND age is less than 25 AND favorite_music is equal to pop The user would enter the segment when the last rule of the segment matches. If the users adds their age and their favorite music at the time the account is created all at once, then this might work. However, if someone signs up, but then takes seven days to add their age and/or music type, that’s when the delay will begin. In that case, a user might receive the messages weeks or months after they sign up, depending on when the other attributes are added. Why are there 2 created at timestamps? You might see two different timestamps when you try to pick out attributes: created_at and _created_in_customerio_at. We maintain these two timestamps so you can differentiate between when you added a person to Customer.io, and when you first recorded a person outside of Customer.io. _created_in_customerio_at is the date-time when you added a person to Customer.io. We assign this value automatically, and it’s immutable. created_at represents the date-time when you first recognized a person—which might happen outside of Customer.io. These values will be different if you create people records outside of Customer.io and then import people from a CRM, upload a CSV, use a database integration, etc. These values will probably be the same if you add people directly to Customer.io, like when you use our JavaScript snippet, our SDKs, etc. Other timestamp rules and their interpretations Here’s a brief walkthrough of using timestamps in segments: Using relative dates: 1. is a timestamp after X days from now is a timestamp after a relative date of X days from now matches users whose date is at least X days in the future. For example, a segment using the rule delivery_date is a timestamp after a relative date of 3 days from now. If we assume that “now” is March 1, 2016, this segment will match users whose delivery_date is a timestamp corresponding to three days after March 1. As soon as it’s less than three days from “now”, they will exit. 2. is a timestamp after X days ago is a timestamp after a relative date of X days ago will match users whose date is within the past X days. This type of segment can be helpful if you want to create a newsletter and only send it to users who signed up in the past 7 days to let them know about a special promotion or warn them about a bug. For example, the segment created_at is a timestamp after a relative date of 7 days ago will include users whose accounts are less than 7 days old. If today is October 8, 2016, the segment will match users who created their account from October 1, 2016 until now. They will exit the segment as soon as more than seven days passes. 3. is a timestamp before X days from now is a timestamp before a relative date of X days from now matches users X days before this date. This type of segment is often used for billing-related campaigns. For example, If you want to send a payment reminder 7 days before next_payment_date you’ll want to use the following trigger segment rule: next_payment_date is a timestamp before a relative date of 7 days from now Users will receive your message exactly 7 days before their next_payment_date and if you want to add multiple reminders, you just need to add a couple more messages to your workflow: Message 1: No delay (sends 7 days before next_payment_date) Message 2: 4 days (sends 3 days before next_payment_date) Message 3: 7 days (sends on next_payment_date) Users will never exit segments of this type, so if you want to automatically send payment reminders every month, you’ll want to make sure that you set your campaign’s Frequency setting to let people re-enter the campaign every month. 4. is a timestamp before X days ago This type of segment can be used if you want to send messages to older users. is a timestamp before a relative date of X days ago matches users whose date is more than X days in the past For example, if “now” is April 28, 2016 and we set up a segment with the rule join_date is a timestamp before 8 days ago, users with a join_date before April 20, 2016 will match. Using specific dates: 1. is a timestamp before X date is a timestamp before a specific date of X will match users whose date is before X. This condition can be used if you want to send messages to users for whom an attribute is a timestamp before a date. For example, say we want to send a message to people who signed up for our app before December 1. This is how we would do that: This segment includes all the users who signed up before the timestamp 1575158400: December 1, 2019, 12:00:00 GMT. 2. is a timestamp after X date is a timestamp after a specific date of X will match users whose date is after X. This condition can be used if you want to send messages to users for whom an attribute is a timestamp after a date. For example, say we want to send a message to people who signed up for our app after December 1. This is how we would do that: This segment includes all the users who signed up after the timestamp 1575158400: December 1, 2019, 12:00:00 GMT. 3. is a timestamp between X date and Y date is a timestamp between X and Y will match users whose date is between X and Y. This condition can be used if you want to send messages to users for whom an attribute is a timestamp between two dates. For example, say we want to send a message to people who signed up for our app in December. This is how we would do that: This segment includes all the users who signed up between the timestamps 1575158400 and 1577750400: December 1, 2019, 12:00:00 GMT and December 31, 2019, 12:00:00 GMT, inclusive. Best Practices If you want to take full advantage of timestamp rules, they are best used as unique rules inside a trigger segment, so that the users can match the time condition, and have delays calculated from that moment onwards. If you have specific questions regarding how timestamps work, have a look at our FAQ. --- ## Using JSON in segments URL: https://docs.customer.io/journeys/json-in-segments/ Attributes and event data can contain nested (JSON) values—arrays, objects, and arrays of objects. You can use these nested values to match people in segments, filters, and trigger criteria. If you're not familiar with JSON, we provide some simple options to help you traverse nested attributes. If you *are* familiar with JSON, you can use JSON dot notation as you normally would.  New to JSON? JSON is a standard, simple way to organize and structure data. Check out our introduction to JSON and learn how you can take advantage of JSON in Customer.io. How it works Attributes and events can contain nested JSON (JavaScript Object Notation) values, and you can evaluate these values when you build a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filter, or trigger condition. If you’re not familiar with JSON, you might check out our introduction to JSON—but we’ll explain a bit about how it works on this page. Read on and you’ll be a pro in no time. If you’re new to JSON (nested attributes and values), or just want a simplified experience, you can use the Has the property or Where at least one selectors to build segments based on nested values. See the If you’re new to JSON section below for more about these selectors. If you’re already familiar with JSON, you can use dot notation to point to nested values. You might use the Advanced option to point right to the nested value you want to evaluate.  You can only use JSON customer attributes and event properties in segment conditions You can’t use nested device attributes, collection properties, or trigger properties (from an incoming webhook or an API-triggered broadcast) in segment conditions. Example data On this page, we’ll consider the following sample JSON data representing a complex attribute or event data. We’ll use it to demonstrate segment matches on this page. In both cases, we’ll call it shopper_history. { "last_shopped": 1662587234, "location": {"city": "Montreal","province": "QC"}, "purchases": [ { "id": 123, "type": "computers", "name": "Monitor", "price": 25, "discount": 10, "shipping_address": {"city": "Calgary","province": "AB"}, "coupons_applied": [ { "coupon_code": "AXXXXX", "discount": "10%" }, { "coupon_code": "BXXXXX", "discount": "15%" } ] }, { "id": 456, "type": "computers", "name": "Mouse", "price": 15, "shipping_address": {"city": "Edmonton","province": "AB"} } ], "stores_visited": ["Winnipeg","Toronto","Vancouver"], "coupons_received": [5,10,20], "children_ages": [1654099180,1654099181,1654099182], "lottery_tickets": [ [1,3,5,7,9], [1,2,3,5,8] ] } If you’re new to JSON… We nest values under parents in two different formats: objects and arrays. See the curly brackets surrounding the data above—the { on the first line and the } on the last line? That’s called an object. If we stored all of the data above in an attribute called shopper_history, you could filter people who last_shopped with you using the Has the property selector—because last_shopped resides within the shopper_history attribute. See the square brackets surrounding the value after stores_visited above? That’s called an array. If we wanted to build a segment of all people who visited the Toronto store, we could build a segment of people with a shopper_history attribute that has the property stores_visited where at least one value is Toronto. An array can also contain objects—that’s what’s going on with purchases above. If we wanted to find everybody who used a coupon on their purchase, we could set up a segment where: shopper_history has the property purchases where at least one property called coupons_applied exists. Nested properties in the basic segment builder The segment builder has two selectors that help you traverse complex values: Has the property: Checks for a child of an object. Where at least one: Checks for a matching value in an array. When working with complex attributes and event values, you might want to open a representative person in another browser window—someone with data that you can reference as you build your segment. Has the property (objects) The has the property selector lets you traverse objects. Using our example data, you could create a condition stating Attribute location has the property city equal to Montreal. This evaluates to true. When you use the has the property selector, we nest child values, helping show the parent-child relationships between nested attributes. At least one of (arrays) The at least one of selector matches an item in an array. When you use it, you select whether you want to match a property or array value in the array. property: your array contains objects—like purchases in our example data, and you want to traverse that data to find a match. array value: your array only contains values, like stores_visited in our example data. Use this to match literally to any value in the array. For a simple array—a list of values in square brackets—simply use the contains or equals operators to match a value. Using the stores_visited array from our shopper_history example data, the following would evaluate to true. shopper_history.stores_visited contains Winnipeg is true, and the person represented by the data above would join the segment. You cannot specify a position or “index” when you use the segment builder this way. If you want to match a value at a specific index, you’ll need to use JSON dot notation in the advanced editor.  Equals and Contains have some minor differences The equals operator searches for a matching value, and contains searches for a match within a value in the array. See our section below for more information. Dot notation in the basic editor You can use dot notation in the simplified, basic editor. However, you can’t specify an array index when you use the editor this way. For example, if you wanted to filter or segment on a matching value in the stores_visited array, you could use Attribute shopper_history.stores_visited[] is equal to Winnipeg. But you’d have to switch to the advanced editor if you only wanted to match the first item in the array—i.e. shopper_history.stores_visited[0]. The Advanced Editor You can click Advanced to switch to an expanded JSON dot notation editor. If you don’t provide an array index when you use dot notation, we’ll match any item in the array (e.g. array[]). If you provide an index, we’ll match a specific position in an array. Arrays are zero-indexed—e.g. array[0] matches the first item in the array. Using our stores_visited example: shopper_history.stores_visited[] contains Toronto is true shopper_history.stores_visited[0] contains Toronto is false This works with nested arrays and arrays of objects as well. For example if we wanted to create a segment of people who used a specific coupon, we might use shopper_history.purchases[].coupons_applied[].coupon_code. This would search against every object within purchases for an array called coupons_applied, and then for the coupon_code for every object within coupons_applied.  Empty brackets don’t work in Liquid You can’t use our empty bracket array syntax (array[]) to match any item in an array using 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}}.. Instead, you’ll need to use a for loop or provide a specific index in the array. The ? operator: for arrays or objects If you’re not sure whether an attribute is an object or an array, you can use the ? operator in place of square brackets. When you use a ?, we’ll treat a value first as an array; if we don’t find a match, we’ll treat the value as an object. For example, if you wanted to access a key in purchases, but you weren’t sure if it was an object or an array of objects, you could use: shopper_history.purchases?.type. If you use the where at least one property in the basic editor and then switch to Advanced, you’ll see that we use the ? operator rather than square brackets, segments aren’t aware of your attribute and event data structure until you save conditions and we begin processing the segment. Traversing nested attributes When you search within an object or an array, we’ll traverse the entire value until we find a match. For example, purchases[].coupons_applied[].coupon_code searches for the coupon_code key within any coupons_applied found in purchases. However, if you know your coupon code values are unique, you could also simply search purchases[] for your coupon code value. We’ll automatically search for a matching value in any child property of the purchases[] array. Equals vs contains in an array In most cases, the equals and contains conditions are functionally identical when you use the at least one of selector (e.g. you reference an array). But there is a minor difference: Equals searches for an exact match. Contains searches for a value containing a string value—so a partial string can match. Using our stores_visited example data, these two statements are identical and true: stores_visited[] contains Toronto stores_visited[] equals Toronto But these two statements are not identical: stores_visited[] contains Toro is true stores_visited[] equals Toro is false If you don’t use array syntax—either the where at least one selector or []—contains will work but equals will not. When you don’t use array syntax, we treat the value as a string; the stringified value of stores_visited contains Toronto, but it has more characters than just that word, so it can’t equal Toronto. stores_visited contains Toronto is true stores_visited equals Toronto is false Use does not exist instead of an empty string In general, you shouldn’t send attributes or event data properties with empty strings. It’s not necessarily a problem if you do, but we don’t store customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. with empty values, and you can’t save a segment or filter condition with an empty value. But, if you do pass us attributes or properties with empty values—like in event data—you can segment or filter against these conditions using the exists or does not exist operators. --- ## Timestamp Conditions FAQ URL: https://docs.customer.io/journeys/faq-timestamps/ Timestamps and timestamp-based rules can be tricky. Here, we've compiled a list of common problems with timestamps, timestamp formats, and segmentation rules for timestamps to help you troubleshoot segment membership and other time-based issues. Why don’t people match my Signed Up segment even after I sent in created_at timestamps? In order for any timestamp rule to work, you need to send the timestamps in unix (seconds since epoch) format. If you are sending timestamps in milliseconds or if you’re using a different timestamp format like ISO 8601, your users will not match. Unsupported timestamp values We support timestamp values in seconds since epoch. We don’t support timestamps between 0 and 100000000, values in milliseconds, or negative timestamp values. Correct: 1461866400 (April 28, 2016, 6:00:00 pm) Incorrect - milliseconds: 1461866400000 Incorrect - ISO 8601: 2016-04-28T18:00:00Z Incorrect - RFC 2822: Thu, 28 Apr 2016 18:00:00 +00:00  Future timestamp values may not trigger campaigns immediately If you trigger a campaign based on membership in the Signed Up segment and you set a person’s created_at to a future timestamp value, that person won’t trigger campaigns based on the Signed Up segment until then unless you explicitly include future dates in your trigger conditions. See Segment-triggered campaigns and future timestamps for more information. Special notes You can easily notice if your timestamp is in the correct format or not by the fact that the human readable date and time will appear next to the epoch value in the user interface. If you need a timestamp converter, you can try epochconverter.com I’m using a timestamp rule for my campaign trigger, but users are only receiving the first message. Timestamp rules can get tricky. Some of them allow users to exit a segment, while others include them in a segment until the timestamp changes. Here are the rules we have: If you’re using one of the rules that allow users to exit, make sure that the user will still be in the segment after any delay. These rules are for segment entry is a timestamp: when someone matches this condition, they will enter the segment, but not exit. You can add multiple messages with different delays and the user will receive them. is a timestamp before a relative date of X days from now: when they match, the user will enter the segment, but not exit. You can add multiple messages with different delays and the user will receive them. is a timestamp before a relative date of X days ago: when they match, the user will enter the segment, but not exit. You can add multiple messages with different delays and the user will receive them. is a timestamp is before a specific date of X: If the timestamp value doesn’t change, the user will enter the segment when they match, but not exit. You can add multiple messages with different delays and the user will receive them. is a timestamp is after a specific date of X: If the timestamp value doesn’t change, the user will enter the segment when they match, but not exit. You can add multiple messages with different delays and the user will receive them. is a timestamp between X date and Y date: If the timestamp value doesn’t change, the user will enter the segment when they match, but not exit. You can add multiple messages with different delays and the user will receive them. These rules allow for segment exit is a timestamp after a relative date of X days from now: The user will exit the segment as soon as there are less than X days left until the date specified. So if your day is one week (7) days from now, then the user will exit the segment as soon as the timestamp is less than seven days away. is a timestamp after a relative date of X days ago: The user will exit the segment as soon as more than X days passed your date. For example, if your date is today and the rule is seven days from now, they will exit the segment as soon as seven days passes. How do I message users whose timestamps are before/after a specific date? You can use the condition is a timestamp before a specific date of to target before a specific date. Use the condition is a timestamp after a specific date of to target after a specific date and compare against your chosen date. is a timestamp before X date is a timestamp before a specific date of X will match users whose date is before X. Use this condition if you want to send messages to users for whom an attribute is a timestamp before a date. For example, say you want to send a message to people who signed up for your app before Nov 30, 2019. This is how you would do that: This segment includes all the people who signed up before the timestamp 1575158400: November 30th, 2019, 12:00:00 GMT. is a timestamp after X date is a timestamp after a specific date of X will match users whose date is after X. Use this condition if you want to send messages to users for whom an attribute is a timestamp after a date. For example, say you want to send a message to people who signed up for your app after December 1st, 2019. This is how you would do that: This segment includes all the people who signed up after the timestamp 1575158400: November 30th, 2019, 12:00:00 GMT. What time zone are you using to display timestamps? Inside Customer.io, the timestamp values are dictated by the time zone settings of your computer/laptop. If you change your settings, the values shown in the user interface will update accordingly. How do I create a segment that only has new users from a certain fixed date (e.g. April 1, 2017)? If you are using the created_at attribute with Epoch Timestamp values, you can segment based on that timestamp as seen in the image below. So, using April 1 2017 as an example, you would convert that date to an Epoch Timestamp and then enter that value for comparison against the created_at timestamp of your users. Note, the timestamp value pictured below (1491022800) is GMT: Sat, 01 Apr 2017 05:00:00 GMT. You can use https://www.epochconverter.com/ to convert to your desired date and time zone and adjust that value accordingly to meet your data requirements. That segment will contain only people created after April 1, 2017. How do I convert timestamps back to readable dates and times? When you export information out of Customer.io, you will see dates and times as unix timestamps. Here’s how to convert timestamps to a readable format in Microsoft Excel. Conversion formula =(((A1/60)/60)/24)+DATE(1970,1,1) This assumes that A1 is the cell with the epoch number. Then, format the result cell or column of cells for date (and time). By default, this conversion is for GMT. Converting to your time zone If you’d like to see the result in your time zone, add your time zone’s UTC offset into the formula: =(((A1/60)/60)/24)+DATE(1970,1,1)+(-[YOUR OFFSET HERE]/24) For example, to convert timestamp data to Eastern Time, where the offset is -5: =(((A1/60)/60)/24)+DATE(1970,1,1)+(-5/24) Here’s a list of UTC offsets. --- ## Timestamp Rules for Building Segments URL: https://docs.customer.io/journeys/timestamp-rules/ Timestamp rules aren't always easy. They may require some deep thinking about *when* things happen, not just for one person but for a group of people, and when you want your campaign to go out in relation to those things. This page provides some basic rules to help you build timestamp-based segments, so you can send timely messages to your audience. Introduction Timestamp rules aren’t always easy. You want to make sure you send opportune messages and campaigns to your users, using timestamp rules in your segments. But this process may require some deep thinking around when things happen, not just for one person but for a lot of people, and when you want your campaign to go out in relation. To help you get going, we’ve put together a couple of quick timestamp segment recipes and explanations here, with common use cases in mind. (This tutorial assumes that you’ve integrated with and are sending data to Customer.io. Timestamp Recipes When building your segments, you’ll see a menu like this for attributes: There are various options here, and we’ll go through each one in turn: 1. is a timestamp Despite being the most basic-looking rule, this one can sometimes be the trickiest! If you have a person belonging to a segment with just the rule created_at is a timestamp, they’ll never leave it. Customer.io ships with a default Signed up segment, which includes all of your users, including those who matched in the past (as long as they have the required created_at attribute). This rule is often used as a baseline trigger segment, along with others, for a triggered campaign—and is best shown with an example: Say we want to send a particular campaign to all new sign-ups in Canada. This is what that campaign trigger would look like: For this type of campaign, you probably want to send to all new sign-ups going forward in time. To do so, you’d set your campaign to include Future matches only. That way, this campaign won’t message all of your Canadian users who are already signed up. When new users sign up, they will enter the “Signed up” segment and, due to the filter, will only receive the campaign if they are in Canada. (If you wanted this campaign to include Canadian users who have signed up in the past and in the future, you’d set the campaign to match “Existing and future matches.”) 2. Is a timestamp before X days from now Want to send users payment reminders? This is the timestamp rule you want to use. Check out this example segment: This segment checks what the time is 7 days from now. Then it checks a customer’s next_payment_date. When the difference between next_payment_date and now is 7 days, the user enters the segment. Using this segment as a trigger for a “reminder” campaign means that users will receive your messages exactly 7 days before their next_payment_date. Then, you can update the value of the next_payment_date, and people will re-enter your campaign as long as the campaign’s Frequency setting lets people re-enter the campaign every month. 3. is a timestamp before X days ago Want to target your most loyal users, ones who have signed up and been active for more than a year or two? Try this: This timestamp rule is looking for people who signed up over a year ago. Let’s break it down: is a timestamp before checks a time (in this case, 365 days ago) against the attribute you choose (in this case, created_at). If the attribute timestamp is before that time, then the user enters the segment. So this example segment checks if the user was created any time before one year ago. You can combine this rule with other attributes or rules (a particular persona, or performed a particular event, etc.) to send your campaigns. But not all people in this segment will trigger a campaign. Go to Using timestamps in segments to learn more. 4. is a timestamp after X days from now This is one of the harder timestamp rules to use, but allows you to be very specific in which messages you plan to send. For example: I have a special feature release planned for next month. I want to send a specific “We have something awesome coming your way” newsletter about that release, and only to people who I’m sure will be users of my app then. For this example, that means their account isn’t expiring in the next 30 days. This is what that segment I’d use to trigger that newsletter would look like: In this case, Customer.io checks what the expiry date timestamp will be 30 days from now, and compares it to the user’s attribute. If the attribute timestamp is at least 30 days in the future, the user matches the segment. As soon as the expiry date is less than 30 days, the user will exit the segment. This is good for sending newsletters based on your users’ current state in your app. You could also use this segment to do research. Let’s say I want to check in with super active users whose accounts are set to expire in 30 days. This segment will tell me which “power user” accounts will expire next month. I can use this segment to send them a message, or I can export this segment and use the list to reach out personally (handy!). This is an example of how Customer.io can be used for research or data analysis, and help you get to know your customers better. 5. is a timestamp after X days ago This is a useful rule to message everyone who has done something in the past within a specific span of time. For example, you can use this type of timestamp rule to segment for users who have signed up for your app in the last 24 hours, like this: They’ll enter this segment as soon as they sign up, and will exit the segment as soon as more than one day passes. 6. is a timestamp before X date is a timestamp before a specific date of X will match users whose date is before X. This condition can be used if you want to send messages to users for whom an attribute is a timestamp before a date. For example, say we want to send a message to people who signed up for our app before December 1st. This is how we would do that: This segment includes all the users who signed up before the timestamp 1575158400: December 1st, 2019, 12:00:00 GMT. 7. is a timestamp after X date is a timestamp after a specific date of X will match users whose date is after X. This condition can be used if you want to send messages to users for whom an attribute is a timestamp after a date. For example, say we want to send a message to people who signed up for our app after December 1st. This is how we would do that: This segment includes all the users who signed up after the timestamp 1575158400: December 1st, 2019, 12:00:00 GMT. 8. is a timestamp between X date and Y date is a timestamp between X and Y will match users whose date is between X and Y. This condition can be used if you want to send messages to users for whom an attribute is a timestamp between two dates. For example, say we want to send a message to people who signed up for our app in December. This is how we would do that: This segment includes all the users who signed up between the timestamps 1575158400 and 1577750400: December 1st, 2019, 12:00:00 GMT and December 31st, 2019, 12:00:00 GMT, inclusive. Wrap Up That’s it! Some basic timestamp recipes with use cases. We know that this can get pretty complex pretty fast, so if you have any questions about timestamps or have a use case that’s got you stumped, please get in touch! --- ## Why don't people match 'within the past X days' conditions? URL: https://docs.customer.io/journeys/past-x-days-help/ This problem often comes up when you set up a segment to check that someone has *not* performed an action within the last X days. The reason this happens is because we can only check that criteria if they've been in Customer.io for at least that many days. Using “within the past X days” to check whether people have or haven’t done certain actions within a certain time can sometimes be tricky, due to how we store data in Customer.io. An example Check out this segment, which aims to identify people who have not made a purchase in the last seven days: What you need to know To match, people must have existed in Customer.io for at least seven days. The “age” of a person’s profile in Customer.io is determined either by: The date-time when you added the person to your workspace. The timestamp of the earliest event performed by that person. You can use back-dated events to effectively backdate the age of a person’s profile. If a new user is added and logs in and has made no purchases, they still won’t match; Customer.io doesn’t have seven days’ worth of data to check! This means that you won’t know whether or not a user hasn’t made a purchase within the last week until they’ve been in Customer.io for at least one week. Technically-speaking, the important qualifier for these negative “has not performed A” conditions “within the past X days” is that people need to have been in Customer.io for at least the value of X days, or have performed an event at least once before X days, in order to be evaluated against that condition. Want people to match anyway? If you’d still want people to match even if we don’t have X days’ worth of data, you can— you just need to explicitly say so in Customer.io, with an OR condition. Like this: This means that if a person has been in Customer.io for under 7 days, they would match the second OR condition. Anyone who has been in Customer.io for longer than 7 days who meets the first condition will not be evaluated for the second condition since we already know they match. --- ## Setting Up Segments for Specific Purposes URL: https://docs.customer.io/journeys/creating-segments/ Segments are groups of people matching the same criteria. Here, we'll provide recipes for some commonly-used segments that you can use to get started with Customer.io. Ingredients Integration with Customer.io A knowledge of the attributes you’re sending to us We’ll start with a basic segment, just to get you familiar with creating a segment in Customer.io, and then ramp up the complexity. So head on to the Segments builder and let’s get started! Basic: target users who clicked a link You can use data-driven segments to track users who clicked a specific link in your messages. First, check to see if URL parameters are enabled in your workspace. If they are, you’ll need to account for them in the segment condition. Use a wildcard (*) between the URL and link parameters to account for all parameters added by your global settings, likehttps://customer.io/release-notes*#2023. Set up the segment condition like this: Then save: If URL Paramters are disabled, you can copy/paste the tracked link exactly as it appears. If you think you might turn on URL parameters in the future, follow the above steps to set yourself up for success. Basic: nudge users to complete a task For this example, imagine that you have an app in which a key activation step you’d like the customer to take is filling out a profile. So, you’d want to know who hasn’t filled out a profile in your app, and create a segment of those users. For this example, we’ve identified this user group with incomplete profiles by including anyone whose value for a particular attribute profile_percentage is less than 95. This is what the segment looks like in the builder: Once you save this segment, we’ll process it, and you’ll be able to see exactly how many people belong to it: See this segment in use in our onboarding campaign recipe, a drip campaign that encourages users to fill out their profile. Basic: follow up on a message (to nudge task completion) Another basic example is whether or not we’ve sent a user a particular message in the past that we’d like to follow up on. In the following example, my users: Have received a ‘10 reasons to subscribe’ email, BUT… Haven’t actually subscribed yet, like this: This is a good example of a follow-up message that considers actual behavior. Instead of merely looking at whether someone opened or clicked on an email, we can see whether someone has actually completed the conversion criteria of subscribing or not to formulate our communication strategy. Note! These segments might feel a little abstract, because the attributes you’re sending us might be different. The idea here is to get you familiar with the builder. If you go through all of these and still find that you need help, please let us know. Basic: ‘Has device’ segmentation If you have set up Push or are sending us mobile device data, you can segment your users based on that. Right now, we allow you to create segments based on whether or not they have a device registered with you: Intermediate: prompt a purchase Say we’re an outdoor clothing manufacturer, and we want to send a campaign to people who live in Canada (where it’s cold all the time) and haven’t bought a jacket recently. First, let’s start with identifying folks who haven’t purchased anything: … followed by clicking Refine and Add filter to identify exactly which purchase they haven’t made. Now we’re looking for folks who haven’t purchased any jacket once within the last 100 days. This segment relies on specific event data flowing into Customer.io (in this case, for purchase events). First, we want to segment for people who haven’t performed a purchase event with a specific product type, jacket, within the last 100 days. Also, we want to make sure they’re located in Canada, which we do with the country attribute. When we’re done building all those conditions, the segment looks like this: Advanced: reach out to specific personas based on new feature usage For our most complex segment, we want to combine and nest a few conditions with a couple of AND and OR operators. Say we released a new feature. Exciting! Now we want to segment for people in a particular role within their company (designer or marketer) who have used the demo for that feature, but subsequently did not upgrade their account. Maybe that feature hasn’t worked out for them, and they have some valuable feedback. --- ## Welcome to Design Studio! URL: https://docs.customer.io/journeys/design-studio-overview/ Design Studio is a flexible editor that helps you create beautiful, responsive messages faster than ever before. Use it to set branding across emails made in Design Studio and in-app messages. In Design Studio, you can: Build emails with our visual, block-based editor. You can also modify the HTML/CSS as you see fit by switching to the code editor! Build emails using reusable blocks. We offer out-of-the-box blocks called standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio.. You can also create your own reusable blocks called custom componentsA custom block of code with content and properties you can reuse across messages made in Design Studio.. Assign global styles that span your emails and in-app messages. Collaborate with teammates by sending test messages, requesting feedback, and more. The level of functionality you have access to depends on your workspace-level role: Workspace Admins and Authors have full access, Viewers can only view. Build your emails visually or with code In Design Studio, every email has two editors: the visual editor and code editor. In the visual editor, you can drag and drop components onto the canvas to build an email. Click a component to open the Properties menu and style it. Open the Personalization panel to add liquid. In the code editor, you can access and modify the HTML/CSS and add components too. It comes with a set of developer tools to help you validate your code, manage content, and test accessibility. If you’re creating an email from scratch, start with our visual editor! If you need to import an email or paste in HTML, use the code editor. Reusable content and formatting We offer two types of components to help you scale your design system and reuse content across emails: Standard components are out-of-the-box elements commonly needed in emails: headings, paragraphs, columns, buttons, etc. Custom components are elements you code from scratch to create custom, reusable content across your emails. You’ll find available components in the Insert menu of the visual editor. How to localize emails You can add translations to any template or email in Design Studio. If you’re familiar with localization in our other editors, it’s the same process. Store people’s language preferences as an attribute on their profiles. Tell Customer.io what attribute you use for audience’s language preferences. Create your default message and add translated versions in the email editor. To add a language, open a Design Studio email, click next to the email’s name, and select Add language to get started. If you use our Auto-translate with AI tool, it will translate the following: Body text Subject lines Preheader text It will not translate: Liquid: Attribute values, filter values, any text rendered by a conditional, etc. Images, but it will translate alt text Snippets: Rather, you should add liquid conditionals based on people’s language preferences to the snippet file. Text in custom components: However, our AI tool will translate the text if you detach the component first, and then translate the message. Learn more about detaching a component from its source file. Learn more about setting a language attribute and translating content with AI in our localization guide. Assign global styles Use Styles to create emails in Design Studio that follow your brand guidelines! Add your brand’s colors, fonts, spacing, and radii. Assign your styles to text, links, and buttons. Your components will automatically inherit these styles! Global styles apply to emails made in Design Studio within a single workspace. Review your emails We offer a number of tools to help you review your emails so you can be confident your recipients are getting the best email possible. Preview settings While editing a message in Design Studio, you can preview: Desktop vs mobile views Light vs dark mode With images blocked For different visual abilities (color blindness) With sample data to ensure your liquid renders personalized information Send a test message Send a test message to yourself and colleagues to ensure your message looks as you’d expect in inboxes. Collaborate with teammates Collaborate with teammates to make sure you’re aligned on how you’re marketing to your leads and customers. Teammates with access to Customer.io can submit rounds of feedback and manage versions. Teammates without access to Customer.io can be sent an export or screenshots with your latest edits.  Only one person can edit a Design Studio message at a time. Design Studio doesn’t offer live, cross-team collaboration. If someone else is working on a message when you enter the visual or code editor, you’ll be prompted to Take over Editing or View Only. Connect to an automation Connect your Design Studio message to a campaign, broadcast, or transactional message to start sending. Currently, you can connect one Design Studio message to one automation. If you make changes to a message after connecting it, you then have to publish changes to update your connected automations. Submit feedback & request features We’re actively working on improving Design Studio to meet your needs. Have a feature request? Want to submit feedback on your experience? Go to the Design Studio dashboard, click Beta Feedback, and fill in the form. Your input is crucial in helping us develop the best experience possible! --- ## Manage your files URL: https://docs.customer.io/journeys/file-manager/ In Design Studio, manage your emails, templates, components, and styles in one place from your dashboard. Overview To access your dashboard, click Design Studio from the left navigation. From there, you can: Switch between your message files and global styles at the top. Create, edit, or delete your emails, templates, and components. Search and filter for existing files. To manage assets, like logos and images, go to Content > Assets. Learn more about managing your assets and what file types and sizes we support. You can also upload and optimize images from a Design Studio email. Create files You can create emails, templates, and custom components in Design Studio. There are some key differences between these file types: File type Use case Can connect to automation? Email The messages you send to customers. Can be created from scratch, from a template, or from an imported email file. yes Template An outline or layout for a message. Can create an email from a template. no Component Reusable, customizable blocks of content to build your messages and templates with. no Components are reusable blocks of content. Check out our out-of-the-box standard components to see if those suit your needs! You can also create custom components from the visual editor or code a custom component from scratch for things like headers and footers. To create a file in Design Studio: Click Create, then choose Email, Template, or Component from the dropdown. Give the file a clear name so your teammates can find it later. For emails and templates, choose to create from scratch or from an existing template. Import files In addition to creating emails from scratch or templates, you can also import existing emails in two ways: Upload a file from your computer Import from email by forwarding it to a unique email address in Design Studio When forwarding, you can either: Send to your workspace’s main import address Send directly to a specific folder or file Click Import in Design Studio to get started. Learn more about migrating emails to Design Studio. Upload a file To upload a file, click Upload and choose a file from your computer. You can upload your existing emails with the following extensions: File type Extensions MIME type HTML .htm .html text/html Email Message .eml text/eml Forward an email to Design Studio Click Import, then choose Copy Import Email Address. This copies your workspace’s unique import address to your clipboard. Forward any email from your inbox to that address. Your file will appear in a folder called Imported from Inbound Email and have a name that uses the subject line and timestamp of the email. Forward an email to a specific file To import directly into a file or folder: Find the file or folder in your dashboard. Click and select Copy Import Email Address. Forward your email to that unique address. Open your file or folder and you’ll find the imported content.  Emails made outside Customer.io may not work with the visual editor The visual editor works best with our standard components and most HTML elements. To make all of your HTML editable in the visual editor, switch to the code editor and add <x-edit-text>. Duplicate files You can duplicate individual files or entire folders. From your dashboard: Click next to the name of the file or folder. Click Duplicate from the dropdown. The file or folder will appear in your dashboard as “[Copy] name.” Create from an automation You can also create a Design Studio message from a campaign, broadcast, or transactional message. In campaigns, drag an Email block into your workflow and click Add Content. In broadcasts, choose Email as your channel type and click the No content box. In transactional messages, select Email and then Add Content. At the top right is a banner with a header “Create with Design Studio.” Click Try it out to switch to this editor. From there, you can select a Design Studio message, copy an existing message, or create a new one from scratch. Click Create New if you want to create from a template. Switch back to the classic email editors Click Go back to classic to go back to the older email editors at anytime. Find files You can find your files in Design Studio in a few different ways: Search by name or content: To quickly locate a file, use the Search bar. This looks for keywords across your filenames and file content. At the top of the results, you’ll see how many files match your search. The matching text is highlighted with an excerpt of the line around it. Reopen recent files: Under Recent Files at the top of the dashboard, you’ll see up to five files presented as thumbnails in order of how recently you opened them. Filter by file type: At the top of the table, you can choose the type of file to view. By default, All is selected and includes all file types. Click Messages, Templates, or Components to narrow your view further. Sort your files and folders: Towards the bottom of your dashboards is a list of your folders and files. By default, files are in alphabetical order by Name, with folders listed first. You can also sort them by whether or not they are Used In an automation, or when they were Last Edited. Click a column header to sort in descending or ascending order. Organize files Keep your files organized by creating folders. Click Create. Click New Folder. Name your folder. Click and drag your files into the folder. You can create a subfolder the same way you create a folder. Then drag it into the folder you’d like it filed under. You can also click on the icon next to an existing folder, and click New Folder in the dropdown. It will automatically be nested in the larger folder you selected. Delete files You can delete files from your dashboard by clicking next to the filename, and selecting Delete from the dropdown. You can delete individual files, or entire folders.  Deleting a folder deletes all subfolders and files. When you’re ready, click OK in the pop-up modal, and your files will be permanently removed.  You can’t delete a file that’s connected to an automation in Journeys You must first disconnect it from the automation, then you can delete it. Publish messages With Design Studio, you create emails and connect them to campaigns, broadcasts, or transactional messages. Then when you make an update to a connected message, you decide when to push those changes to their automations. This is called publishing in Design Studio. To publish messages with drafted changes, go to the Design Studio dashboard: Select one or more messages. Click Publish above the table. When you publish a message, we compile all changes to the email: updates you made to the email file, changes to the code of referenced components, and any changes to global styles. Learn more about publishing emails in Design Studio. Migrate emails to Design Studio If you want to migrate emails to Design Studio, we recommend you build them from scratch using global styles and components in the visual editor. This way, you can continue to use all functionality in the visual and code editors and ensure brand consistency across your emails. However, if you choose to import emails or paste HTML into the code editor, here are some things to keep in mind: Pasting or importing your source code If you paste your full email template into the code editor, make sure you remove the standard componentA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio. <x-base></x-base>. We add that by default to all Design Studio emails, but it will conflict with your source code. You’ll be able to edit the content of semantic HTML elements (like <p> or <img>) in the visual editor and add CSS styles or classes through the Properties menu. Semantic means the name clearly defines the purpose of the element. However, for non-semantic tags (<div> and <span>), you can’t edit them, but you can edit the text if you add some code. To edit the content of non-semantic tags in the visual editor, wrap the text of these elements in <x-edit-text> in the code editor. You’ll also need to do this to edit text within <table>. Unlike semantic tags, you won’t be able to add CSS styles or classes through the visual editor’s Properties menu. Migrating emails made in Customer.io’s legacy email editors Layouts are not supported in Design Studio. If you’re migrating any emails that were made in the code or rich text editor and they use layouts, you’ll have to recreate the layout. Snippets currently work in Design Studio, but we are moving away from supporting them. The equivalent in Design Studio is custom componentsA custom block of code with content and properties you can reuse across messages made in Design Studio.. --- ## Keyboard shortcuts URL: https://docs.customer.io/journeys/keyboard-shortcuts/ There are a number of keyboard shortcuts you can use to access Design Studio functionality. You can see a full list of shortcuts at any time by clicking the Shortcuts button in the bottom right corner of the code editor. Any shortcut that uses CMD on Mac will use CTRL on Windows (and vice versa). General Name Shortcut Toggle Preferences CMD/CTRL , Toggle Sidebar CMD/CTRL SHIFT B Editors Editor management Name Shortcut Close current editor OPTION W Formatting Name Shortcut Wrap Bold CMD/CTRL B Wrap Italics CMD/CTRL I Wrap Underline CMD/CTRL U Basic editing Name Shortcut Cut line CMD/CTRL X Copy line CMD/CTRL C Paste line CMD/CTRL V Move line up OPTION ↑ Move line down OPTION ↓ Basic editing - Code editor only Name Shortcut Copy line up SHIFT OPTION ↑ Copy line down SHIFT OPTION ↓ Delete line CMD/CTRL SHIFT K Indent line CMD/CTRL ] Outdent line CMD/CTRL [ Toggle line comment CMD/CTRL / Fold current region CMD/CTRL K CMD/CTRL [ Unfold current region CMD/CTRL K CMD/CTRL ] Preview - Code editor only Name Shortcut Toggle Preview CMD/CTRL SHIFT D Toggle Inspect Element CMD/CTRL SHIFT C Toggle Focus Mode CMD/CTRL SHIFT M Find & replace - Code editor only Name Shortcut Find CMD/CTRL F Replace CMD/CTRL OPTION F Find Next CMD/CTRL G Find Previous CMD/CTRL SHIFT G Select all matches OPTION Enter Multi-cursor & selection - Code editor only Name Shortcut Insert cursor OPTION + Click Insert cursor above CMD/CTRL OPTION ↑ Insert cursor below CMD/CTRL OPTION ↓ Select current line CMD/CTRL L --- ## Set global styles URL: https://docs.customer.io/journeys/set-styles/ Global styles in Design Studio let you define reusable design elements—like colors and fonts—that apply across emails made in Design Studio and in-app messages. You’ll add global styles—your colors, fonts, spacing, and radii—then assign them to standard elements—headings, paragraphs, links, and buttons. This way, your components will default to styles that best support your brand, making it easy to keep your messages consistent no matter the content, or how many people are making edits.  Global styles now apply to in-app messages! Styles apply differently to in-app messages than they do to emails, so make sure you check out our article on using global styles with in-app messages. Access your global styles Click Design Studio, then choose Styles from the dropdown. You can also preview or modify global styles while editing an email without leaving the message itself: Click in the top right then select Global Styles or Go to the visual editor’s Properties menu. Select an element on the canvas, then find a property for colors, fonts, radii, or spacing. Select the field and you’ll see View Global Styles. Add your global styles Set default, global style variables so you can assign them to global components or quickly add them when editing a message. Add colors In Styles, click Add color then specify a Variable name. This name is the label you’ll see when assigning the color to Text Styles, Links, and Buttons. This is also how you’ll locate the global style when editing colors in the Properties menu. Next, add the hex value and specify the opacity. You can also click the color preview to choose from a color wheel, specify the RGBA value, use the dropper to capture a color on your screen, or specify a recently used color. Add fonts In Styles, click Add font then specify a Variable name. This name is the label you’ll see when assigning the font to Text Styles, Links, and Buttons. This is also how you’ll locate the global style when editing fonts in the Properties menu. Choose the Type of font. Click Standard to choose from a list of available fonts. If you choose Custom, you can add a link to an external stylesheet, provide the name for that font, and specify a fallback. Add custom fonts You can use a custom font in Design Studio by linking to an external stylesheet. It must have the extension .css. This can be a font hosted by a third-party service (like Google Fonts) or a custom stylesheet you host yourself.  Custom font display depends on the recipient’s email client Custom fonts (webfonts) rely on the email client to download and render them. Many email clients don’t support webfonts at all, typically for security reasons. For example, Gmail only supports its own fonts (Roboto and Google Sans). See Can I email for more about webfont support across email clients. To add a custom font: Host your font stylesheet. You should only include font-related styles in your stylesheet; extra CSS can affect rendering or slow down your message. Use a trusted service like Google Fonts Or upload your own stylesheet to a publicly accessible URL If you’re using a font hosted by a third party, make sure you have the right to use the font. Otherwise, the font may not load. From Styles > Fonts, click Add font. Enter a Variable name. This is the label you’ll see when assigning the font to Text Styles, Links, and Buttons. This is also how you’ll locate the global style when editing fonts in the Properties menu. Click Custom. Add the Font name.  The font name in Design Studio must match the font family name in your external stylesheet. Otherwise, the font won’t load. Add the URL. It must have the extension .css. Set a Fallback font for email clients that don’t support custom fonts. Choose one that is as similar as possible to your custom font so your message looks consistent across devices. You can check CSS Fonts for compatibility. Add custom font stacks You can fully customize your font stack in the advanced settings of a global font. This lets you customize fallbacks, in case our automatic fallbacks don’t suit your needs.  Custom font stacks override font dropdown selections The fonts you specify in the custom font stack field take precedence over the Font name or Fallback font fields. If you want these dropdown selections included, add them to your custom font stack. To get started, click Show advanced to find the Font stack field. When writing a font stack, keep the following in mind: Order your fonts by preference: primary-font, secondary-fallback, tertiary-fallback, etc. Add single ticks around font names with spaces like 'Helvetica Neue'. Include web safe fonts as fallbacks—fonts that are widely supported. If your primary font is a custom font, make sure you include a font name that matches the linked stylesheet. Add radii In Styles, click Add radius then specify a Variable name. This name is the label you’ll see when assigning the radius to buttons. This is also how you’ll locate the global style when editing radii in the Properties menu. Add the pixel value for the radius. Add spacing In Styles, click Add spacing then specify a Variable name. This name is the label you’ll see when assigning the spacing to buttons. This is also how you’ll locate the global style when editing spacing in the Properties menu. Add the pixel value for the spacing. Assign styles to components Once your global styles are set, you can apply them as defaults for key components—like headings, paragraphs, links, and buttons—or use them manually in the Properties menu. Assign dark mode styles Dark mode styles apply to Design Studio emails only, not in-app messages at this time. To modify global dark mode styles: Hover over a component block, like Heading 1, and click . Click next to a color field. Specify the dark mode color or choose a global style variable. Save your changes. In Styles, you can click the toggle to switch between light and dark mode previews under Components. When you toggle dark mode in Styles, you’ll see your dark mode styles against a dark background—but we don’t do this in your actual emails. When toggling between light and dark mode in a Design Studio email, make sure you set a background color on your containers, like a section block, or on your base component through Layers > Message > Properties. Define responsive styles You can define responsiveness for your brand styles in Styles > Components: Hover over a component block, like Heading 1, and click . Click next to a style. Specify the values for mobile and desktop, indicated by the different icons. Save your changes. In Styles, you can click the toggle to switch between desktop and mobile views under Components. Remember to preview your emails in Design Studio to test them for responsiveness. Text styles In Styles, you can set default styles for text-based components under Text Styles. You’ll see options for each heading type (1-6) and paragraphs. Note that paragraph global styles also apply to lists. The font of the paragraph global style also applies to links. Hover over a text style and click to edit it. You can choose a color from your existing global styles, or specify a new one. Click to specify a color for dark mode. You can add the hex value and specify the opacity or click the color preview to access more options. Choose your font, size, weight, and line height too. For font size, you can click to specify mobile vs desktop styling. Click Update to save your changes. In a message, any component that has text styles will default to these global styles. Headings will default to the global style set for that level header. Message properties, boxes, sections, and columns will default to your paragraph styles, as will actual paragraphs and lists. Links In Styles, hover over the Links block, then click to edit them. Choose the weight, decoration, and color for your links. Click to specify a color for dark mode. Click Update to save your changes. The global paragraph font applies to links. In a message, highlight a piece of text and click the hyperlink button to add your URL. Click Save and we’ll apply your global link styles. Button In Styles, hover over the button block and click to edit it. When you style the Text for your button, you can choose between your global fonts and colors, or set new ones. Set the font size and weight for your button here as well. For dark mode, click to specify a color. Under Layout, you can set the padding for your button. Choose between your global spacing styles, or add a new pixel value. Click to specify mobile vs desktop styling. Under Styles, choose your fill color from your existing color styles, or set a new one, then specify the opacity. You can also click Add a Gradient/Image if you don’t want your button to be a solid color. Learn more about setting a background gradient or image. For dark mode, click to specify a color. Click to specify mobile vs desktop styling. Choose a Radius from your existing styles, or type in a new pixel value. Click Update to save your changes. Now when you drag a button into your message, you’ll see your styles applied.  For in-app messages, you must reset components with multiple buttons to make them use global styles Single button components automatically use global styles in new messages. However, components with multiple buttons in an in-app message must be manually reset to connect to global styles. Dividers In Styles, hover over the dividers block and click to edit it. Specify the Fill or color of the line, and click to specify a color for dark mode. You can add colors or choose from global variables. Modify the Height to change the thickness of the divider. Click to specify mobile vs desktop styling. Click Update to save your changes. Now when you drag a divider into your message, you’ll see your styles applied. Generate styles with AI Generate styles from your website using AI to quickly add your brand info. We analyze the sending domain for your workspace, then add styles to your global style variables and components for your review.  Save your changes You must save changes for the AI-generated styles to take effect; we won’t change your styling without your consent. Generating styles adds onto the variables already saved to your workspace; it does not replace them. However, it may replace global component styling with the generated styles. Make sure you review the results before saving to ensure the results are what you want. The suggested styles are indicated with a sparkle icon . We can generate styles even if your domain is unverified. If you have multiple sending domains, we’ll pull from the oldest one. You can find your domains in Workspace Settings > Email > Sending domains. To generate styles with AI in Design Studio: Go to Styles and locate Generate styles at the top. If this is the first time you’re generating styles, check the domain on the banner before initiating the process. For any subsequent attempts, you’ll click this button then preview the domain. If it’s not the correct domain, you can still try to generate styles, but may need to manually edit the results. You can’t manually specify your domain at this time.  Does your domain load a webpage? If your domain does not load a webpage, we won’t be able to generate styles. You’ll have to manually enter them. Click Generate styles. This process can take up to 1 minute. You can navigate away and come back to see the results. Review the results. The suggested styles are indicated with a sparkle icon . Are the variables accurate? Were components updated correctly? For instance, are buttons using a desired variable? Edit your variables and components as you see fit. Click Save updates. If you’re not happy with the results, you can also refresh the page or click Discard. Update & publish your global styles You can update global styles from the Styles page or while editing an email. From the email editor, you can save global styles across your Design Studio files, but will still need to publish them separately: Click and then select Global Styles. Make your change and click Save Updates. This saves across your messages in Design Studio, but does not push changes to connected campaigns, broadcasts, or transactional messages. You still have to manually publish your changes. From Styles, you can save only or save AND publish changes across your messages and campaigns. Delete a global style If you delete a global font, color, radius, or spacing, it is no longer discoverable as a global style and no new messages will use the old styles. If any of these global variables were in use in your messages before deleting the style, we’ll make no changes to your messages; they’ll continue to use the style that you deleted. You’ll see the value of the former global style, rather than the variable’s name. --- ## Dark mode URL: https://docs.customer.io/journeys/dark-mode/ You can customize how your message appears in dark mode when a user's device or inbox is set to that preference. Why set dark mode styles? Dark mode is increasingly the default setting across devices and email clients. As customer expectations shift toward modern design standards, supporting dark mode helps: Reduce broken email experiences by providing intentional styling Increase readability with properly contrasted colors designed for dark backgrounds Meet accessibility standards and user preferences Reduce support complaints about poor email rendering in dark mode Where to set dark mode styles You can set default, global dark mode styles in Styles. You can also modify dark mode styles of standard components while editing an email and code custom components to support dark mode. To set dark mode for global styles, go to Set global styles. In this article, you’ll learn how to set dark mode styles on standard and custom components, the building blocks of Design Studio emails. With standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio., you can set dark mode styles without any code in the visual editor. With custom componentsA custom block of code with content and properties you can reuse across messages made in Design Studio., we’ll show you how to add dark-mode enabled properties and create @media queries to support more email clients. Inbox limitations & best practices When creating emails in Design Studio or other email building tools, the preview won’t always match what people see in their email inbox. This is due to how email clients sanitize code to ensure no malicious code is included in the email file. As such, not all email clients support dark mode styles or they support them in different ways: In Gmail, if the theme is set to dark mode, then Gmail forces dark mode and blocks any control from senders. Outlook webmail forces dark styles and allows senders limited control of dark mode. Notion mail forces dark styles but also allows senders control of dark mode. Apple Mail allows senders full control of dark mode. Keep in mind, some email clients simply follow the settings of the recipient’s operating system when applying dark mode, while others allow separate control in the inbox app. If the operating system and the app have opposing settings, this can lead to further inconsistencies. Behind the scenes, we use media queries (prefers-color-scheme) to apply dark mode styles. These detect whether recipients have dark mode enabled and attempt to adjust your email’s colors accordingly. Learn more about which email clients support this at Caniemail. Dark mode vs Forced dark mode After you turn on the Preview toggle in the visual editor, you’ll see options to preview styles in Dark mode or Forced dark mode. Dark mode represents what recipients will see if their email client fully supports dark mode media queries. Forced dark mode represents what recipients will see if Dark mode is enabled but media queries aren’t supported by their email client. Learn more about Forced dark mode for Outlook and Android apps. The tips below will help you make the most visually appealing email, no matter the email client. Reduce inconsistencies when dark mode styles aren’t supported Here are some tips for making your email more consistent across all email clients. Set a solid background color between a background image & text You should avoid using background images or gradients directly behind text. With forced dark mode, the text color may invert but the background won’t, which can make text unreadable. For instance, if your white text inverts to black but your dark background image remains unchanged, your text becomes invisible (black text on dark image). If you must use a background image, try one of our workarounds that sets a background color between the image and text so your text remains visible if the color inverts: In the visual editor, insert a box with a background color between your text element and the element with the background image. In the code editor, insert a <div> tag around the text and set a background color. In the visual editor, you can insert a box with a background color between your text element and the element with the background image. Drag your text into a box then drag that box into the container with the background image. Click the box and set a background color in the properties menu so your text is visible even if the color inverts. You could reduce the transparency so some of the background image remains visible too. If your text is in a paragraph component and a section component contains the background image, this is what the Layers menu should look like: In the code editor, add a <div> tag around the text and set a background color. Switch to the code editor in the top right. Locate your text element. It might be helpful to click Format Document above the editor then click Inspect Element at the bottom of the preview. In Inspect Mode, you can click the text element in the preview, and it will highlight the code for you. Wrap a <div> tag around the text element. Set a background color on the <div> tag. For instance, if your text is in a paragraph component and your image is in a section, you’d wrap the <div> tag around the paragraph and set a style attribute. <x-section outer-background="url('<link-to-image>')"> <div style="background-color:#f9d930"> <x-paragraph :color="{ light: `#000000`, dark: `#ffffff` }">Welcome aboard!</x-paragraph> </div> </x-section> Add a background or border to images If you’ve added an image that has a transparent background, like a logo, forced dark mode could invert the background behind the image which could prevent the image from standing out. To work around transparent backgrounds, modify the original file to have a background color or border that helps it stand out no matter what. For instance, here’s an example of a logo in light mode and dark mode with different options: Light mode Dark mode Dark mode: image w/ border Dark mode: image w/ background Use a black & white color scheme If you don’t like the colors inserted by forced dark mode, you could convert your email to use only black, white, and shades of gray. Forced dark mode more predictably inverts this color scheme. Add dark mode styles to standard components You can globally set dark mode styles for standard components in Styles. You’ll define your colors under Variables then assign these variables to light or dark mode for standard components under Components. You can also change the styles for individual emails that deviate from your global branding. To change a dark mode color through the visual editor: Click a standard component. Find a color property like text color, background/fill, or border color. Set your default color or choose a different global style variable. Click the moon icon to open dark mode. Set the color for dark mode or choose a different global style variable. Click the moon toggle at the top of the editor to preview your message in dark mode. Your changes save automatically. Remember to publish your changes if the email is connected to a campaign, broadcast, etc. This feature also supports the email client Thunderbird so our out-of-the-box dark mode styles are compatible with more email clients! Swap images based on dark mode You can specify dark vs light modes of an image in the Properties menu. This is available for the Image component and background images for containers like sections. Image components To swap images based on dark mode, follow these steps: Click an image component. Click next to Source. Click Add dark mode properties. Under , add the image URL or select a file for light mode. This is the default, fallback image. Under , add the image URL or select a file for dark mode. Click the toggles at the top to preview dark vs light modes of the image. Background images To swap background images based on dark mode, follow these steps: Click a container component, like a section. Under Background, click Add Fill then select Image. Add the image URL or select a file for light mode. This is the default, fallback image. Click then add the image URL or select a file for dark mode. Click the toggles at the top to preview dark vs light modes of the image. Add dark mode styles to custom components Currently, you’ll have to code dark mode into custom components. Try out our GPT made for custom components to get started faster! Create a custom component file. Create properties with schema that enable dark mode. Set a function to define the values of properties depending on if they’re objects or strings. Set variables to process the property values. Create a style object that references your variables. Reference the style object in the template. In this example, we’ll create a custom component so you can specify dark mode styles for background and text colors through the visual editor. <!-- Insert this component in your email with the following code: <dark-mode-test></dark-mode-test> --> <script> export const config = { label: "dark mode test", presets: [ { label: "dark mode test", content: `<dark-mode-test></dark-mode-test>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.text(), }, }); export const props = Component.defineProps({ 'background':{ section: 'Styles', label: 'Fill', schema: Component.props .withDynamicStyles(Component.props.string(), { darkMode: true, }) .optional(), type: 'background', }, 'color':{ section: 'Styles', label: 'Color', schema: Component.props .withDynamicStyles(Component.props.string(), { darkMode: true, }) .optional(), type: 'color', } }); function lightDark(value) { if (typeof value === 'object') { return { light: value.light || '', dark: value.dark || undefined, }; } if (typeof value === 'string') { return { light: value, dark: undefined }; } } const newColor = lightDark(props.color); const newBackground = lightDark(props.background); const styleObject = { color: newColor?.light, background: newBackground?.light, }; </script> <style #if="newColor?.dark || newBackground?.dark"> @media (prefers-color-scheme: dark){ .dark{ color: set(newColor?.dark) !important; background: set(newBackground?.dark) !important; } } </style> <style #if="newColor?.dark || newBackground?.dark" media="(prefers-color-scheme: dark)" isolated="thunderbird"> .moz-text-html .dark{ color: set(newColor?.dark) !important; background: set(newBackground?.dark) !important; } </style> <template> <div :style="styleObject" class="dark"> <slot>Content goes here</slot> </div> </template> Create dark mode-enabled properties If you’re building a custom component and want it to support dark mode, any properties that should respond to theme changes, like colors or backgrounds, must be marked as dynamic. Use .withDynamicStyles() in your property schema to ensure those styles update automatically when the theme changes. schema: Component.props .withDynamicStyles(Component.props.string(), { darkMode: true }) .optional(), Component.props.string() is one of the validation types you can use to define your property. darkMode is a boolean. After you’ve added your custom component to the visual editor, click it to open the Properties panel. When darkMode is set to true, you’ll see the dark mode moon toggle in the panel. Click it to set the style. Set a function to handle light/dark property outputs This function sets up the ability for your custom component to pass light and dark mode values, ensuring that you always get an object with both light and dark properties, even if one or both are not explicitly provided in the initial input. function lightDark(value) { if (typeof value === 'object') { return { light: value.light || '', dark: value.dark || undefined, }; } if (typeof value === 'string') { return { light: value, dark: undefined }; } } Set variables to process the property values Then define two variables, one for the text color and one for the background color, to use in a style object we can reference in our template. const newColor = lightDark(props.color); const newBackground = lightDark(props.background); Create a style object to reference in your template Finally, add a style object to the script tag so you can reference these properties in the template. const styleObject = { color: newColor?.light, background: newBackground?.light, }; Add your style object to the template Now you can reference the style object in the content of the custom component through the template tag. <template> <div style="styleObject"> <slot> Content goes here </slot> </div> </template> Add @media queries to support more email clients After your script tag, add style tags to define dark mode styles. Here we’ve added two style tags. The first includes a generic media query that sets a class dark. The second isolates styles needed to support Thunderbird. Learn more about the attribute prefers-color-scheme <style #if="newColor?.dark || newBackground?.dark"> @media (prefers-color-scheme: dark){ .dark{ color: set(newColor?.dark) !important; background: set(newBackground?.dark) !important; } } </style> <style #if="newColor?.dark || newBackground?.dark" media="(prefers-color-scheme: dark)" isolated="thunderbird"> .moz-text-html .dark{ color: set(newColor?.dark) !important; background: set(newBackground?.dark) !important; } </style> Then in the template tag you’d add class="dark". <template> <div :style="styleObject" class="dark"> <slot>Content goes here</slot> </div> </template> --- ## Responsive styles URL: https://docs.customer.io/journeys/responsive-styles/ Use the Properties menu to show or hide elements as well as customize responsive styles like font size and text alignment. Set default responsive styles in your global styles. You can also code your own responsive styles if we don't natively support making it responsive yet like image swapping, text transformations, and font families. How to set responsive styles You can set responsive styles in the visual editor or by writing your own CSS. Set responsive styles in Design Studio You can set responsive styles for your brand and for individual emails. Our responsive styles have a breakpoint of 600px. To set responsive styles globally, go to Styles. To modify mobile vs desktop styles in the visual editor, click a component and locate properties with . Add custom code in Design Studio You can code custom styles for features that don’t currently have UI support for responsiveness like text transformations and font families. Check out how to use media queries to accomplish this. We recommend building your emails mobile-first. This means optimizing your email for smaller device sizes (mobile) then customizing responsive styles for larger device types (desktop). This is important because a number of email clients don’t support responsive styles, and generally speaking, mobile styles look better on desktop than desktop styles look on mobile. To build custom, responsive styles in Design Studio: Build mobile-first in the visual editor. Add custom CSS classes to elements. Define @media queries for larger screens in the code editor. Add responsive styles. Inbox limitations & best practices Email clients can vary from fully supporting responsive styles to not supporting them at all. To help address this, we recommend you: Build mobile-first emails as mobile styles tend to look better on desktop than vice versa. Preview messages across device dimensions to see if you need responsive styles. Preview with styles blocked to simulate what recipients will see when responsive styles are not supported. Some email clients don’t support responsive styles: T-Online GMX web.de Gmail mobile webmail Gmail mobile app when used with a non-Google account In these clients, recipients will see the mobile version of your email by default. Our standard components are built mobile-first since mobile styles tend to look better on desktop than vice versa. Some email clients have a preview pane for emails, like Gmail webmail, Outlook webmail, and Yahoo webmail, but an email may not display as expected in the pane because the breakpoint is based on the width of the email client app, not the pane itself. Preview message before adding responsive styles Before you add responsive properties, preview the email and send yourself test messages to see how it displays across screen sizes. You may not need as many responsive styles as expected because of our fluid layouts. We built our standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio. scale to any screen size. So it’s possible your emails will look good without you explicitly setting responsive styles! Preview with styles blocked If you add responsive styling, preview the email with Block styles enabled to see what the email may look like in email clients that don’t support responsive styles. Then modify your default (the styles under the mobile icon) as needed to improve display. Add responsive styles to standard components From the Properties menu, you can set responsive styles for images, text, and containers as well as hide elements based on screen size. The breakpoint for desktop vs mobile is 600px. To set default responsive styles across your Design Studio emails, go to Styles. Swap images based on screen size You can specify desktop vs mobile versions of an image in the Properties menu. This is only available for the Image component, not background images. Click an image component. Click next to Source. Click Add responsive properties. Under , add the image URL or select a file for mobile. This is also the default image. Under , add the image URL or select a file for screens over 600px wide. Click the toggles at the top to preview desktop vs mobile versions of the image. Hide components based on screen size By default, all components show on both desktop and mobile. You may want to show or hide elements in your message to better support mobile or desktop screens. You’ll use the Hide on setting to control the visibility of components. In the visual editor, click a standard component—like a section or image, to open the Properties menu. Then locate the Hide on property. By default, nothing is hidden. Select to hide the component on screens less than or equal to 600px wide. Select to hide the component on screens greater than 600px wide. To remove visibility rules, click the counter-clockwise arrow icon, or select “none.” By default, the preview hides the content you’ve specified. Click to show the content. The hidden content is more opaque when previewing the screen size it should be hidden from:  Hiding a component hides any elements nested within it Before you hide a parent component like a box or a section, make sure there isn’t information within it that should remain visible. If you have HTML tags nested within a standard component, those tags will inherit the hide property of the parent standard component as well. Email client support The Hide on property works for all email clients that support @media queries, in addition to Thunderbird. For email clients that don’t support @media queries except Outlook, your emails will fall back to what you defined should be hidden on mobile. In the case of Outlook, your emails will fall back to the desktop version. Style based on screen size From the Properties menu, you can set responsive styles for text and containers, like sections or boxes. To set default responsive styles across your Design Studio emails, go to your Styles page. You can set responsive styles for text, like font size and text alignment, as well as layout properties like width, height, padding, margin, and container alignment for sections and boxes. In the visual editor, click a component—like a section or image, to open the Properties menu. Then find a field with . For screens less than or equal to 600px wide, add the style under . For screens greater than 600px wide, add the style under . To reset to your default styles, click the counter-clockwise arrow icon. To preview desktop vs mobile styles, turn the toggle on/off at the top. To set responsiveness for other styles like font families and text transformations, learn how to add code for responsive styles. Email client support Responsive styles work for Thunderbird, Outlook, and all email clients that support @media queries. For other email clients that don’t support @media queries, your emails will fall back to what you defined should be visible on mobile. Add responsive styles to custom components Currently, you’ll have to code responsive styles into custom components. Try out our GPT made for custom components to get started faster! Basic example of image swapping In this example, the custom component lets you add two images, one for mobile and one for desktop, in the visual editor. <!-- Insert this component in your email with the following code: <responsive-image></responsive-image> --> <script> export const config = { label: "Responsive Image", presets: [{ label: "Responsive Image", content: `<responsive-image></responsive-image>` }] } // Define the source and alt text for the images export const props = Component.defineProps({ src: { label: 'Source', schema: Component.props .withDynamicStyles(Component.props.string(), { responsive: true, }) .optional(), type: 'media', accept: 'image/*', placeholder: 'Paste the image url here...', }, alt: { label: 'Alt text', schema: Component.props.string().optional() } }); // Check whether the source is an object (multiple images) or a string (one image) const isResponsive = typeof props.src === 'object'; </script> <style #if="isResponsive"> /* If the source is an object, show the image based on screen size */ @media (min-width:600px) { .desktop-img { display: inline-block !important;; } .mobile-img { display: none; } } </style> <template> <div> <img :src="isResponsive ? props.src?.small : props.src" :alt="props.alt" class="mobile-img" /> <img #if="props.src?.medium" :src="props.src?.medium" :alt="props.alt" class="desktop-img" style="display:none" /> </div> </template> Advanced example: image swapping based on dark mode and screen size In this example, the custom component lets you add an image for desktop/mobile and dark/light modes in the visual editor. <!-- Insert this component in your email with the following code: <image-swap></image-swap> --> <script> export const config = { label: "image-swap", presets: [ { label: "image-swap", content: `<image-swap></image-swap>` } ] } export const props = Component.defineProps({ src: { label: 'Source', schema: Component.props .withDynamicStyles(Component.props.string(), { responsive: true, darkMode: true, }) .optional(), type: 'media', accept: 'image/*', placeholder: 'Paste the image url here...', }, alt:{ label: 'Alt text', schema: Component.props.string().optional() } }); // Check to see if the value is dynamicStyle or just a string value function dynamicStyle(value) { return value && typeof value === 'object'; } // Define a default value as the fallback const defaultImage = dynamicStyle(props.src) ? props.src.light ?? props.src.small ?? props.src['small:light'] : props.src; // Create a hash that can be used to make a unique class for the default image function hashClassName(input) { const str = typeof input === 'string' ? input : JSON.stringify(input); let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash * 31 + str.charCodeAt(i)) | 0; } return `h-${(Math.abs(hash) % 1e9).toString(36)}`; } const hash = props.src ? hashClassName(props.src) : undefined; </script> <style #if="props.src?.dark" > /* Shows this image when in dark mode and hides the default image */ @media (prefers-color-scheme:dark){ .defaultImage-set(hash){ display:none; } .darkImage{ display:inline-block !important; } } </style> <style #if="props.src?.medium" > /* Show this image when the viewport is over 600px (desktop) and hides the default image */ @media (min-width:600px){ .defaultImage-set(hash){ display:none; } .desktopImage{ display:inline-block !important; } } </style> <style #if="props.src?.['small:dark']"> /* Shows this image when the viewport is under 600px (mobile) and dark mode is enabled */ @media (max-width:600px) and (prefers-color-scheme:dark){ .defaultImage-set(hash){ display:none; } .mobileDarkImage{ display:inline-block !important; } } </style> <style #if="props.src?.['medium:light']"> /* Shows this image when the viewport is over 600px (desktop) and light mode is enabled */ @media (min-width:600px){ .defaultImage-set(hash){ display:none; } .desktopLightImage{ display:inline-block !important; } } </style> <style #if="props.src?.['medium:dark']"> /* Shows this image when the viewport is over 600px (desktop) and dark mode is enabled */ /* Hides not just the default image, but also the desktop light image */ @media (min-width:600px) and (prefers-color-scheme:dark){ .defaultImage-set(hash){ display:none; } .desktopLightImage{ display:none !important; } .desktopDarkImage{ display:inline-block !important; } } </style> <template> <div> <!-- Set the default image --> <img :src="defaultImage" width="200" :alt="props.alt" :class="`defaultImage-${hash}`"> <!-- Dark mode --> <img #if="props.src?.dark" :src="props.src.dark" width="200" :alt="props.alt" style="display:none" class="darkImage"> <!-- Responsive --> <img #if="props.src?.medium" :src="props.src.medium" width="200" :alt="props.alt" style="display:none" class="desktopImage"> <!-- When a combination of responsive and dark/light modes are available --> <img #if="props.src?.['small:dark']" :src="props.src['small:dark']" width="200" :alt="props.alt" style="display:none" class="mobileDarkImage"> <img #if="props.src?.['medium:light']" :src="props.src['medium:light']" width="200" :alt="props.alt" style="display:none" class="desktopLightImage"> <img #if="props.src?.['medium:dark']" :src="props.src['medium:dark']" width="200" :alt="props.alt" style="display:none" class="desktopDarkImage"> </div> </template> Add responsive styles to the code editor You can set responsive styles in the visual editor for text and images, but not for all properties. If you want to make properties like text transformations or font families responsive, follow these steps to create your email. 1. Build mobile-first in the visual editor First, build your email in the visual editor with mobile styles in mind. Drag in standardA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio. or custom componentsA custom block of code with content and properties you can reuse across messages made in Design Studio. from the Insert menu. Click any component to open the Properties menu and style your components for mobile. Switch to mobile view at the top of the editor to preview at a smaller device size while you edit. 2. Add custom CSS classes to elements To style components differently on larger screens, start by adding class names to the elements you want to target. Click a component in the visual editor to open the Properties menu. At the bottom, click Advanced, then enter a class name in the CSS Class field—something like text-transform-uppercase. The class name can be anything you want with a few exceptions: No spaces; you can use hyphens - or underscores _ instead. Can’t start with a number It’s also best practice to: Avoid starting a class name with a hyphen or underscore Avoid using special characters like & + $ etc. 3. Define @media queries for larger screens in the code editor Now that you’ve added CSS classes to the elements you want to make responsive, you’ll define when those styles should apply using a @media query. In the top right, click and switch to the code editor to define your CSS classes. In the <x-head> of your email, add a <style> tag with a @media query. This tells your message when to switch from mobile styles to styles intended for larger screens. <x-head> <style> @media screen and (min-width:500px){ } </style> </x-head> In this example, we’ve set min-width:500px which means that the styles within the query apply to emails on screens 500px wide and up. You can change this value to suit your needs. 4. Add responsive styles Now that you’ve defined when your styles should apply to larger screens, you can write the CSS that updates your layout at those sizes. Inside the @media query, add your styles for larger screen sizes, like desktops. Add each class name you added to your components to the query. Preface the name with a period and follow it with curly brackets. Add CSS attributes to style the class. Add !important to each style attribute to make sure it overrides any other style. <x-head> <style> @media screen and (min-width:500px){ .text-transform-uppercase{ text-transform: uppercase !important; } } </style> </x-head> Some components are made of multiple elements, each with specific styles. This means sometimes you’ll need to add selectors (img, div, p, etc) to get the styles to work. For example, an image would need an img selector: <x-head> <style> @media screen and (min-width:500px){ .responsive-margin img{ width:300px !important; } } </style> </x-head> Test out an example New to responsive styles? Try out the example below! Create a Design Studio email then copy/paste the below HTML into the code editor. Click between the desktop and mobile views at the top of the preview panel to see the styles change based on screen width! <x-base> <x-section padding="20px" class="padding-40"> <x-heading-1 :font-size="30" class="font-size-50">Hello world&nbsp;</x-heading-1> <x-image class="image-100p" align="center" width="300px" src="https://images.unsplash.com/photo-1550414485-9f22b971dbf0?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <x-cta class="button-300" width="100%" href="https://parcel.io/">Click Me</x-cta> </x-section> </x-base> <x-head> <style> @media screen and (min-width:500px) { .image-100p img { width: 100% !important; } .button-300 { width: 300px !important; } .text-transform-uppercase { text-transform: uppercase !important; } .padding-40>div { padding: 40px !important; } } </style> </x-head> --- ## Get started URL: https://docs.customer.io/journeys/visual-editor-overview/ In Design Studio, you can create an email with visual blocks, no coding necessary. Here’s a quick guide to what’s available in the visual editor, and how each part helps you build your message. It has four key areas—the Insert menu, Layers menu, Canvas, and Properties menu. Together, they enable you to add and style content without code. For more on previewing your message for accessibility and responsiveness, check out Preview email in Design Studio. If you get stuck, reach out to our agent! Click at the top of your workspace to get started. The agent can create or edit Design Studio emails at your request. Add blocks to your message You can find the content blocks you’ll use to build your email in the Insert menu. We offer two types of content blocks: standard and custom components. Standard components include out-of-the-box elements like headings, paragraphs, and buttons. Use these to quickly structure and style your messages. Custom components are reusable blocks of content created by your team. You won’t see custom components in the menu until you’ve created your first one. Click, hold and drag one or more content blocks from the Insert menu to drop them onto the Canvas, where you build your message. To create more room for your canvas, you can close the Insert menu by clicking the icon and toggle it open again through the canvas toolbar. Navigate your layout The Layers menu shows you the structure and nesting of your components at a glance. From here, you can reorder components as well as rename, duplicate, or delete them. Click a component to highlight it on the canvas and open its properties. You can also click and drag a component in the Layers menu to change the order. Hover over a component and click to rename, duplicate, or delete the layer. For instance, if your email has multiple Section components, you might relabel each to differentiate the parts of your email. To create more room for your canvas, you can click to close the Layers menu and toggle it open again through the canvas toolbar. Build your message The Canvas is where you visually build your message. Drag and drop components, upload images, and modify text here. To move a component, hover over it then click and drag the handle to the left. To duplicate or delete a component, click the component and choose the action from the context menu. To style part of your text, highlight the text then modify the marks available, like color or italics, in the bubble menu. Otherwise, edit your styles through the Properties menu. You can use the canvas toolbar to open and close the Insert, Layers, and Properties menus, undo or redo your work, or preview your message for different devices, color schemes, and visual impairments. Upload & optimize images From the visual editor, you can choose media that’s already been uploaded to your workspace (in your Assets library) or upload a new file as you build your message. If you upload a file larger than 2 MB, you’ll need to optimize the image to compress it while maintaining visual quality. This helps reduce the time it takes email clients to load your messages, including with poor internet connections. Keep in mind, it’s best to keep images under 1 MB. Add an image-based component to your canvas. Click Choose or Select Media. Click Upload file. Select the file you want to upload. If your file is larger than 2 MB, you must click Try optimizing. Otherwise, optimization is optional. Adjust the settings to your liking: Use View Type to compare the original image against the optimized version. Use Compression Type to balance file size vs. visual fidelity. Use Retina Mode to manage pixel density for high-resolution screens. Compare the size of the original to the optimized image. Click Save optimized image. Our optimize feature is only available in Design Studio. However, after you optimize images in Design Studio, they will appear in Assets for use in other messages. Translate your message You can add translations to any template or email in Design Studio. If you’re familiar with localization in our other editors, it’s the same process. Store people’s language preferences as an attribute on their profiles. Tell Customer.io what attribute you use for audience’s language preferences. Create your default message and add translated versions in the email editor. To add a language, open a Design Studio email, click next to the email’s name, and select Add language to get started. If you use our Auto-translate with AI tool, it will translate the following: Body text Subject lines Preheader text It will not translate: Liquid: Attribute values, filter values, any text rendered by a conditional, etc. Images, but it will translate alt text Snippets: Rather, you should add liquid conditionals based on people’s language preferences to the snippet file. Text in custom components: However, our AI tool will translate the text if you detach the component first, and then translate the message. Learn more about detaching a component from its source file. Learn more about setting a language attribute and translating content with AI in our localization guide. Style your message The Properties menu is where you style your components. Change the layout like margin and padding, text styles, add links to images, and more. To create more room for your canvas, you can close the Properties menu by clicking the icon and toggle it open again through the canvas toolbar.  Bubble menu vs. Properties menu Use the Bubble menu in your Canvas for more specific changes, like changing the text color of an individual word. Use the Properties menu for more general changes, like changing the text color of an entire paragraph. Edit multiple components at once You can select multiple components then edit shared properties or move them in your email to speed up email creation. To make edits across multiple components, you have two options: Under Layers, hold Shift and click each component. On the canvas, click and drag your cursor to select multiple components. Edit shared properties After you select multiple components, you can edit their shared styles in the visual editor. This way you can apply identical settings like margins across paragraphs, columns, and more, rather than spending time individually modifying them. If you don’t see a property when you select multiple blocks, then the setting doesn’t exist for one or more of the components you selected. You’ll modify that setting separately. Move multiple components After you select multiple components, you can drag and drop your selection to move them to another part of the canvas. Just hover over them to find the drag handle . Then click and hold to move them around. Message properties Use Message properties to style your email overall—set a background and default text styles for your message here. Click Message under Layers and choose Properties to get started. Behind the scenes, this styles the base component of your message. By default, your global styles for paragraphs define the color, font family, size, weight, and line height set on your base component. Manage links You can add links to text and components with images or buttons, like the Navigation block, to encourage people to take action. To add a link to text, highlight the text then select from the bubble menu. To add a link to a component, select the component then find Link in the Properties menu. Then choose the right type of link from the dropdown: URL, Email, or Phone number. URL opens a link to a webpage. This is also how you add standard links—common liquid tags for unsubscribing and viewing the email in a browser. Email opens a recipient’s email client. You can specify a sender address, subject line, and email template. Phone number asks to start a call. To preview your links, turn on the Preview toggle in the canvas toolbar then click your components or hyperlinked text. Standard links Standard links are just that—common links needed for accessibility and compliance: view in browser and manage subscription options. Within a Link field, click Standard links, and then choose the relevant link type. This adds a liquid tag which you can preview in the editor or when you send a test message. Learn more about these liquid tags below: Unsubscribe View in browser Manage subscription preferences Disable link tracking for specific links Learn more about disabling tracking on specific links within standard components. Fill out the envelope Fill out the envelope (subject, sender, recipient, etc) at the top of your message. Click at the top of your message. Fill out the envelope: From (Required): Learn how to add from addresses to your workspace. To (Required) Subject (Required): You can generate AI-powered subject line suggestions based on your email content and business context. Click next to the subject field to get multiple options that match your brand’s tone and audience. Title: Appears after opening “View in browser” links Preheader: Learn how to set custom preheader text Reply-to BCC: Learn about Fake BCC. Custom headers: Learn how to set custom email headers and which aren’t allowed. --- ## Style individual messages URL: https://docs.customer.io/journeys/properties-menu/ In Design Studio, the Properties menu lets you style each component of your email. You can adjust layout, set backgrounds, apply text styles, and control how components display on different devices. Global styles vs message styles By default, components pull from your Global Styles—like text, link, and button styles. If you want to change these for an individual message, you can override them in the Properties menu. To override styles for a group of standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio., add them to a container (like a section, box, or column) and modify the container’s styles—the components will inherit these. To override a single standard component, modify the style in the Properties menu. To override a piece of a component (like the text color of a word), highlight it then modify the styles in the Bubble menu. To change the styles of a custom componentA custom block of code with content and properties you can reuse across messages made in Design Studio., you may be able to edit the styles from the Properties menu depending on how it was coded. Apply styles to a component To style a component: Click an element on your canvas. Use the Properties menu to edit styles like font, color, padding, borders, and more. Preview your changes. If you want to reset the component’s property to its default value, click the counter-clockwise arrow next to the property name. You can also edit shared styles across multiple components at once. For instance, if you want to change your paragraphs to have the same font color, first click and drag your cursor to select them all, then edit the color in the Properties menu. Set a background You can add a background to most components using a solid color, gradient, or image. Not all background types are available for every component. Check out which components support backgrounds. If you want to set a background for your entire message, click Message under Layers. If the Layers menu is closed, click the layers icon on the canvas toolbar to open it. Solid color To set a solid background color for a component: Click the component you want to modify. Under Styles, locate the Background or Content Fill field. Enter the hex value of the color and the opacity percentage, or click the preview icon to choose a color and opacity. Preview your changes and send a test to see how it renders in your email client. To reset your background color, click the counter-clockwise arrow icon to the left of the field. To remove it, click the icon to the right. Gradient To set a gradient as the background of your component: Click the component you want to modify. Under Styles, locate the Background or Content Fill field. Set a fallback color in case your customers’ email clients do not support gradients. Click Add Gradient/Image. By default, we set a linear gradient based on your background color. Click the gradient field to change the gradient type or add color stops. Modify the layout properties to change the position, size or repeat of the gradient. Preview your changes and send a test to see how it renders in your email client. To remove the gradient, click the icon to the right of the field. Gradient type: linear or radial By default, we set a linear gradient. You can change the angle of the gradient by typing a different degree (0-360). You can also switch to a radial gradient by clicking the radial icon. We do not support conic gradients. Color stops A color stop defines the relative position of a color in a gradient. Every gradient will start with two color stops, but you can add as many as you’d like. To add color stops, click a location on the top slider. Then choose a color: From Recently used colors. From the second slider, then change the shade up top. By using the eyedropper to identify a color not yet in your email. Adjust the opacity as you see fit on the bottom slider. Click and drag a color stop along the top slider to change its position and see how it affects the way your gradient appears in your message. Click the trash icon to delete the selected stop. Image To set an image as the background of your component: Click the component you want to modify. Under Styles, locate the Background or Content Fill field. Set a fallback color in case your customers’ email clients do not support images. Click Add Gradient/Image. Click the new field, then choose Image from the pop-up. Paste the image URL if hosted outside Design Studio, or click “Select Media” and browse for a file in your workspace. Modify the layout properties to change the position, size or repeat of the background image. Preview your changes and send a test to see how it renders in your email client. Test with Block images enabled to see how it renders when recipients’ email clients have settings that block images. To remove the background image, click the icon to the right of the field. Layout Properties In the Gradient/Image menu, open the dropdown to edit your layout properties.  Set a custom size if you want to position and repeat a gradient While layout properties apply to both images and gradients, you’ll need to set a custom size for gradients to make positions and repeats useful. Remember to preview your changes and send a test to see how the layout of your gradient renders in your message! Position Choose how to orient your image or gradient. Left – Aligns the background to the left edge of the container. Right – Aligns the background to the right edge of the container. Center – Centers the background horizontally (and vertically, if vertical alignment isn’t separately specified). Top – Aligns the background to the top edge of the container. Bottom – Aligns the background to the bottom edge of the container. Size Control the size of your background image. For gradients, you’ll want to set a Custom size to see changes. Auto – Uses the original dimensions of the background image or gradient without any scaling. This is the default behavior if no size is set. Note that in this setting, if the image you add is a different size than your container, you may only see a portion of the image. Fit – Scales the background to fit entirely within the container while preserving its aspect ratio. The background will be fully visible, but there may be empty space if the aspect ratios differ. Fill – Scales the background to completely cover the container, preserving aspect ratio. Some parts of the background may be cropped to fill the area edge-to-edge. Custom – Allows you to specify an exact width, height, or both. For each, you can choose between auto sizing, a fixed pixel value, or a relative percentage value. This is useful when you want precise control over how large the background appears, regardless of container size. Repeat Control how the background fills the container when its size is smaller than the container. Tile – Repeats the background both horizontally and vertically to fill the entire area. Tile Horizontally – Repeats the background only across the x-axis (left to right). Tile Vertically – Repeats the background only along the y-axis (top to bottom). Space Evenly – Repeats the background with equal spacing between tiles. Tiles will not be clipped or stretched. Space to Fit – Scales and repeats the background to fit exactly into the container without clipping. May adjust spacing or sizing to make it fit evenly. None – Displays the background once, without any repetition. Fallbacks when email clients don’t support background images or gradients If you set a gradient or image as a background, set a solid color as a fallback. Not all email clients support gradients and background images, but they do support solid colors. You can find out more about email client support at Can I Email. Also note, your background settings are evaluated from top to bottom. For instance, imagine your background starts with a gradient followed by an image and ends with a solid color. If the email client doesn’t support layered backgrounds (like Yahoo and AOL) or gradients, then we will provide a fallback of the image over the solid background color. If images aren’t supported either, then just the solid color background will render. You can change the order by clicking the arrows to the left of the items in the Properties menu. Components that support backgrounds Some standard components support backgrounds, and you can code your custom components to have properties that support backgrounds too: For standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio., you can modify the background of boxes, columns, and sections. Consider whether you want a single area to have a background or the entire message: You can modify the background of your entire message in Message > Properties (gray above). A section runs the full width of the message. Set Background if you want the color (blue above) to span the width of the message. Set Content fill if you want the background color to only span the width of the content (red above). You set the width in Layout in the section’s Properties menu. For a custom componentA custom block of code with content and properties you can reuse across messages made in Design Studio., the component code needs a property with type background before you can modify its background in the Properties menu. You’ll add background styles to HTML elements with CSS. In the Properties menu, add your background under Advanced > CSS style. Check that your code doesn’t break across email clients. Add a shadow You can add a shadow to most standard components including containers like sections, boxes, and columns and content like buttons and images. To set a shadow: Click the component you want to modify. Under Styles, locate the Shadow field. Click Add. Then click the box to set your styles. The X property is the horizontal offset. The Y property is the vertical offset. The Blur property is the blur radius. As the value increases, the shadow lightens and expands. The edges become less sharp. The Spread property is the spread radius. This determines how large the shadow is. Negative numbers shrink the shadow. The Color property is the color of the shadow. Click the color preview to access more options, like your brand colors. % is the transparency of the shadow. The Position property is whether the shadow is inside or outside the component. Preview your changes and send a test to see how it renders in your email client. Here’s an example of a button with an outside shadow. On the left is the setup in the Properties menu and on the right is the preview of those settings. The shadow includes a vertical and horizontal offset of 5px. The blur and spread are 2px. The color is gray with 50% transparency. And it’s positioned outside of the button: Shadow settings Preview Set font weight When editing any font property, you’ll have the option to set a font weight. Not all fonts support the full list of font weights. If we don’t support the selected weight, you’ll see the next closest one. Set responsive styles You can set responsive styles for images, text, and containers as well as hide elements based on screen size. The breakpoint for desktop vs mobile is 600px. Learn how to swap images, hide content, and style based on screen size in our Responsive styles article. Set dark mode styles You can add dark mode styles for variables and images. Learn how to set dark mode styles in the visual editor. --- ## Add & preview liquid URL: https://docs.customer.io/journeys/liquid-visual-editor/ In Design Studio, you can personalize your emails using liquid, a templating language that lets you pull in data about your recipients, events, objects, and more. This article shows you have to add liquid using the Personalization panel in the visual editor. Use the Personalization panel to add the data you want into your messages without having to memorize our syntax or remember all of your attributes. After you add your liquid, enter Preview mode to check that your data renders as you’d expect! Add liquid You can add liquid to text-based standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio. in three ways: Using the Personalization panel, which helps you insert attributes without remembering syntax Manually, by typing liquid directly into the canvas Using the agent, which can add liquid to your message at your request  All Design Studio messages use our latest liquid You’ll notice two versions of liquid in our docs: legacy and latest. All messages made in Design Studio use our latest liquid. Add attributes with the Personalization panel To use the Personalization panel: Click the personalization icon in the canvas toolbar. A left-hand panel will open. Under Previewing As, find a person with example data you want to add to your message. Either choose an email from the dropdown or click the magnifying glass to search by their profile attributes. Under Data Type, choose what kind of data you want to add. Hover over an attribute and click . Choose how you want to add the data. If you click Add, we’ll add your liquid wherever your cursor is. Click Copy for more flexibility then paste it into a text area of your canvas.  Add or copy with a fallback so your messages don’t fail to send Emails fail to send if a recipient doesn’t have the data specified in the liquid. You can remedy this by including a fallback conditional or filter! Conditionally render content  Use the agent to create liquid statements! It can be a pain to create the conditions with all the logic, filters, and fallbacks you want. Prompt our agent to write the liquid for you! Then preview with sample data and ask the agent for revisions if needed. You may want to conditionally render content to localize your messages or customize your email’s responsiveness. You can use liquid inside a component to conditionally change content in your message. You can also use liquid outside a component to conditionally render components altogether. To conditionally render entire components, you’ll need to switch to the code editor. Here’s an example of how to conditionally render images: {% if customer.language == "es" %} <x-image src="image-spanish" /> {% else %} <x-image src="image-default" /> {% endif %} Note that the liquid conditional wraps around the components. Preview liquid To preview your liquid with sample data, turn on Preview in the canvas toolbar. Then click the personzalization icon if the left-hand panel isn’t already open. In Preview mode, you can view sample data, but not add any new liquid. Preview people who have your attribute data and who don’t so you know how your fallback content renders. From here, you can also preview across device sizes, with visual impairments in mind, and more. Review liquid errors If there’s an issue with your liquid syntax, you’ll see “Errors found” in red at the top of the preview. Click this to see a list of issues. Sample data won’t render if any of your liquid has errors. Messages with liquid errors will also fail to send, so make sure you fix any issue you find. Learn more about liquid errors and how to fix them. Understand data types You can add attributes stored on people, eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages., relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins., objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., or triggers for API-triggered broadcasts and transactional messages through the Personalization panel. Some data types may be disabled in the Personalization panel. This could mean: The person you’re previewing doesn’t have any data for that type. For example, they don’t have a specific profile attribute. The message isn’t connected to an automation that provides the data, like a campaign. Try switching to a different person who has the right data, or double-check that your message is connected to the correct automation. Profile attributes You can add profiles attributes to any Design Studio message. People data is available across any type of campaign, broadcast, and transactional message. After you add profile attribute data using the Personalization panel, you’ll see the liquid key customer on the canvas. Any other data—events, relationships to objects, trigger attributes, use their own keys. Events You can add event data after you connect to a campaign that’s triggered by an event. You can only add attributes from the trigger event; you can’t add any other event data performed by the person you’re previewing. When you navigate back to your Design Studio message and open the Personalization panel, we automatically surface a person who has performed the event in the trigger. Click the Data Type dropdown and choose Events to find available attributes.  If no person has performed the event in the last 30 days, you won’t be able to add event trigger data through the panel. Event trigger data uses the liquid key event. Learn more about event trigger attributes. Relationships You can add relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. and objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. attributes to any Design Studio message. This data is available across any type of campaign, broadcast, and transactional message. You must first set up objects to then relate them to people.  Review the limitations of object and relationship liquid Make sure you understand the limitations of object and relationship liquid before sending your message: referencing objects is relative, and you cannot reference more than 10 objects of the same type. Note, you can’t currently add trigger data for objects or relationship-triggered campaigns through the Personalization panel. But you can add it manually to a text-based component. When referencing trigger data for object or relationship-triggered campaigns, you’ll use the trigger key. For non-trigger data, you’ll use the objects and/or relationship keys. Trigger The Trigger data type only applies to API-triggered broadcasts. It’s easier to preview trigger data from the broadcast itself because you can add a test payload. In Design Studio, you can only filter for someone who’s already triggered your broadcast, which may take more steps. To preview data from previous sends in Design Studio, the message must be connected to a broadcast that’s already been triggered. Then in Design Studio, you can preview liquid with previously sent trigger data: Click in the canvas toolbar to open the Personalization panel. Choose Trigger under Data Type. Then select a previous send to pull in data from the previous 30 days. Click Preview to check your trigger liquid.  If no person has entered the connected API-triggered broadcast in the past 30 days, you won’t be able to add trigger data through the panel. Trigger data for API-triggered broadcasts uses the liquid object trigger. Learn more about trigger keys. Transactional The Transactional data type lets you test trigger data for transactional messages. To test transactional trigger data within Design Studio, you first need to connect your email to a transactional message. Then you can add attributes and values to the Personalization panel under Transactional Data (JSON). This represents the message_data object in the transactional API request. Turn on Preview in the canvas toolbar to see your liquid render with the sample data. You can also test trigger data within the transactional message editor. --- ## Get started URL: https://docs.customer.io/journeys/code-editor-overview/ In Design Studio code editor, you can create a message from scratch or edit a message made using the visual editor. Build emails using components, HTML, CSS, MJML, and AMP and validate your work with developer tools. This guide gives you a tour of the code editor so you can navigate the workspace, write and structure your message, and use built-in tools to validate and troubleshoot your work. The code editor has three main areas: File Manager: navigate between messages, templates, and components Editor: write your HTML, plain text, AMP, and transformer logic Preview: render your message and access developer tools for testing Navigate between your files To the left of the code editor, you’ll find the File Manager. You can switch between messages, templates, and components here. Hover over the panel and you’ll see a button to collapse or expand files. Code your message The Editor is where you’ll write your code. You can toggle between HTML, Text, AMP, and Transformers to write your message and automate repetitive processes. If you open your email and see the visual editor, click the three dots in the top right then choose Switch to Code to find this editor. HTML—Structure and style your message You’ll spend most of your time in the HTML tab, adding the content and styles of your message through HTML and CSS. This is also where you’d specify components. You can: Add standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio. by typing x- and choosing from the pop-up list. Add a custom componentA custom block of code with content and properties you can reuse across messages made in Design Studio. tag to insert reusable code. Text—Add a plan-text fallback Add a plain text version of your message in the Text tab. This is important for accessibility and deliverability. After you’re done crafting your message, click Generate and we’ll create a plain text version for you! You can modify the output as you see fit too. Note, generating text will overwrite any text already in the tab. AMP—Add interactive elements AMP enhances your email with dynamic content and interactivity. With it you can load dynamic content, build entire forms, or even include a checkout experience. Transformers—Automate coding actions Transformers are powerful tools that help you automate repetitive workflows during email development. You can use as many or as few transformers as you’d like. Transformers modify the code in the Source window of developer tools but do not affect your code in the main editor. Transformers are tied to each message; if you make changes to the transformers in one message, it won’t affect any others. Learn more about transformers: MJML framework CSS inlining Formatting URL parameters CSS cleanup CSS variables Accessibility fixes Editor actions At the top of the editor, you can: Manage versions - save a new version to share with your teammates or view version history Auto-format your code Toggle the preview At the bottom, you can modify the display of your code by changing the font size and spaces. You’ll also find a menu of keyboard shortcuts. Hover over your code to learn how compatible certain elements are with email clients using Can I Email. To turn off hover overlays, go to your editor preferences (Cmd/Ctrl + ,) then turn off the toggle. Minimap The minimap shows a zoomed out view of your code. We’ll highlight issues found with the problem checker so you can quickly navigate between them. To turn on the minimap, click Cmd/Ctrl + , to find code editor preferences then turn the toggle on. Translate your message You can add translations to any template or email in Design Studio. If you’re familiar with localization in our other editors, it’s the same process. Store people’s language preferences as an attribute on their profiles. Tell Customer.io what attribute you use for audience’s language preferences. Create your default message and add translated versions in the email editor. To add a language, open a Design Studio email, click next to the email’s name, and select Add language to get started. If you use our Auto-translate with AI tool, it will translate the following: Body text Subject lines Preheader text It will not translate: Liquid: Attribute values, filter values, any text rendered by a conditional, etc. Images, but it will translate alt text Snippets: Rather, you should add liquid conditionals based on people’s language preferences to the snippet file. Text in custom components: However, our AI tool will translate the text if you detach the component first, and then translate the message. Learn more about detaching a component from its source file. Learn more about setting a language attribute and translating content with AI in our localization guide. Preview your message On the right, use the Preview panel to see how your code would visually render. Above the editor, you can use the preview toggle to close and open it. You can also click the personalization icon to preview your message with sample data. You can preview your message on different screen sizes and considering accessibility needs. Capture a screenshot to share with your teammates for review. Developer tools Within code editor’s preview, you’ll find developer tools which help you validate your message. At the bottom of the preview, click the caret to expand or collapse the panel. Developer tools include: Inspect element Focus mode Expanded table view Source code Problems checker Link validator Image validator SpamAssassin Accessibility checker Learn more about validating your emails. --- ## Validate your email URL: https://docs.customer.io/journeys/developer-tools/ In Design Studio, you can use a number of tools to validate your email in the code editor preview. These tools help you catch code issues, debug layout and accessibility, validate links and images, and check your spam score. Click the caret to expand or collapse the panel. Inspect element Find the HTML source for any element in the preview using Inspect Element. Click the Inspect Element button in toolbar or enter Cmd/Ctrl + Shift + C. Click the element in the preview panel and the corresponding code will be highlighted in the editor. When you’re done, click the button (or keyboard shortcut) again to deactivate inspect mode. Focus mode Focus mode keeps the preview aligned with the code you’re working on. As you navigate your code, the preview will jump to the matching location and outline the relevant element. Turn Focus Mode on or off from the toolbar with Cmd/Ctrl + Shift + M. Expanded table view Use Expanded Table View to visualize table and cell boundaries and debug table-based layout issues. Since this view outlines all table structures in your message, it’s helpful when working with complex tables. Click the toolbar icon to turn it on or off. Source code The Source tab contains the compiled, final HTML code used to render the preview, including transformers. Click Source in the toolbar to access it. You can resize the window by dragging its edge up or down, left or right. When you export your email, the message includes the source code. Problems checker The problem checker analyzes your code and highlights issues. Click Problems in the toolbar to find a list of errors and warnings. The results automatically update everytime you stop typing in the editor. Click on any problem to locate that section of code. You can see where problems exist in your code in the scrollbar to the right of the editor.The red corresponds to the errors in the checker and yellow corresponds to warnings. Errors are likely to cause rendering issues in an email client while warnings are recommendations to improve code quality. You can disable parts of the problems checker. In code editor preferences (Cmd/Ctrl + ,), you can turn off the toggle for Code Checker or Spell Checker. Link validator In the Links tab, validate whether your links are working and going to the right websites. If the tab says In Sync, then the results come from the latest version of your code. Otherwise, click Refresh to get the latest results. Click on a link to open it in a new tab. If you use the same link multiple times in your message, the validator only lists it once. Image Validator In the Images tab, check that all your images are loading, secure, and optimized. If the tab says “In Sync,” then the results come from the latest version of your code. Otherwise, click Refresh to get the latest results. SpamAssassin SpamAssassin is an open-source tool that helps you identify how likely your message is to be filtered out as spam. Click SpamAssassin to view your message’s spam score. The lower the score, the better. Higher scores are more likely to be sent to spam. You can re-run the SpamAssassin check at any time by clicking Refresh. Accessibility checker Evaluate your email for accessibility issues and best practices in the Accessibility tab. Depending on the size of your message, the checker may take a moment to show results. It sorts issues by severity (Critical, Serious, Moderate, Mild). Many issues include a link on the second line with more information. You can also click an issues to locate the relevant line of code. Collapse any severity layer by clicking on its header. Rules We check many AXE 4.3 rules and a number of custom rules. Custom Rules Id Info Impact Description Help body-lang Checks that the content inside the body tag has a lang attribute Serious Content inside the body should be wrapped in a lang attribute Email clients will often remove <html> element body-dir Checks that the content inside the body tag has a dir attribute Serious Content inside the body should be wrapped in a dir attribute The language direction is often inherited from the email client, if it is different it will cause issues. table-role Check if tables have the role set to presentation Serious Tables used for formatting should have the role attribute set to “none” or “presentation”. Screen readers will read each cell of every table without the role of none or presentation. Use tables without role none or presentation only for tables of data role-button Check if the <a> tags have [role="button"] Critical Dont use [role="button"] on <a> tags Assistive technology users may not be able to find it in the links menu for the page, and may have difficulty clicking on it. accessible-hyperlinks Check if the link text is non-descriptive Moderate Link text should be descriptive. Learn how to write more descriptive links article-article Check for HTML semantic elements and suggest replacing with div elements with comparable roles Mild Use <div role="article"> instead of <article> HTML5 semantic tags are not reliable. region-section Check for HTML semantic elements and suggest replacing with div elements with comparable roles Mild Use <div role="region"> instead of <section> HTML5 semantic tags are not reliable. navigation-nav Check for HTML semantic elements and suggest replacing with div elements with comparable roles Mild Use <div role="navigation"> instead of <nav> HTML5 semantic tags are not reliable. banner-header Check for HTML semantic elements and suggest replacing with div elements with comparable roles Moderate Avoid using the banner landmark in an email The email client webpage likely already contains a banner landmark. contentinfo-footer Check for HTML semantic elements and suggest replacing with div elements with comparable roles Moderate Avoid using the contentinfo landmark in an email The email client webpage likely already contains a contentinfo landmark. main-main Check for HTML semantic elements and suggest replacing with div elements with comparable roles Moderate Avoid using the main landmark in an email The email client webpage likely already contains a main landmark. complementary-aside Check for HTML semantic elements and suggest replacing with div elements with comparable roles Moderate Avoid using the complementary landmark in an email The email client webpage likely already contains a complementary landmark. h1-tag H1 Tag(s) Moderate Email should have exactly one h1 tag (but has more than one) Most email web clients don’t add an <h1> tag to the page. vml-alt Checkes that VML elements have an alt attribute set Critical VML requires an alt attribute VML is an image format so requires an alt attribute to set alternative text. vml-links Checkes that VML elements do not contain <a> elements Critical Avoid placing links inside VML elements Links inside VML can’t be navigated to of clicked on using a keyboard input or some screen readers. vml-headings Checkes that VML elements do not contain heading elements Moderate Avoid placing headings inside VML elements Headings placed inside VML elements can’t be navigated to using some screen readers. --- ## MJML framework URL: https://docs.customer.io/journeys/transformer-mjml/ [MJML](https://mjml.io/) is a markup language that makes it easier to code responsive emails. In Design Studio, our code editor supports MJML so you can import existing emails with this syntax. This language is not fully supported by our visual editor though. We recommend building your email from [components](/journeys/components-overview/) when possible! Add MJML to code editor To use MJML in your message: Go to Transformers > Framework and turn on MJML Or, append your message file with a .mjml extension. If you create a message from scratch ending with .mjml, the editor will auto-populate this template to help you get started: <mjml lang="en"> <mj-head> <mj-title>Subject goes here</mj-title> <mj-preview>Preview text goes here</mj-preview> </mj-head> <mj-body> <!-- Content goes here --> </mj-body> </mjml> Add HTML to MJML ending tags Ending tags, like <mj-raw>, let you add HTML inside an MJML block. Text within HTML won’t be escaped. Check out MJML’s documentation for more information. <mjml> <mj-head></mj-head> <mj-body> <!-- content goes here --> <mj-raw> <div> text <img src="example.png" alt="sample image" /> </div> </mj-raw> </mj-body> </mjml> Use MJML in components You can create components in MJML by adding .mjml to the filename. You aren’t limited to using MJML though. For instance, you can add standard or custom components to MJML ending tags. <mjml> <mj-head></mj-head> <mj-body> <mj-raw> <x-paragraph></x-paragraph> <custom-component></custom-component> </mj-raw> </mj-body> </mjml> While you can create components in MJML, you can’t add MJML inside a component. --- ## CSS inlining URL: https://docs.customer.io/journeys/transformer-css-inlining/ In the code editor of Design Studio, use the CSS inlining transformer (aka [CSS pre-processing](/journeys/css-pre-processing/)) to convert embedded CSS into inline styles. This can help improve page loading times and tells browsers to treat your styles as the most important.  Only use CSS inlining for emails made with standard HTML Enabling this transformer on emails with our standard components will not work well and will likely lead to unintended styles. Click Transformers > CSS Inlining then turn on Inline CSS. Once enabled, you can customize what styling should be converted or preserved. The state of those toggles will be saved if you deactivate and later reactivate the transformer.  CSS inlining does not convert all styles in Design Studio It does not convert <style> tags with data-embed or data-ignore-inlining. It does not download and convert external stylesheets <link rel="stylesheet" href="....css" />. If you want to inline all styles, avoid using these attributes or linking to outside CSS files. --- ## Formatting URL: https://docs.customer.io/journeys/transformer-formatting/ In the code editor of Design Studio, use the Formatting transformer to apply your choice of formatting to the source code. Click Transformers > Formatting to get started. Format your source code Click Prettify to make your code easy to read. Clicking this unlocks further preferences you can enable: Indent with spaces or tabs Wrap HTML attributes We remember the state of those toggles if you deactivate and later reactivate them. To format the code in your editor, click Format Document at the top of the editor. Reduce the size of your email Click Minify to reduce the size of your source code when it sends. You can see the size of your email before and after at the bottom of the editor. Clicking this unlocks further preferences you can enable. We remember the state of those toggles if you deactivate and later reactivate them. Encode HTML entities Special characters (like ©), reserved characters (which would otherwise be interpreted as HTML code), and invisible characters (like non-breaking spaces) can cause bugs that are hard to track down in emails. To reduce these issues, turn on Encode HTML entities. We’ll automatically convert your charactres to HTML entities, which start with an ampersand (&) and end with a semicolon (;). For example, © becomes &copy;. Prevent widow words This transformer prevents the last words of paragraphs from being on their own lines, which can make your email look cleaner. --- ## Set URL parameters for a single message URL: https://docs.customer.io/journeys/transformer-url-parameters/ Add URL parameters through the code editor in Design Studio to help you track and analyze how your customers interact with your messages. You can define URL parameters in two ways: Across all messages in Workspace Settings For a single email made in Design Studio This article shows you how to add URL parameters to a single message made in Design Studio. If you set URL parameters in workspace settings and in a single message, we append all parameters to your links. One does not override the other, so make sure you don’t duplicate URL parameters across these locations. Add URL parameters to a Design Studio message Click and choose Switch to Code. Click Transformers > URL Parameters. Click the toggle to turn on the transformer. Add a key and value for each parameter. (Optional) Click Encode to turn off encoding for a parameter. Click to remove a parameter. Encode URL parameters By default, we encode each URL parameter. Encoding replaces special characters so they can safely be sent inside of links. If a URL parameter includes hello world, encoding replaces the space with %20, so the output becomes hello%20world. You might disable encoding if you escaped the parameter value before entering it into the transformer.  Check out our URL parameters feature With our URL parameters feature, you can set up a template of URL parameters and automatically append them to links in your messages using {% cio_link url:"https://example.com" %}. That way you don’t have to manually add parameters to each link! Ignore URL parameters defined in a message If you want a link to not contain message-defined URL parameters, add data-ignore-params. For instance, perhaps you link to your company’s X account and you don’t want to send the parameters to X. <a href="https://x.com/CustomerIO" data-ignore-params>Icon</a> The output for this would be: <a href="https://x.com/CustomerIO">Icon</a>  To disable workspace-defined URL parameters, add `class= Workspace-defined and message-defined URL parameters are enabled and disabled separately. This gives you more control over your links! --- ## CSS cleanup URL: https://docs.customer.io/journeys/transformer-css-cleanup/ In the code editor of Design Studio, use the CSS cleanup transformer to remove unused CSS and optimize your CSS selectors. Remove unused CSS Enable this transformer and we’ll automatically find embedded CSS classes or ids that aren’t in use and remove them. This only affects selectors with at least one class or id. If the selector only has attributes and tags (i.e. a[x-apple-data-detectors]), it will not be affected. In this code, the class i-never-get-used will be removed since no HTML tags use that particular class. <html> <head> <style> .i-am-used { background: blue; } .i-never-get-used { background: red; } </style> </head> <body> <h1 class="i-am-used">Hello world</h1> </body> </html> Shorten CSS selectors After you enable Remove unused CSS, you can enable Shorten CSS selectors. This shortens your id and class values. Continuing with the previous code example, the class i-am-used is pretty long — 9 characters. This transformer would shorten it to a single character to keep your code as small as possible. After transformer: <html> <head> <style> .w { background: blue; } </style> </head> <body> <h1 class="w">Hello world</h1> </body> </html> Allowlist values Set an allowlist to prevent us from cleaning up specific classes and ids. For instance, you’ll want to do this for links in Outlook: span.MsoHyperlink { color: inherit !important; mso-style-priority: 99 !important; } The name shouldn’t be shortened since Outlook sets it, and the class shouldn’t be removed, because even though it doesn’t appear in our code, Outlook will add it to your HTML. By default, Design Studio will not remove or rename any classes mentioned in these reset files: Modern Email Resets and Old Email Resets. Ignore templating language patterns If your code uses a templating language like liquid or handlebars within class names or ids, the CSS Cleanup transformer may incorrectly modify them because it treats all words as part of the class. By default, when CSS Cleanup is on, we ignore values within {{ }} and {% %}. You can add/remove syntax so we don’t clean up those values. --- ## CSS variables URL: https://docs.customer.io/journeys/transformer-css-variables/ In the code editor of Design Studio, use the CSS variables transformer to convert CSS variables to a format that most email clients support. CSS variables (also known as CSS custom properties) help you define reusable styles and reduce duplication across your stylesheets. However, most email clients don’t support them. The CSS Variables transformer solves this by inserting the values of the variables. Statically insert :root CSS variables Enable this transformer to convert your CSS variables into styles that are compatible with more email clients.  Define CSS variables on the :root element in a <style> tag We currently only support CSS variables defined in this way. We won’t process CSS variables on other elements or tags. For instance, if you set these variables: :root { --primary-color: #ff0000; --secondary-color: #00ff00; } h1 { color: var(--primary-color); } h2 { color: var(--secondary-color); } .gradient { background: linear-gradient( 0.25turn, var(--primary-color), var(--secondary-color) ); } Then this transformer would convert them to: h1 { color: #ff0000; } h2 { color: #00ff00; } .gradient { background: linear-gradient(0.25turn, #ff0000, #00ff00); } Preserve CSS variables Enable this if you want to maintain your CSS variables in your source code, but have us generate a fallback for email clients that don’t support CSS variables. --- ## Accessibility fixes URL: https://docs.customer.io/journeys/transformer-accessibility-fixes/ In the code editor of Design Studio, use the Accessibility fixes transformer to add or remove common tags and attributes that cause accessibility issues. Within Transformers > Accessibility fixes, enable the feature to set further preferences. You can customize how the Accessibility transformer behaves. For example, you can: Choose whether to autodetect the message language or define it manually. Automatically add lang and role attributes. Remove meta tags that prevent mobile users from zooming. These updates can make your message more accessible and reduce the need for manual fixes. --- ## Use Emmet syntax URL: https://docs.customer.io/journeys/code-editor-emmet/ The code editor in Design Studio supports [Emmet](https://emmet.io/), a shorthand syntax that expands into full HTML. It’s a fast way to generate repetitive structures like tables, links, or nested divs. For example, type this (don’t copy/paste): div>div>table>tr*3>td>a Then hit tab and it expands to: <div> <div> <table> <tr> <td><a href=""></a></td> </tr> <tr> <td><a href=""></a></td> </tr> <tr> <td><a href=""></a></td> </tr> </table> </div> </div> You can learn more about abbreviations in the official Emmet documentation. Specify child tags with > table>tr>td Becomes… <table> <tr> <td></td> </tr> </table> Specify sibling tags with + a+a+a Becomes… <a href=""></a> <a href=""></a> <a href=""></a> Multiply tags with * table>tr*3>td*2 Becomes… <table> <tr> <td></td> <td></td> </tr> <tr> <td></td> <td></td> </tr> <tr> <td></td> <td></td> </tr> </table> Add IDs with # a#my-link Becomes… <a href="" id="my-link"></a> Add classes with . a.text-blue Becomes… <a href="" class="text-blue"></a> Add IDs and classes together a#my-link.text-blue Becomes… <a href="" id="my-link" class="text-blue"></a> --- ## Use MSO syntax URL: https://docs.customer.io/journeys/code-editor-mso/ The code editor in Design Studio includes tools to help you write and manage Microsoft Office (MSO) syntax so your messages render more consistently in Outlook. Add conditional logic for Outlook You can add conditional logic that’s only parsed by Outlook through MSO comments. Syntax highlighting We visually distinguish MSO comments from regular comments to help you scan your email. For example, the following comment: <!--[if true]> <table role="presentation" width="100%" align="center" style="background:#EEE"> <tr> <td></td> <td style="width:600px;background:#ffffff;padding:24px;"> <![endif]--> becomes Disable syntax highlighting You can disable syntax highlighting for MSO comments in your editor preferences. Click Cmd/Ctrl + , then turn off the toggle. MSO comments will then show in grayscale instead. Auto-complete MSO properties In the code editor, type mso within a custom CSS property or style attribute and you’ll see a list of MSO CSS properties. Type until you find the one you want or scroll through the list. Then press Enter to auto-complete the property name. --- ## Preview email in Design Studio URL: https://docs.customer.io/journeys/preview-email-in-design-studio/ Preview your emails to make sure your messaging and styling work across different settings in people's browsers and email clients. Preview liquid Check out Add & preview liquid for more information on personalizing your messages with liquid data. Review links Click Review Links to make sure all links in your email work. You’ll see a list of valid links and those that need attention under “errors” and “warnings”. Access preview settings In the visual editor, you can preview a few settings while you edit: Desktop vs mobile views Light vs dark modes Content hidden based on screen size A more in-depth preview is available when you turn on Preview in the canvas toolbar. From this view, you can preview liquid, review across device dimensions, and simulate blocked images and visual impairments. Check responsiveness Preview your message at different heights and widths to make sure that your email is responsive on devices of all sizes. In the visual editor, turn the preview toggle on in the canvas toolbar. Open the dropdown menu. Choose between Desktop, Mobile, Responsive, or specific devices. You can also click + drag the right or bottom border to adjust the size of the preview window. Preview controls Click to preview your message with different visual settings. You can block images, change your color scheme, or adjust for visual impairments. Block images You can see how your message looks without images loaded by clicking Block images in the preview controls dropdown. Some email clients have remote content loading disabled by default, requiring email recipients to manually opt-in to image loading for each email. If your email contains crucial content inside images, the recipients may miss out on this information without images loaded. Being able to see this difference allows you to modify your emails so they still contain the information you wish to convey even when images are blocked. Always include alt text in case the images don’t load too so your recipients understand what the image was meant to convey. Block styles Some email clients don’t support <style> or <link> blocks and only render inline styles. To check how your message looks when they’re blocked, click Block styles in the preview controls dropdown. Use this feature to check embedded styles like responsive layouts, web fonts, and interactive styles (hover, checkboxes, radio buttons, etc). If you encounter an issue when styles are blocked, you may be able to modify your styles to improve your design: Column stacking—You’ll have to decide whether you want both the desktop and mobile experiences to stack horizontally or vertically. You won’t be able to specify responsive styling. Web fonts—These require styles in a <link> element, so there’s no way to maintain these fonts when clients block your styles. Hover styles—They require embedded styles, so there’s no way to maintain hover styles when clients block them. Learn more about which email clients support <style> and <link> elements. Color scheme Preview your color scheme to make sure your message will be visible to everyone in your audience. Choose between Light mode, Dark mode, or Forced dark mode. Forced dark mode Forced dark mode emulates color inversion of certain email clients. Some email clients—including Outlook and Gmail, will find areas of your email with dark text and light background and flip them to match the user’s Dark Mode preference. We don’t have full control over these cases, so it’s always a good idea to test your images and colors. You have two preview settings for forced dark mode: Forced dark (Outlook) and Forced dark (Google). Outlook Forced dark (Outlook) shows what your email will look like in these locations: Outlook webmail all versions Outlook progressive web app Outlook on Mac or Android It does not represent what you’ll see here: Outlook Windows desktop App Outlook iOS app Google Forced dark (Google) shows what your email will look like across a number of email apps on Android, including Gmail, Samsung, Yahoo, AOL, K-9, and GMX. It does not represent what you’ll see here: Webmail version of Gmail (this has no dark mode) Gmail iOS app Visual impairments You can simulate a variety of visual impairments to help you design inclusive emails and support your entire audience. You can filter for: Protanopia: Red-green Deuteranopia: Red-green Tritanopia: Blue-yellow Achromatopsia: Total You can only apply one filter at a time. Test your email in your inbox To request review from teammates without access to Customer.io, you can send them a test message, share a screenshot, and more! It’s also a good idea to send yourself a test message so you know how your email looks in your inbox. --- ## Connect an email to an automation URL: https://docs.customer.io/journeys/add-email-to-automation/ While you can [create a Design Studio email as a part of your campaign, broadcast, or transactional message](/journeys/file-manager/#create-from-an-automation), you can also [create the email first](/journeys/visual-editor-overview/) then connect it to an automation later, as described in this article.  You can only connect a Design Studio message to one automation. You can connect an email made in Design Studio to any campaign, broadcast, or transactional message, but only one. If you’d like to connect the same email to multiple automations, you can duplicate the email. Connect your message How you connect your Design Studio message depends on the type of automation you’re working with. Campaigns and API-triggered broadcasts Open your campaign or API-triggered broadcast. Drag an Email block into your workflow. Click Add Content in the email block. Under Use a Design Studio Email, hover over your email and click Use email. Your messages are displayed in order of how recently you edited them. (If you don’t see this option, click Try it out! in the top right to switch to Design Studio.)  You can connect emails, not templates. You can’t connect templates to automations, but you can create an email from a template. If you need to make a new email, click Create in Design Studio to get started. Newsletters and transactional messages Open your newsletter and go to the Content step. Choose Email as the message channel. Click the content block. Under Use a Design Studio Email, hover over your email and click Use email. Your messages are displayed in order of how recently you edited them. (If you don’t see this option, click Try it out! in the top right to switch to Design Studio.)  You can connect emails, not templates. You can’t connect templates to automations, but you can create an email from a template. If you need to make a new email, click Create in Design Studio to get started. Define the envelope If you didn’t fill out the envelope (From, To, etc) when creating the email, you can do so after you connect the email too. Preview your message Make sure you preview your liquid and test out your email with different accessibility settings: Learn more about adding and previewing liquid. Learn more about checking responsiveness, dark mode, and accessibility. Learn more about validating your code with our Developer Tools (you’ll have to switch to the code editor). A/B test your Design Studio message You can A/B test your Design Studio message in a campaign or newsletter. Each variation is a duplicate of the message you connected to your automation. Back in your Design Studio dashboard, all variations are grouped together under the original file name. The winning variant will have a trophy icon next to it. --- ## Edit connected messages & publish changes URL: https://docs.customer.io/journeys/publish-changes/ If you change a connected message, make sure you publish changes in Design Studio so your connected automation is up-to-date.  You’ll need to publish changes not just when you update the message in the editor, but also when you update a custom componentA custom block of code with content and properties you can reuse across messages made in Design Studio. file or global style used in the message. Edit your message Access the message from either the connected automation or directly in Design Studio. In either case, you’ll have full access to editing and previewing your message. Make sure you preview any changes to liquid with sample data, review your links, and see how your email renders with different inbox and accessibility settings. Publish your changes Design Studio automatically saves your edits to components, templates, and emails, so you won’t lose your work. However, your connected automation won’t reflect those changes until you click Publish in Design Studio, which gives you time to review and make updates safely before they go live. To push changes to automations (your campaigns, broadcasts, newsletters, and transactional messages), make sure you publish emails made with Design Studio. When publishing a Design Studio email, we compile any changes made to referenced components, global styles, and the email file itself. You can also publish multiple emails at once from the Design Studio dashboard. When updating custom components, you can decide whether to publish connected emails that include the component right away. Unlike messages and components, we do not automatically save changes to your global styles. You must decide whether to only save to Design Studio files OR save and publish to connected automations. After you publish your changes, we update the email in connected automations. If the campaign is running, drafted messages will immediately update with your latest, published changes. This is only relevant if your messages are set to “Queue Draft” instead of “Send Automatically”. Publish changes from a component When you need to update a component referenced in multiple messages, you can publish all emails connected to automations from the component’s page. You can immediately view changes to components in Design Studio emails that reference them, but to see changes in connected automations, you must publish the messages. After you make your updates, click Publish from the component’s page. Select one or more emails connected to your automations. Click Publish.  Publishing compiles all changes in selected emails If there are any unpublished changes in the selected emails, Design Studio will also publish those. For instance, if a teammate updated a paragraph and you go to publish global styles to that message, Design Studio will publish both the styles and the content update. Consider checking with your team before publishing to multiple files at once. Publish changes from global styles Unlike other files, we don’t automatically save changes to global styles, so you need to decide whether to save styles to Design Studio only or save AND publish to your messages and campaigns. From the main dashboard, go to Styles. After you make your updates, click Save Updates. Decide whether you’re ready to push your changes to connected automations. If you are, click Save and publish changes. Otherwise, click Save in Design Studio only. Select one or more messages. This list includes in-app messages and connected emails made in Design Studio that have at least one global style, regardless of whether the updated style impacts the message. So if you select all, you could be publishing no changes to some.  Publishing compiles all changes in selected emails If there are any unpublished changes in the selected emails, Design Studio will also publish those. For instance, if a teammate updated a paragraph and you go to publish global styles to that message, Design Studio will publish both the styles and the content update. Consider checking with your team before publishing to multiple files at once. Click Publish messages. Publish messages from your dashboard To publish messages with drafted changes, go to the Design Studio dashboard: Select one or more messages. Click Publish above the table. When you publish a message, we compile all changes to the email: updates you made to the email file, changes to the code of referenced components, and any changes to global styles. Discard your changes You can discard changes you haven’t published from Styles or your email. From Styles, click Discard. From your Design Studio email, click Publish and choose Discard unpublished changes. From a Design Studio email, the act of discarding changes only removes changes you made in the email file directly. To avoid publishing changes from referenced files, choose one of the following options: Remove the global style or custom component from the message. Disconnect the message from your automation and replace it with a different one. --- ## Disconnect an email from an automation URL: https://docs.customer.io/journeys/disconnect-from-automation/ You can disconnect an email from a campaign, API-triggered broadcast, newsletter, or transactional message made in Design Studio so you can use it elsewhere. You can also duplicate the message to use it in another automation. Disconnect message from an automation To disconnect a Design Studio message from a campaign or API-triggered broadcast, delete the email block from your workflow. To disconnect a Design Studio message from a newsletter or transactional message, you can either switch to another message type (then back to “Email” if you want a different email connected) or delete the automation and start over. After you disconnect the message, your message will update to Unused on your Design Studio dashboard. --- ## Understand components URL: https://docs.customer.io/journeys/components-overview/ Components help you reuse content and structure across your Design Studio messages so you can build faster. In Design Studio, you can use our out-of-the-box Standard Components and create your own Custom Components. Standard components Standard components are pre-built blocks that help you build beautiful, engaging messages as quickly as possible. They are the backbone of Design Studio messages — headings, paragraphs, buttons, sections, and more. You’ll see a list of available standard components in the insert menu of the visual editor. Once you’ve dragged a component onto the canvas, you can style it by adjusting the component’s properties. Our standard components are: Accessible: use our accessibility checker to ensure you’re following best practices. Responsive: they scale down automatically to fit smaller viewports, but you can also change this for some standard components like columns. Able to inherit your styles: for instance, if you nest multiple paragraph components within a box component, the paragraphs would inherit the text styles of the box. Lightweight: to reduce code bloat. Flexible: you can modify standard properties or add your own under Advanced in the Properties menu. We created our standard components following the Email Markup Consortium compliant standards to help you make flexible, lightweight code. We maintain and update these components to ensure they’re compatible with the latest email client quirks and bugs. Custom components Custom components are pieces of messages you can save and reuse—like headers and footers. Creating a custom component saves you time so you don’t have to re-create parts of messages, and helps you maintain consistency across messages. Custom components help you: Build efficiently: create a reusable block once then easily find it to reuse across future messages. Cascade changes from one source of truth: update your component file so changes cascade to all messages using it. Streamline team collaboration: Developers can create reusable code in JavaScript, HTML, and CSS, and marketers can drag them into the visual editor with no coding required. Create reusable, interactive features: Interactive messages are time consuming to make, but with custom components you can make an interactive feature (like a survey) once, and reuse it as many times as you see fit. --- ## Add components to your message URL: https://docs.customer.io/journeys/insert-components/ Add components to the visual editor of Design Studio to start building your message. After you've dragged a component to the canvas, you can adjust the settings to modify its appearance. Insert components You can add standard or custom components to your message by dragging them onto the canvas from the visual editor’s Insert menu. Standard Components appear at the top of the menu. Collapse the folder and you’ll see Custom Components immediately below. If you haven’t created custom components, you’ll only have the option to drag in standard components. Click and hold any component then drag and drop it onto the center canvas. If you switch to the code editor, you can type x- to choose from a list of available standard components. Modify content Once you’ve dragged a component to the canvas, you can start modifying the text or asset. For example, if you drag a Paragraph component, you can start typing to add text to your message. Add an image After you drag an Image component onto your message, select a file from assets you’ve already uploaded or upload an image that is 2 MB or smaller, with a width and height no greater than 4096 px. You can also add a hosted image to Source in the Properties menu. For best performance, you should use .jpg, .png or .gif file types. Some email clients don’t fully support more modern formats. Add space We recommend you use margin or padding properties where possible to add space between content. However, if for any reason those properties don’t fulfill your needs, you can use the Spacer component to add vertical spacing. Edit component settings Click a component on the canvas and you’ll see a menu where you can perform actions like duplicating or deleting it. You can also highlight text to quickly change basic styles. Click the pencil icon to open the Properties menu on the right. For standard components, you’ll see a list of common settings like color or font family and can also scroll down to Advanced to add your own inline CSS properties. For custom components, you can modify settings as defined in their source files. --- ## Understand and style standard components URL: https://docs.customer.io/journeys/standard-components/ Standard components come with a predefined set of properties you can modify to customize and brand your Design Studio messages. We also offer some standard components behind the scenes to help you structure your email, enhance accessibility, style imported emails, and import Google fonts. How styling works Standard components pull from your global styles so you can create a consistent brand across your messages, but you can individually overwrite these styles too. To style a standard component, click the component on the canvas of the visual editor to open the Properties panel. Component properties are generally broken down into three categories: general, layout, and styles. General includes overarching properties like links. Layout includes spacing, sizing (margin and padding), and alignment properties. Styles includes text styles, colors, borders, and more. You can choose whether to hide each standard component on desktop or mobile devices. For columns, you decide whether to show or hide the entire column block, not each column within the block.  Dividers do not pull from global styles automatically. But you can update your dividers to use global colors and radii when editing a message. Dark mode and responsive styles You can specify responsive or dark mode styles for content and containers. Properties with mean you can add desktop and mobile styles. The breakpoint is 600px. Properties with support dark mode styles. Learn more in our Responsive styles and Dark Mode articles. Add and style content Style text, links, and images to look exactly how you want. Button A button is a CTA (call to action) which is a key component for driving users to click through to the main landing page of your message. It’s a link styled to look like a button.  To preview the link from the visual editor, you must include the protocol, like https://, in the link. Under Styles in the Properties panel, you can customize the button in a number of ways, including the following: Add a Hover Effect—what a message recipient sees when they mouse over the button. Hide the button on desktop or mobile devices. Under Advanced, add custom code without navigating to the code editor. Add a CSS Class or Style attribute. Add a Data Attribute. If your email setup requires specific HTML attributes on buttons that our other settings don’t cover, you can add a key/value pair here. In the code editor, a standard button is defined by the <x-cta> tag. List of CSS button properties LabelTypehrefstring widthstring heightstring paddingstring marginstring alignenum One of: left, center, rightbackgroundstring opacitynumber border-radiusstring border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring hover-colorstring hover-backgroundstring hover-opacitynumber hover-box-shadowstring hover-border-radiusstring colorstring font-sizenumber font-familystring font-weightstring One of: 300, 400, 700text-alignenum One of: left, center, rightline-heightnumber text-transformstring One of: none, capitalize, uppercase, lowercasetext-decorationstring One of: none, underline, line-throughclassstring stylestring Headings In the visual editor, when you drag a Heading component onto the canvas, it defaults to Heading 2 (the equivalent of <h2> in HTML). You can switch from Heading 1 to Heading 6 or convert it to a paragraph by clicking the component then selecting a style from the dropdown menu. In the code editor, headings are defined by <x-heading-#>. List of CSS heading properties LabelTypemarginstring colorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, righttext-transformstring One of: none, capitalize, uppercase, lowercasetext-decorationstring One of: none, underline, line-throughlangstring direnum One of: ltr, rtl, autoclassstring stylestring Image Use the Image component to add media to your message. Your file should be: 2 MB or smaller 4096px or less width and height a hosted image or file with the extension .jpg, .png or .gif In the Properties panel under General, include alt text to make images accessible for screen readers. Modify their appearance under Alt Text Style. With Image Set, you can provide multiple versions of an image and let email clients choose the best one based on your recipient’s screen size. Learn more about the srcset and sizes properties. Like with buttons, you can define a separate style when someone hovers over the image and hyperlink the image too. In the code editor, this component is defined by <x-image>. List of CSS image properties LabelTypesrcstring hrefstring altstring widthstring marginstring alignenum One of: left, center, rightopacitynumber border-radiusstring border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring hover-opacitynumber hover-box-shadowstring hover-border-radiusstring background-colorstring colorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700letter-spacingnumber line-heightnumber text-alignenum One of: left, center, righttext-transformstring One of: none, capitalize, uppercase, lowercasetext-decorationstring One of: none, underline, line-throughclassstring stylestring srcsetstring sizesstring Lists You can create unordered and ordered lists from the Lists component. You can modify List Type to change between numbered or bulleted items. In the code editor, lists are defined by <x-list> and <li> tags. List of CSS lists properties LabelTypelist-stylestring One of: disc, circle, square, decimal, decimal-leading-zero, lower-alpha, upper-alpha, lower-roman, upper-roman, lower-greek, lower-latin, upper-latin, armenian, georgian, hebrew, hiragana, hiragana-iroha, katakana, katakana-irohacolorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, righttext-transformstring One of: none, capitalize, uppercase, lowercasetext-decorationstring One of: none, underline, line-throughclassstring stylestring Navigation Add a menu of buttons that link to your website using the Navigation component. By default, it includes three buttons for Home, About, and Contact. Click each button to open the properties panel and add your company’s links. To adjust the number of buttons, click any button, then choose Duplicate or Delete from the pop-up menu. In the layers panel, all buttons are nested under Navigation. Click either Navigation or an individual Button to open the Properties menu. Within the outer Navigation properties, adjust the horizontal or vertical spacing between the buttons using the Gap setting. Within a button component, you have access to the same settings as a standard Button. In the code editor, this component is defined by an outer <x-wrap> and inner <x-cta> standard components. Paragraph The Paragraph component adds a block of text to your message. You can change a paragraph component to a heading component in the dropdown above the textbox on the visual editor canvas. In the code editor, a paragraph is defined by the <x-paragraph> tag. List of CSS paragraph properties LabelTypemarginstring colorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, righttext-transformstring One of: none, capitalize, uppercase, lowercasetext-decorationstring One of: none, underline, line-throughlangstring direnum One of: ltr, rtl, autoclassstring stylestring Social links Link out to your social media accounts using the Social links component. This is commonly used in email footers. By default, it includes icons for Facebook, X, Instagram, and LinkedIn. Click each image to open the properties panel and add your company’s links. To adjust the number of icons, click one then click the duplicate or delete button from the pop-up menu. In the layers panel, all buttons are nested under Navigation. Click either Navigation or an individual Image to open the properties panel. Within the Navigation properties, adjust the horizontal or vertical spacing between the buttons using the Gap setting. The default images work well in both dark and light color schemes. If you want to surface different icons based on recipients’ color scheme settings, check out how to code for dark mode. In the code editor, this component is defined by an outer <x-wrap> and inner <x-image> standard components. HTML Add an HTML block if you want to add custom code like form embeds or hosted videos to your email. In the Properties panel, you add your HTML. You also have the option to hide it on desktop or mobile. Send a test message to make sure your HTML compiles as expected. Manage links You can add links to text as well as components with images or buttons, like the Navigation block, to encourage people to take action. Select the component in the visual editor then find Link in the Properties menu. Choose the right type of link from the dropdown: URL, Email, or Phone number. URL opens a link to a webpage. This is also how you add standard links—common liquid tags for unsubscribing and viewing the email in a browser. Email opens a recipient’s email client. You can specify a sender address, subject line, and email template. Phone number asks to start a call. To preview your links, turn on the Preview toggle in the canvas toolbar then click your components or hyperlinked text. Standard links Standard links are just that—common links needed for accessibility and compliance: view in browser and manage subscription options. Within a Link field, click Standard links, and then choose the relevant link type. This adds a liquid tag which you can preview in the editor or when you send a test message. Learn more about these liquid tags below: Unsubscribe View in browser Manage subscription preferences Disable link tracking for specific links Links in your emails inherit tracking settings in your automations—campaigns, broadcasts, etc. By default, link tracking is on. Turn off Track link if you don’t want to track clicks for a specific link: For text in a standard component, like Heading and Paragraph, highlight the text, click the link icon, then turn off the toggle. Remember to click Save. This adds class="untracked" to your link. For standard components, like Image, Button or Social Links, click the component on the visual editor to open the Properties panel and turn off the toggle. This adds :track-link="false" to your component. If your email is connected to an automation, remember to click Publish to push your changes. Separate and space out components Use dividers and spacers to create visual breaks and improve message flow. Divider Use a Divider to separate content with a horizontal line. In the Properties panel, you can adjust the color (Fill), size (Height and Width), border style, radius, and more. In the code editor, a divider is defined by <x-hr>. List of CSS divider properties LabelTypemarginstring background-colorstring heightnumber widthstring border-radiusstring alignenum One of: left, center, rightclassstring stylestring Spacer We recommend you use margin or padding properties where possible to add space between content. However, if for any reason those properties don’t fulfill your needs, you can use the Spacer component. The spacer component has a fixed width, dependent on the component it’s nested within. If you dragged a spacer into a section, it would span the width of the section. If you dragged a spacer into a column, it would span only the width of that column. Adjust the size in the Properties panel to add or subtract vertical space. This does not inherit global styles. In the code editor, a spacer is defined by <x-spacer>. List of CSS spacer properties LabelTypesizenumber classstring stylestring Organize and style your message layout Use Sections, Boxes, and Columns to style multiple components in your message. A Box groups related content together with a single background color. Columns separate content horizontally, and you can set an outer background color on Columns then inner background colors on each Column. A Section spans the full-width of the message and lets you apply both outer and inner background styles for high-level grouping and styling. Box Use the Box component to group and style components that should stand out from the rest of your message content, like a footer or an offer. All components inside the box component will inherit its text styles. This is a slightly simplified version of the section component. Unlike section, you only set a background property. You can set a full-width background image, gradient, or solid color to make the box stand out from other content. Find Box Fill in the Properties panel to get started. Under Advanced > Accessibility, you can add semantic meaning to your box by adding ARIA landmarks. Screen readers use them to help people navigate your email. You have to set both Role and Label to use them in your email; we ignore them if only one is set. While these landmarks are intended to improve accessibility, they can easily make accessibility worse if not used properly. Keep the following in mind when using ARIA landmarks in your emails: Make sure Label describes the email content in some way so it’s differentiated from other navigation elements that people see when opening the landmarks menu of their screen reader. Make sure you set the right Role; for instance, Article isn’t just a news article but rather any standalone bit of content. Use them to help readers navigate main sections of your email. Adding too many roles and labels throughout your email can bloat the landmark menu and distract people from the most important parts of your message. In the code editor, a box is defined by <x-box>. List of CSS box properties LabelTypewidthstring heightstring paddingstring marginstring alignenum One of: left, center, rightbackgroundstring opacitynumber border-radiusstring border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring colorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, rightlangstring direnum One of: ltr, rtl, autolabelstring roleenum One of: article, region, navigationclassstring stylestring Columns Use this to add columns to your layouts. Click a column to style it individually or click Columns under Layers to adjust the number of columns, gap, break point, etc. You can set a background image, gradient, or solid color to differentiate the outer and/or inner columns from other content. In the visual editor, select the component to view the Properties panel. Click the plural Columns under Layers for the outermost columns component. Then set Fill under Styles. Click a column to set the background fill for an individual column. We recommend you don’t use Columns to create a single column layout as that will add unnecessary code. Instead, use the Box component. In the code editor, columns have two component tags: an outer x-row component and inner x-column components. The x-row component is a required container; without it, the x-column components will not work. It’s important that the number of columns defined in the :layout property of the x-row component match the number of x-column components. For instance, in the visual editor, if the count in the Columns Properties panel is four, then the layout property of x-row must reflect four like :layout="[25,25,25,25]. In this case, each column is 25% of the row’s width. List of CSS outer columns properties LabelTypelayoutunknown gapnumber widthstring paddingstring marginstring alignenum One of: left, center, rightbackgroundstring opacitynumber border-radiusstring border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring break-pointnumber fallbackenum One of: single, multicolorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, rightclassstring stylestring List of CSS inner column properties LabelTypepaddingstring backgroundstring vertical-alignenum One of: top, middle, bottomopacitynumber border-radiusstring border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring classstring stylestring Stack columns By default, columns stack on narrower screens (at or below a width of 600px). You can change this by either clicking so columns display side-by-side on smaller screens or adjusting the break point at which columns stack. To adjust the break point at which columns stack: Click Columns under Layers. In the Properties panel, click Edit next to Stack Columns. Set the pixel width at which columns stack under Break point. By default, columns stack at or below 600 px. (Optional) Change the fallback for clients that don’t support responsive styles. By default, columns stack on narrower screens, but you can change this to Multi column to prevent stacking. In the code editor, you can add a :break-point property as well as a fallback property for email clients that don’t support break points. When you set fallback to multi, the columns will stay in a column layout. When set to single, the columns will stack on top of each other. Reverse columns on narrower screens Enable the toggle Reverse top and bottom columns to reverse the column order on narrower screens. Click Columns under Layers. In the Properties panel, click Edit next to Stack Columns. Enable the toggle Reverse top and bottom columns. Click to switch to mobile view and preview changes. For instance, the right-most column in desktop view would become the top-most column in mobile view. A note on accessibility: people navigating with a screen reader, keyboard or other tabbed input on a narrower screen or preview panel likely won’t experience the reversed order. For example, let’s say the order of columns is A, B, C and you enable reverse columns for narrower screens (C, B, A). If someone is tabbing through a narrow preview panel in Gmail, the navigation would go from A to C, where A is at the bottom and C at the top. So the navigation would appear to jump to C then backwards to B then A. While we’ve implemented a fix for this, not many email clients support it at this time. Section This component adds a full-width section you can use to separate content on a page. This is commonly used as a top-level component, inside the base component, to create a central column of content in the message. You can create multiple sections and apply different backgrounds to each for better visual separation. You can set a background image, gradient, or solid color to separate this section from other content. In the visual editor, select the component to view the Properties panel. You’ll see Background and Content Fill under Styles. Modify Background to cover the full width of the section. Modify Content Fill to set an inner color based on the width value you specify under Layout. Components nested inside <x-section> will inherit the section’s text styles. List of CSS section properties LabelTypewidthstring paddingstring marginstring border-radiusstring alignenum One of: left, center, rightouter-backgroundstring backgroundstring opacitynumber border-styleenum One of: none, solid, dashed, dottedborder-widthstring border-colorstring box-shadowstring colorstring font-familystring font-sizenumber font-weightstring One of: 300, 400, 700line-heightnumber text-alignenum One of: left, center, rightlangstring direnum One of: ltr, rtl, autolabelstring roleenum One of: article, region, navigationclassstring stylestring Structure your code with standard components The structure of the body and head of your message is defined by the standard components <x-base> and <x-head>. Any message made using the visual editor automatically has an <x-base> component that you can style by clicking Messages under Layers. Base The Base component is the equivalent of a blank page. When you build a message from scratch using the visual editor, we add an <x-base> tag to your code after you drag your first component onto the canvas. Switch to the code editor and you’ll see it wrapping all components in your message. Each message should only use one base component, and it should be the first component, wrapping all content inside it. This standard component sets the <!doctype html>, <html>, <head> and <body> tags for your message.  Style the base component in the visual editor Though you can’t drag and drop this component from the Insert panel, you can style it in the visual editor—click Message under Layers and select the Properties tab. The default text styles of your base component inherit your global paragraph styles, but you can overwrite these and modify other properties from the editor. Your message inherits all text styles set on the base component, unless you override the settings on the nested components. To set a background color, gradient, or image, modify the background property. Make sure you set a fallback color for email clients that don’t support background images or gradients. List of CSS base/message properties LabelTypetitlestring preheaderstring backgroundstring colorstring font-familystring font-sizenumber font-weightstring One of: 100, 200, 300, 400, 500, 600, 700, 800, 900line-heightnumber text-alignenum One of: left, center, rightlangstring direnum One of: ltr, rtl, autoclassstring stylestring Head In the code editor, add the <x-head> component to pass additional content into the <head> tag of your messages. This is most commonly used to pass in custom <style> blocks, but you can also add <link>, <meta>, or even HTML comments. You can add as many <x-head> components as you need and place them anywhere in your code. We pass the content of the <x-head> component directly to the head without any processing, so style blocks won’t be combined and deduplicated like they would be in a custom component. Add context for accessibility In the code editor, you can add <x-hidden-text> to enhance accessibility. Any text within the component is available to screen readers and text-to-speech tools, but visually hidden in your messages. This allows you to provide additional context for things like link text, without cluttering the visual appearance of your message. Style HTML like standard components If you made your email outside of Design Studio, it likely doesn’t use our component language. We’ve made it possible for you to edit and style many HTML tags from the visual editor, but some you’ll have to take an extra step to edit them. For <div>, <span>, and <table> tags, switch to the code editor and wrap the text inside those elements in the standard component: <div style="font-size: 12px;"> <x-edit-text> This is a div that you can edit in the visual editor. </x-edit-text> </div> Then highlight the text back in the visual editor and you’ll be able to edit basic marks like bold and italics, change the text color, or hyperlink the text. Import Google fonts In the code editor, use <x-font-family> to import Google fonts if you aren’t solely using standard components to build your message. Include this in any custom component that would allow the user to set a font family. It will look over a font stack, then, if any of the fonts included are Google fonts, it will import them into the template. <x-font-family font-family="Roboto, sans-serif"/> <p style="font-family:Roboto, sans-serif">text</p> List of CSS font family properties LabelTypefont-familystring --- ## How to create & edit a component URL: https://docs.customer.io/journeys/create-custom-component/ In Design Studio, you can create custom blocks, like headers and footers, to reuse across emails to save you time, ensure brand consistency, and reduce errors. Overview You can create a custom component to reuse across emails in a few ways: Create and save a component from the visual editor Prompt our GPT to generate your component Create one from scratch using our templating language Save a component from the visual editor Create and save custom components from the visual editor to reuse in subsequent emails. This way you can create things like headers and footers once, without having to write HTML/CSS! In the visual editor, build a block on the canvas. You can use any standard component (heading, image, button, etc.), existing custom component, or HTML block to build a custom component. Select the outer most component of the block you want to save. Click to save it as a custom component file. Choose an icon and name you’ll use to locate the component in your files and the visual editor. Specify which section (if any) the component should show under in the Insert menu of the visual editor. Click Continue to finish. You can find your new component in Files on your dashboard. Drag it into your Design Studio emails from the Insert menu.  You can only create locked components from the visual editor A component saved from the visual editor won’t have custom properties or slots—areas in your email that your team can customize. If you need those, save your component, go to the file, copy the code, and ask our GPT help you customize the code further. Prompt our Design Studio GPT to create your code We’ve created a GPT that can code your component for you! The Design Studio GPT is best for: Creating a Design Studio-compatible component from raw HTML Creating a component from scratch using natural language Configuring guardrails for a component (like defining properties that people can edit in the visual editor or placeholders for content) Troubleshooting issues with a component ChatGPT can make mistakes. Results are not guaranteed to work as expected in the Design Studio. Please reach out to win@customer.io with any questions. Create a custom component with your own code If building a component from the visual editor or by using our GPT doesn’t meet your needs, you can also create a component from scratch using our templating language. In your Design Studio dashboard, click Create then New Component to get started. Give the file a name that will help your teammates find it in the visual editor’s Insert menu. Code your component. Adjust <script> to include the variables and properties needed. Adjust <template> to include the content needed. Then your teammates can drag the component into a message or you can use it as content in other components. If your teammates should not be able to access this component from the visual editor, you can hide it. Edit a component You can edit a component in a few different ways: Edit the source component file so changes apply to all messages Detach a component from the source so changes only impact a single message Learn more about deleting components. Edit the source file You can edit the source file of a component from Files or by clicking your component from the visual editor and choosing Edit source. Changes you make to the source file automatically apply to messages the component is referenced in, but not workflows. If your message is connected to a campaign, newsletter, or other workflow, you must publish your changes for them to take effect. Edit the component in your email If you want to create a unique instance of your custom component, you can detach it from its source file. This lets you use the component like a template—bringing in the source material, and then modifying it as you see fit for the specific email. If you want to translate the text of a component, you must first detach it from its source, and then translate the message. Select the component in your email. Click Detach component. Modify your component. The component expands into its individual blocks, which you can modify within the email.  You can’t publish changes to a detached component Any changes to the original source file will not apply to this component moving forward. And editing this detached component has no impact on the source file either. --- ## Create a component from scratch URL: https://docs.customer.io/journeys/code-custom-component/ In Design Studio, organize your code into reusable blocks with custom components.  New to custom components? If you’re new to custom components, check out how to build them using visual blocks or our GPT. This may save you some time! Understand the structure There are two required tags when setting up a component: <script> and <template>. The <script> tag is where you define properties for styles, slots for modifiable content, and other JavaScript logic. The <template> tag is where you create your content and reference variables and slots. When you create a component from scratch, we provide the following code: <!-- Insert this component in your email with the following code: <example-component></example-component> --> <script> export const config = { label: "example component", presets: [ { label: "example component", content: `<example-component></example-component>` } ] } // export const slots = Component.defineSlots({ // default: { // schema: Component.slots.text(), // }, // }); // export const props = Component.defineProps({ // }); </script> <template> <p>Content goes here</p> </template> In the <script> tag, use the config variable to define how the component is referenced in the visual and code editors - there are a number of names and labels to keep in mind. We also include code that will help you get started defining slots for modifiable, placeholder content and properties for styles. Learn more about optional top-level elements—<style>, <title>, <link>, <meta>. Presets Presets define the name of the component and what gets inserted into the editor. presets.label is the name you’ll see in the insert menu. When you drag a custom component into your message, we insert the value of presets.content into the code editor. With this template, we’d insert <example-component></example-component> into the code and render the content of the <template> in the final source code. Hide from the insert menu To hide a custom component from the Insert menu, delete the presets array of the <script> tag. export const config = { "label": "My paragraph", presets: [ { label: "My paragraph", content: `<my-paragraph></my-paragraph>` } ] } You’ll still be able to add the custom component to the code editor and include the component as content in other component files. Removing this label will not change any existing reference in your messages. Names and labels In a component file, you’ll find a number of names and labels: Filename - visible in the Design Studio dashboard, what we draw from to label your component when you first create the file. Component Tag - located at the top of the component file. To define parent and child relationships, you’ll reference the component tag. Defines the name of the component tag visible in the code editor. This must match the component tag name referenced in presets.content. The name can’t be an HTML element, like h1, or start with our standard component naming convention: x-. config.label - appears in the Layers menu of the visual editor, also appears when you hover over the component on the canvas. presets.label - appears in the Insert menu of the visual editor. Component tag name validation Component tags follow the same rules as custom HTML elements with these additional restrictions: Maximum 100 characters Must start with a lowercase ASCII letter (a–z) Must not start with a hyphen or a digit Must not end with a hyphen Must not contain uppercase letters Must not contain consecutive hyphens (-) May only contain lowercase letters, digits, and hyphens: [a-z0-9-] Must not start with x- (reserved prefix) Must not be a standard HTML element name (e.g. div, span, table) Must not be a reserved Web Components spec name: annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph Must not be a reserved Design Studio name: context, fetch, slot, fragment, microlink, template, gretchen, component, email, scrape How changes to a component file cascade to emails If you update a component file, those changes will cascade to your Design Studio messages. Whether you update the <script> or <template> of your component, changes cascade to references. One caveat is the presets array. If you change presets.content, you must pull in the component again or directly modify the email to see the latest changes. Otherwise, any other change to definitions in your <script> or content in your <template> will immediately be reflected in your referenced messages.  Publish changes to your emails after you update components Updates first cascade to the message in Design Studio. Then you must click Publish to push those changes to connected campaigns, broadcasts, or transactional messages. Code your component When you create a custom component, you can decide how much control your teammates have over the content and styling in the visual editor. Add content By default, our out-of-the-box component code helps you build an uneditable block of reusable content—this might make sense for headers and footers to ensure consistency across emails. You’ll add content—standard components, custom components, and/or HTML elements—to the <template> tag.  You can use liquid directly in your components Your component’s <template> has access to all the same liquid variables available in your emails—like customer, event, trigger, and objects. You don’t need to pass these as parameters; just use them directly in your template code. For example, {{ customer.first_name }} works in a component template the same way it does in an email. Learn more about liquid in Design Studio. To create modifiable, placeholder content, you’ll add slots. You can create slots that are text-based or drop-zones for other components. Add styles By default, our out-of-the-box component code helps you build a block where styles are locked—this might make sense for headers and footers to ensure brand consistency across emails. You can also code components so that after your teammates drag a custom component onto the visual editor, they can choose between specific styles you laid out in the code. To do this, define properties in the <script> of your custom component. Otherwise, you can hard-code inline styles in the <template> tag. Create responsive styles You can use @media queries to define screenbreaks in a custom component or the media attribute. For Thunderbird and other email clients that don’t support @media queries, use the media attribute within an isolated <style> tag. Reference global styles You can use your global styles to define properties so your team stays on brand. You’ll reference them using globalStyles.<global-style-type>.<global-style-variablename-variableid>. This might look like globalStyles.colors.pink_khyqtq8v5rcs.  Use autocomplete to reference the global style Note the variable id needed at the end of the global style reference. You can find this by using autocomplete in the component editor. You can reference global styles in both properties and CSS to customize your components: As an inline style—best for quick, one-off styles: <div :style="`background: ${globalStyles.colors.pink_khyqtq8v5rcs};`"> As a CSS class (make sure you use the set() function) while creating a stylesheet As a property value in a style object As a variant for select and toggle properties—these property types let people in the visual editor choose the correct variant Define relationships between components You can define parent/child relationships between components such that team members using the visual editor can only drag/drop specific components into each other. Parents For instance, maybe you have a component social-icons that team members should only be able to insert into your footer component. In this case, the config code of the social-icons component should include allowedParents: <script> export const config = { "label": "social-icons", allowedParents: ['footer'], presets: [ { label: "social-icons", content: `<social-icons></social-icons>` } ] }; </script> Children If you want to specify that a component can only have certain child components, take advantage of slots, which let you create modifiable, placeholder content. The slots code would include this, where you’d replace social-icons with any Component Tag Name. <script> export const slots = Component.defineSlots({ default: { schema: Component.slots.children(['social-icons']), }, }); </script> Check out validation of slots for more info on child components. Troubleshooting issues If your component throws an error or fails to load after adding it to a Design Studio email, go to your component file and review your code. This often means you’ll need to resolve an issue with the config, props or slots definitions. Make sure you define your properties/variables and slots using: Component. notation in the <script> Only the schema we support Make sure you define any custom properties in the <script>. Ensure there’s both a <script> and <template> tag in your component file. Make sure all code is within our supported top-level elements. Make sure the component tag name at the top of your component file matches the tag name used in presets.content in your <script>. For example, if your component tag name is “custom-footer”, then presets.content must include <custom-footer></custom-footer>. --- ## Create modifiable, placeholder content URL: https://docs.customer.io/journeys/component-slots/ Add slots to determine what content you can modify in the visual editor of Design Studio. You can create text-based slots or drop-zones for components.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. How to create a slot To create a slot, define the type of slot in <script> and insert <slot> tags in the <template> where you want users to insert or edit content. You can also add placeholder text to guide your team when they’re working in the visual editor. To define a slot, export the slots variable for use in the visual editor and define your validation schema in the <script> tag. <script> export const config = { "label": "My paragraph", presets: [ { label: "My paragraph", content: `<my-paragraph></my-paragraph>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.text(), marks: ['bold', 'italic', 'underline', 'strikethrough', 'link'] }, }); </script> <template> <p> <slot>Add text</slot> </p> </template> In this example, we added validation for text through schema: Component.slots.text(),. You can also add validation for drop-zones for other components. Inside the <template> tag, you’ll add <slot> tags where you want teammates to edit text or drop in components. Within the <slot> tag, you can also define placeholder, fallback text. In this example, after you drag this component into the visual editor, you’ll see “Add text” in the visual editor until you edit it. You can also define multiple slots in a component file. Define your slot You can define slots in a few ways: As text - Component.slots.text() As a container for any other components - Component.slots.any() As a container for specific components - Component.slots.children(['component-name-1', 'component-name-2']) Add text that users can edit and format To add text, use Component.slots.text() in the schema field. Then, after team members drag this component onto the visual editor, they can edit the text and highlight words to change their marks. By default, you can edit these marks in the visual editor: bold italics strikethrough underline code superscript subscript link color To limit which marks are available, specify which ones you want to keep in a marks property. <script> export const config = { "label": "component-one", presets: [ { label: "component-one", content: `<component-one></component-one>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.text(), marks: ['bold', 'italic'] }, }); </script> <template> <p> <slot>Placeholder, fallback text</slot> </p> </template> This is the full list of marks available: marks: ['bold', 'italic', 'underline', 'strikethrough', 'link', 'color', 'superscript', 'subscript', 'code', 'linebreak'] Notice you can also add linebreak to your slot’s marks. Add this if you want to let teammates in the visual editor create a break in the component’s text by typing shift+enter. This adds <br> to the code. Similarly, clicking items in the bubble menu adds standard HTML tags like <strong> or <sup> to the code. Allow any component to be inserted Use this to define that a slot can take any component; you can drag any component into the slot of the visual editor. Keep in mind, you may not be able to drag some components in if they have specific parents. <script> export const config = { "label": "component-two", presets: [ { label: "component-two", content: `<component-two></component-two>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.any() }, }); </script> <template> <slot>Insert components here.</slot> </template> Allow only specific components to be inserted Use this to define that a slot can only take certain components. Add a min to specify a minimum number of children required for the component. Add a max to specify a maximum number of children allowed. <script> export const config = { label: "component-three", presets: [ { label: "component-three", content: `<component-three></component-three>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.children(['component-name-1', 'component-name-2']).min(1).max(3) } }); </script> <template> <div><slot>Drag in component-one or component-two.</slot></div> </template> Define placeholder, fallback content You can define placeholder, fallback text for any custom component. Add text between the opening and closing <slot> tags. When you drag a component onto the visual editor canvas, you’ll see the placeholder content until you modify the component. <script> export const config = { "label": "My first component", presets: [ { label: "My first component", content: `<my-first-component></my-first-component>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.text(), marks: ['bold', 'italic', 'underline', 'strikethrough', 'link'] }, }); </script> <template> <p><slot>Placeholder, fallback content</slot></p> </template>  Fallback text renders in your source code If a teammate forgets to change the text or add in a component to the slot, this fallback text will render when the email is sent. For component drop-zones, you can add <x-empty-state/> to your <slot> tag to visually show a drop-zone. For instance, for a component that only accepts certain child components, you could include <div><slot>Insert paragraphs or buttons<x-empty-state/></slot></div> so your teammates know exactly what to do. Dragging in a component replaces the text and drop-zone so they won’t render in your output code. Define multiple slots You can also make a component that has multiple editable areas in the visual editor. For instance, here’s a component that adds a basic format to an email (title, body, and an image) where title is a text slot and article is a component slot that takes as many paragraphs as you need—the main content of your message. Notice in the script, we define two slots—one named title and the other named article. You’ll reference these in the slot elements of the template. <!-- Insert this component in your email with the following code: <email-body></email-body> --> <script> export const config = { label: "Email body", presets: [{ label: "Email body", content: `<email-body></email-body>` }] }; export const slots = Component.defineSlots({ title: { schema: Component.slots.text() }, article: Component.slots.children(['x-paragraph']), }) </script> <template> <x-heading-1> <slot name="title">Greeting text</slot> </x-heading-1> <x-box> <slot name="article">Drag a paragraph here</slot> </x-box> <x-image width="517px" align="center" margin="20px 0px 20px 0px" src="file-name.png" alt="description-of-image" /> </template> Define multiple presets with slots in a single component file This is an advanced use of slots where we join the #slot directive in the script with the <slot> element in the template. This helps you create components with prefilled content that is static and/or modifiable in the visual editor. We’ll create a single custom component file with multiple presets, where teammates can drag each preset separately into the visual editor. In this example, there are two presets: “Product Updates layout” and “Generic layout”. Teammates in the visual editor can drag either onto the canvas. Visual editor: Component code: <script> export const config = { label: "Layout", presets: [ { label: "Product Updates layout", content: `<article-layout> <fragment #slot="title">Product Updates</fragment> <fragment #slot="article"> <x-paragraph>Summary of product updates</x-paragraph> <x-paragraph>Example use case</x-paragraph> </fragment> </article-layout>` }, { label: "Generic layout", content: `<article-layout></article-layout>` } ] }; export const slots = Component.defineSlots({ title: { schema: Component.slots.text() }, article: Component.slots.children(['x-paragraph']), }) </script> <template> <x-heading-1> <slot name="title">Article title goes here</slot> </x-heading-1> <x-box> <slot name="article">Drag paragraphs here</slot> </x-box> <x-image width="5%" margin="20px 0px 20px 0px" src="file-name.png" alt="company logo" /> </template> The Product Updates layout pre-populates both the title and article slots with content. Team members can’t edit the title, but they can modify the content within the article as well as add or remove paragraphs. The bottom of the layout includes a static image, defined in the template. The Generic layout leaves the slots empty so team members can add their own content, but they’ll see the placeholder content in the template. They can modify the title and article and add/remove paragraphs. The bottom of the layout includes the same static image.  Fragment elements don’t render in output The <fragment> element is a special element that groups content without rendering an actual HTML element in your final email. This is perfect for slot content where you don’t want to add extra wrapper elements. --- ## Style custom components URL: https://docs.customer.io/journeys/component-properties/ Learn how to define and validate properties for your components in Design Studio. To create responsive custom components or enable dark mode, check out Responsive Styles and Dark Mode.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. Define properties To declare properties, export a variable like props so the property can be set in the visual editor. Then set this equal to the Component.defineProps object of your <script>. Each property must have a name that starts with an alphanumeric character and a validation schema. <script> export const config = { "label": "My paragraph", presets: [ { label: "My paragraph", content: `<my-paragraph></my-paragraph>` } ] }; export const slots = Component.defineSlots({ default: { schema: Component.slots.text(), marks: ['bold', 'italic', 'underline', 'strikethrough', 'link'] }, }); export const props = Component.defineProps({ color: { section: 'Text', label: 'Color', schema: Component.props.string().optional(), type: 'color', }, 'font-size': { section: 'Text', label: 'Size', schema: Component.props.number().default(16), type: 'number', min: 14, max: 30, }, 'text-align': { section: 'Text', label: 'Align', schema: Component.props.enum(['left', 'center', 'right']).optional(), type: 'toggle', options: [ { label: 'Left', value: 'left', icon: 'format_align_left' }, { label: 'Center', value: 'center', icon: 'format_align_center' }, { label: 'Right', value: 'right', icon: 'format_align_right' }, ], } }); const styleObject = { color: props.color, 'font-size': props['font-size'] + 'px', 'text-align': props['text-align'], 'line-height': '150%', }; </script> <template> <p #set:style="styleObject"> <slot>This is a paragraph</slot> </p> </template> For ease of use in the visual editor, we also recommend defining a type to give teammates the most useful mechanism for modifying styles (like a slider vs toggle). In this example, notice that text-align has an array called options—these are variants your teammates can choose from in the Properties menu. You can set options on properties of type toggle and select. You can optionally define an object to pass in properties which can help reduce code bloat and increase readability of your code. You can also create your own stylesheets and pass in JavaScript variables. For simple properties, where you don’t need to validate the values or let people modify properties in the visual editor, you can pass in an array of property names. Note the square brackets instead of curly braces. <script> export const props = Component.defineProps(['prop-1', 'prop-2', 'prop-3']) </script> Validate properties Usually, you’ll want to specify what data type a property accepts. For example, if you create a property called name, it should probably accept a string. If you create a property called age, it should probably take a number. Instead of simply declaring, “we take a prop called name and age," you can explicitly define validation and default values by passing in an object. We can use the Component.props variable to define how each property functions: export const props = Component.defineProps({ 'prop-1': Component.props.string().default("default value"), 'prop-2': Component.props.boolean(), 'prop-3': Component.props.string().array().optional(), 'prop-4': Component.props.enum(['left', 'center', 'right']).optional() }) These are the validation schema we fully support: string boolean number enum Each key is the name of a property like prop-1. Each value is either validation schema or an object that includes the validation schema and other key/values that define it, like type and label. If you use the property types select or toggle, you can pre-define variants that teammates can choose from in the visual editor. Check out the zod validation library for more info on what powers our property validation. (Optional) Define an object to pass in properties After you set your properties, you may want to create an object to pass in properties. This isn’t necessary but helpful for readability of your code, especially if you have complex logic inside the styles, and to reduce code in the event that a variable renders undefined. In this example, we’ve named our object styleObject, but you can name this anything you like. const styleObject = { color: props.color, 'font-size': props['font-size'] + 'px', 'text-align': props['text-align'], 'line-height': '150%', }; The names on the left are the CSS attributes you want to include, and on the right are the values. If a property is not set, the value is undefined and won’t be included in the output code. You can also hard-code styles which aren’t defined in the props script, like line-height in the example above. Add global styles In your style object, you can reference your global styles to keep your component on brand too. You’ll reference them using globalStyles.<global-style-type>.<global-style-variablename-variableid>. This might look like globalStyles.colors.pink_khyqtq8v5rcs.  Use autocomplete to reference the global style Note the variable id needed at the end of the global style reference. You can find this by using autocomplete in the component editor. <script> export const config = { label: "global style test", presets: [ { label: "global style test", content: `<test2></test2>` } ] } const styleObject = { background: globalStyles.colors.pink_khyqtq8v5rcs, }; </script> Pass the object into the component template After you’ve defined your properties and an object to pass in the styles, set the object in the component template. <template> <p #set:style="styleObject"> <slot>This is a paragraph</slot> </p> </template> When you add a style attribute to content in the <template> tag, we automatically pass this JavaScript object to output your styles. Keys for properties When defining a property, you can set a number of keys that define how we validate the property and how people interact with it in the visual editor. schema - defines the validation type type - defines the type of property in the visual editor (a toggle, slider, etc.) label - name of the property in the Properties menu helpText - adds a hoverstate to the label in the Properties menu section - used to partition properties in the Properties menu Types of properties Set a type to define how team members modify the property in the visual editor’s Properties menu. If your component has no type, we default it to type text. background A string input used to define a CSS color, image, or gradient for the background of a component. The output is a single CSS background string. If opacity is less than 100%, then the value of rgba is used instead, like rgba(255, 0, 0, 0.5). background: { label: 'Background', section: 'Styles', schema: Component.props.string().optional(), type: 'background', } Note, there are some CSS properties we do not support for backgrounds: background-attachment background-clip repeating-linear-gradient and repeating-radial-gradient, though you can achieve similar results with Repeat (background-repeat) in layout properties box A string input used to define pixel values on four sides of a box. The output is between one and four values in pixels as a single string. For instance, it could be any of these: 10px or 10px 20px or 10px 20px 5px or 10px 20px 5px 15px. Commonly used for padding, margin, and border-width. The collapsedInput property defines the starting point of the box: global gives one input that applies to all sides of the box. axis gives two inputs - one for top/bottom value and one for left/right values. In the visual editor, you’ll see the option to select global styles for spacing too. padding: { label: 'Padding', section: 'Layout', schema: Component.props.string().optional().default('10px'), type: 'box', collapsedInput: 'axis', } code A string input that’s the same as a text input; however, the styling of type code is monospace. color A string input used to define a CSS color. The output is a six digit hex code. If opacity is less than 100%, then the a value of rgba is used instead, like rgba(255, 0, 0, 0.5). In the visual editor, you’ll see the option to select global styles for colors too. color: { label: 'Text color', section: 'Text styles', schema: Component.props.string().optional(), type: 'color', } font-family A string input to define a font-family. In the visual editor, you’ll see the option to select global styles for fonts too. 'font-family': { label: 'Font family', section: 'Styles', schema: Component.props.string().optional(), type: 'font-family', }' hidden A property with type hidden will not appear in the Properties menu of the visual editor. However, you can still set the property from the code editor. This can be helpful if you want to limit access to advanced features to more technical users, if you want to keep new features hidden while you’re testing them, or if you want to support deprecated features behind-the-scenes. media A string input for a media asset such as an image or video. This is commonly used for the src input on an image component. accept is an optional property that lets you define what file types, like image/png or image/jpeg, are allowed. You can also specify image/* or video/* to allow any image or video format respectively. Use placeholder to inform team members what to enter into the component in the visual editor. The placeholder text is never included in the output email code. src: { label: 'Source', schema: Component.props.string(), type: 'media', accept: 'image/*', placeholder: 'Paste the image url here...', } number A number input. On the Properties menu, you’ll see +/- buttons to increase or decrease the value. Consider whether you want to set these optional properties too: min for a minimum number value max for a maximum number value step to define the increment by which a team member can increase/decrease the value with the +/- buttons 'font-size': { label: 'Size', section: 'Text styles', schema: Component.props.number().optional(), type: 'number', min: '1', max: '100', } radius A string input for setting rounded corners. Similar to the box input, the output is between one and four values in pixels as a single string. For instance, it could be any of these: 10px or 10px 20px or 10px 20px 5px or 10px 20px 5px 15px. In the Properties menu, you’ll see the option to select global styles for radii too. 'border-radius': { label: 'Radius', section: 'Styles', schema: Component.props.number().optional(), type: 'radius', } select An enum or string input that displays a dropdown list of options to select from. Include options to define the dropdown list. Each item can include: label for the display name in the dropdown list value for what the dropdown item should mean optgroup to organize specific options under the same header 'font-weight': { section: 'Text Styles', label: 'Weight', schema: Component.props.string().optional(), type: 'select', options: [ { label: 'Light', value: '300' }, { label: 'Normal', value: '400' }, { label: 'Bold', value: '700' }, ], } You can also reference global styles in the values of your options. You’ll reference them using globalStyles.<global-style-type>.<global-style-variablename-variableid>. This might look like globalStyles.colors.pink_khyqtq8v5rcs.  Use autocomplete to reference the global style Note the variable id needed at the end of the global style reference. You can find this by using autocomplete in the component editor. shadow A string input for a CSS box-shadow. The input allows you to set multiple shadows, but will output a single string. 'box-shadow': { section: 'Styles', label: 'Shadow', schema: Component.props.string().optional(), type: 'shadow', } size A string input to define a size. Commonly used as width and occasionally height. units is an array of the allowed units you can select and include in the output: auto will output the keyword ‘auto’. The browser calculates the width. px will add ‘px’ to the number in the output value. percentage will add ‘%’ to the number in the output value. 'width': { section: 'Layout', label: 'Width', schema: Component.props.string().optional(), type: 'size', units: ['auto', 'px'], } slider A number input, but controlled with a slider. You can optionally include min as a minimum number value or max as an optional maximum number value. step An optional step value to define the increment someone can increase/decrease the value by with the +/- buttons in the visual editor. 'letter-spacing': { label: 'Letter spacing', section: 'Text styles', schema: Component.props.number().optional(), type: 'slider', min: 0, max: 1, step: 0.1, } switch A boolean input. This is used for a simple true/false statement. You could use this in a footer component to choose if you include social icons, for example. 'social': { label: 'Include social', section: 'Options', schema: Component.props.boolean().optional(), type: 'switch', } text A string input for setting text. If your component has no type set, we default it to type text. Use placeholder to inform team members what to enter into the component in the visual editor. The placeholder text is never included in the output email code. 'alt': { label: 'Alt text', schema: Component.props.string().optional(), type: 'text', placeholder: 'Add image description...' } textarea A string input for setting text. This is similar to ’text’ but has more options. placeholder - use this to inform team members what to enter into the component in the visual editor. The placeholder text is never included in the output email code. rows - use this to require a number to define how many rows of text tall the component is, the default is 2 multiLine - use this to require a boolean to define if you want to preserve line breaks, the default is false resize - use this to require an enum to define if the component can be resized; options are none (default, can’t resize) or vertical (can resize) const style = { label: 'CSS Styles', schema: Component.props.string().optional(), type: 'textarea', multiLine: true, rows: 3, resize: 'vertical' } toggle An enum input that will display a toggle in the Properties menu. Use options to set an array of toggles: label defines the display name for the toggle. value defines how the code handles the toggle. icon if included, replaces the label. You can use Google material symbols and icons. 'text-align': { section: 'Text Style', label: 'Text Align', schema: Component.props.enum(['left', 'center', 'right']).optional(), type: 'toggle', options: [ { label: 'Left', value: 'left', icon: 'format_align_left' }, { label: 'Center', value: 'center', icon: 'format_align_center' }, { label: 'Right', value: 'right', icon: 'format_align_right' }, ], } You can also reference global styles in the values of your options. You’ll reference them using globalStyles.<global-style-type>.<global-style-variablename-variableid>. This might look like globalStyles.colors.pink_khyqtq8v5rcs.  Use autocomplete to reference the global style Note the variable id needed at the end of the global style reference. You can find this by using autocomplete in the component editor. url Use url for links, emails, and phone numbers. 'href': { label: 'Link', schema: Component.props.string().optional(), type: 'url', placeholder: 'Add destination...' } In the visual editor, the component’s properties menu shows the Link field. You’ll decide whether to set a URL, Email, or Phone number from the dropdown. Learn more about each option. Use placeholder to inform team members what to input if they choose URL or Email from the dropdown. The placeholder text is never included in the output email code. --- ## Delete a component URL: https://docs.customer.io/journeys/delete-component/ Understand how deleting components impacts your emails. Delete a custom component On your Design Studio dashboard, click Components. Click next to your component. Click Find Component References to see if your component is used in your messages. Remove the component from all messages where it’s referenced.  Publish changes to connected messages After you remove component references from messages, publish your changes so your messages and automations are in sync. Click Delete. If you delete a component file before removing its references from messages, you’ll see an error in the visual editor. To remedy this: Open your message in the code editor — click , then click Switch to Code. Remove the component tag. Publish the message to update any connected automations and keep everything in sync with your Design Studio messages. --- ## Understand syntax URL: https://docs.customer.io/journeys/custom-comp-syntax/ In Design Studio, there are a number of top-level elements available in a custom component file as well as other syntax.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. Required top-level syntax You must include both <script> and <template> tags in your custom component file. The <script> tag handles logic, properties, and configuration for the component. In the <script> tag, you can: Write JavaScript logic to help render your <template> HTML. It supports the full JavaScript run-time. Define and validate slots - a way to make certain content editable in the visual editor Declare and validate properties Set context for use in any child components Define component configuration by exporting the config variable The <template> tag defines the HTML structure and content of the component. In the <template> tag, you can: Add HTML, standard components, and other customer components Reference styles created in the <script> or <style> tags Use JavaScript to create dynamic values Set directives Create <fragment> tags to add logic without unneeded HTML Optional top-level syntax We offer four, optional, top-level elements to help you create custom components: <style> <title> <meta> <link> You can also set directives in these tags. style <style> tags allow you to add CSS to your component. Pass them to the <template> tag to style your content. Check out our article on stylesheets for more. title Add a title to your email. We add the title to the <head> of the message. If you create multiple, identical titles, we only add one to the <head>. If you create multiple, non-identical <title> tags, we place all of them in the <head> and let the browser decide which to display. meta Add metadata to your email that a browser can read but won’t display for a recipient. This will render in the <head> of the message. If multiple, identical tags are added, only one is placed in the head. <meta name="robots" content="noindex" /> <template>This is the component</template> If you create multiple, non-identical <meta> tags, we place all of them in the <head> and let the browser decide which to use. link You can link to external stylesheets with the <link> tag. This makes it easy to add support for hosted fonts. If multiple, identical tags are added, only one is placed in the head. <script> const fontFamily = 'Inter' </script> <link rel="stylesheet" #set:href="https://fonts.googleapis.com/css?family=${fontFamily}"> <template> <p #style:font-family="fontFamily"><slot /></p> </template> If you create multiple, non-identical <link> tags, we place all of them in the <head> and let the browser decide if it needs to fetch each one. Define config to use your component in the visual editor Use the config variable to define how the component is referenced and export it for use in the visual editor. You must define config as a static object. <script> export const config = { label: "example component", presets: [ { label: "example component", content: `<example-component></example-component>` } ] } </script> Pass data to nested components with context Set a context to store brand information, color schemes, or user inputs that you want to pass down to nested components. You can pass data down from a grandparent to a grandchild without having to pass properties manually at every level (aka props-drilling). You’d set the context in the highest-level component and get the context in any nested component. First, call Component.context.set with a key/value or string. All nested components can then use the context as shown below. <script> Component.context.set('theme', {primary: 'blue'}); </script> Then call Component.context.get in the <script> tag of components you’ll nest inside the one where the context is set. This gives nested components access to the full context. <!-- <nested component /> --> <script> const inherited_theme = Component.context.get('theme'); </script> You can access all contexts by simply omitting the context name too. <!-- <nested component /> --> <script> const all_contexts = Component.context.get(); </script> Fragments: add logic without extra HTML Fragments are fake elements. Whereas a <p> tag renders a certain way in an email client, a <fragment> tag wouldn’t display at all. This makes them useful for setting conditionals. <script> const lang = "fr" </script> <template> <fragment #if="fr"> <span>Bonjour</span> </fragment> <fragment #else> <span>Hello</span> </fragment> </template> Above, we used fragments to conditionally render translated content. Note, the directives that set the values of attributes have no effect on <fragment> tags, except when combined with #is. You can also use the #set:html directive to render HTML defined in the script tag or elsewhere. This is useful if a system might have trouble parsing opening and closing tags. <fragment :html="myHTMLVariable" /> Insert dynamic values Additionally, you can insert full JavaScript expressions inside the content of any HTML using the standard template literal syntax ${}. <div>${1+2*3/4}</div> To support injecting arbitrary content inside a component, you can use slots. <template> <a :href="href"><slot>Click me</slot></a> </template> Comments You can add standard HTML comments anywhere within <template>. <!-- I'm a standard HTML comment --> In the <script> tag, use standard JavaScript comments. // I'm a standard JavaScript comment. In the <style> tag, use standard CSS comments. /* This is a standard CSS comment */ While standard comments are not evaluated, all conditional comments are processed as normal HTML. <!--[if mso]><i :style:letter-spacing="`${10*5}px`" hidden>&nbsp;</i><![endif]--> Note, we compile comments inside the <template> and <style> tags into the final output. --- ## Add conditionals & directives URL: https://docs.customer.io/journeys/directives/ Directives are special attributes that direct an element's behavior. Use them to create conditionals, set an attribute, skip evaluation, and more in your Design Studio components.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. Use directives in HTML, standard components, custom component templates and our optional, top-level elements of your email to set attributes dynamically and conditionally add/remove them. Every directive begins with #. For instance, you can use #if, #else-if, and #else for conditionally displaying these tags: <script> const useExternal = true; </script> <link #if="useExternal" rel="stylesheet" href="https://example.com/hosted.css"> <style #else> /* local CSS */ </style> You can also dynamically set properties on the <title> , <meta> and <link> tags using #set. List of directives #if Conditionally render an element based on whether the given JavaScript expression is truthy. Accepts: any <div #if="lang === 'en'">Hello</div> #else-if Chain this to a sibling element with an #if or #else-if directive. If none of the leading conditions are true, this element will render. It must be the last item in a conditional chain. Accepts: any <div #if="lang === 'en'">Hello</div> <div #else-if="lang === 'es'">Hola</div> #else Chain this to a sibling element with an #if or #else-if directive. If none of the leading conditions are true, this element will render. It must be the last item in a conditional chain. Accepts: nothing <div #if="lang === 'en'">Hello</div> <div #else-if="lang === 'es'">Hola</div> <div #else>Bonjour</div> #each This allows you to iterate over lists of values and render the element for each value. Accepts: Array | Object | number | string <div #each="item in items">${item}</div> <div #each="(item, index) in items">${index} – ${item}</div> <div #each="(value, key) in object">${key}: ${value}</div> <div #each="(value, key, index) in object">${index} – ${key}: ${value}</div> #set This allows you to dynamically set an HTML attribute. The final value of the expression will be converted to a string and set as an attribute on the HTML element. Set objects You can pass an object to the #set directive to set multiple attributes at once. <!-- source --> <div #set="{ id: 'my-id', class: 'my-class' }"></div> <!-- result --> <div id="my-id" class="my-class"></div> Set individual attributes You can set individual attributes by setting the key after a colon (:) and the value. <!-- source --> <div #set:id="2*3*4"></div> <!-- result --> <div id="24"></div> Shorthand As a shorthand, you can simply add a : before an attribute. <!-- these are equivalent --> <div #set:id="2*3*4"></div> <div :id="2*3*4"></div> Set props on child components You can pass all properties set on a component into a child component using #set="props". <template> <x-paragraph #set="props"><slot/></x-paragraph> </template> Or you can pass individual variables into a child component. In this example, we’re passing the font size from the parent component into a paragraph of a child component: Input in the custom component editor: <template> <x-paragraph #set:font-size="props['parent-font-size']"><slot/></x-paragraph> </template> Output of a sent email that uses the component: <p style="font-size:<value-of-parent-font-size>"><content></p> #set:html Dynamic values output as escaped by default to prevent accidentally injecting HTML. To skip escaping, use the #set:html directive. <div #set:html="rawHTMLVariable"></div> Use this with <fragment> to avoid adding an extra element to your output. <fragment #set:html="rawHTMLVariable" /> #set:class Setting a class can take either a string or an object. Object <div #set:class="['class1','class2','class3']" ></div> String <div #set:class="class1 class2 class3 " ></div> In both cases, this is the output: <div class="class1 class2 class3" ></div> #set:style Setting a style can take either a string or an object. If you add a string, you must wrap the styles in backticks. <div :style="`font-size:1em;color:green;font-family:${props[font-family]}`"> If you add an object, set the object in the <script> tag to keep your code cleaner and more readable. const styleObject = { color: props['color'], 'font-family': props['font-family'], 'font-size': props['font-size'], 'font-weight': props['font-weight'], 'text-decoration': props['text-decoration'], 'line-height': props['line-height'], 'text-transform': props['text-transform'], 'text-align': props['text-align'], }; Then when you set the style, the output code will be cleaner, absent of blank values and extra spacing. <div :style="styleObject"> #class:* This directive allows you to easily control whether a class is added. Use #set:class:* to dynamically set it. Accepts: string <div #class:my-class>Test</div> <div #set:class:my-class="Math.random() > 0.5">Test</div> #style:* This directive allows you to set a style property and value. Use #set:style:* to dynamically set it. Accepts: string <div #style:background="blue">Test</div> <div #set:style:background="Math.random() > 0.5 ? 'blue' : 'red'">Test</div> #is This directive allows you to set HTML elements as well as components. Use #set:is to dynamically set it. Accepts: string | Component Component <a #is="custom-button">Click me</a> <a #set:is="CustomButton">Click me</a> <a #set:is="Math.random() > 0.5 ? CustomButton : AnotherButton">Click me</a> HTML <ul :is="orderedList ? 'ol' : 'ul'"> <li></li> </ul> #is:raw This directive indicates that any children of the element should be skipped for compilation. This is helpful when you are inserting text or attributes with conflicting syntax and you don’t want us to process it. Accepts: Nothing <div #is:raw> <a #if="I am not evaluated">${I am also not evaluated}</a> </div> #slot This directive tells us which slot to insert the element into. This is commonly used in component presets to pre-populate specific named slots with content. See Slots for comprehensive information about creating and configuring slots. Accepts: string #slot cannot have dynamic values. <layout> <fragment #slot="header">This goes in the header slot</fragment> <fragment>This goes in the default slot</fragment> <fragment #slot="footer">This goes in the footer slot</fragment> </layout> --- ## Loop through data URL: https://docs.customer.io/journeys/component-loops/ In Design Studio, components support loops, enabling you to iterate with fewer keystrokes.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. Loop through numbers Numerical iteration is 0 indexed - the first number is 0. <template> <ul> <li #each="n in 10">${n}</li> </ul> </template> Using this component in an email: <html> <body> A simple email <my-first-component></my-first-component> </body> </html> The output source becomes: <html> <body> A simple email <ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> </ul> </body> </html> Loop through arrays You can loop through an array to dynamically generate repeated elements—like list items—based on your data. <template> <ul> <li #each="food in ['tomato','potato','lettuce', 'cucumber', 'carrots','apples']" > ${food} </li> </ul> </template> Using this component in an email: <html> <body> A simple email <my-first-component></my-first-component> </body> </html> The output source becomes: <html> <body> A simple email <ul> <li>tomato</li> <li>potato</li> <li>lettuce</li> <li>cucumber</li> <li>carrots</li> <li>apples</li> </ul> </body> </html> Passing in arrays You can also pass in arrays to a component to render data defined in each email they’re in. <script> export const props = Component.defineProps({ products: { schema: Component.props.array(Component.props.string()), }, }); </script> <template> <ul> <li #each="product in products">${product}</li> </ul> </template> Using this component in an email: <html> <body> A simple email <my-first-component :products="['apples','bananas','tomatoes']"> </my-first-component> </body> </html> The resulting output is: <html> <body> A simple email <ul> <li>apples</li> <li>bananas</li> <li>tomatoes</li> </ul> </body> </html> Passing in objects <script> export const props = Component.defineProps({ products: { schema: Component.props.array( Component.props.object({ name: Component.props.string(), price: Component.props.string(), }) ), }, }); </script> <template> <ul> <li #each="product in props.products">${product.name} - ${product.price}</li> </ul> </template> <html> <body> A simple email <my-first-component :products="[ {name:'apple', price:'$0.50'}, {name:'banana', price:'$0.75'}, {name:'tomato', price:'$3.00'} ]"> </my-first-component> </body> </html> The output is: <html> <body> A simple email <ul> <li>apple - $0.50</li> <li>banana - $0.75</li> <li>tomato - $3.00</li> </ul> </body> </html> You can also import objects that are not iterated over. <script> export const props = Component.defineProps({ products: { schema: Component.props.array( Component.props.object({ name: Component.props.string(), price: Component.props.string(), }) ), }, store: { schema: Component.props.object({ name: Component.props.string(), location: Component.props.string(), }), } }); </script> <template> <ul> <li #each="product in props.products">${product.name} - ${product.price}</li> </ul> ${props.store.name} <br /> ${props.store.location} </template> <html> <body> A simple email <my-test :products="[ {name:'apple', price:'$0.50'}, {name:'banana', price:'$0.75'}, {name:'tomato', price:'$3.00'} ]" :store="{ name: 'Main Office', location: '123 Somewhere Drive' }"> </my-test> </body> </html> The output is: <html> <body> A simple email Main Office <br /> 123 Somewhere Drive </body> </html> Enumerated iteration Sometimes you want access to both the iterated item and its index (count) in the loop. <template> <table> <tr> <th>Food</th> <th>Index</th> <th>Conditional Message</th> </tr> <tr #each="(food,index) in ['tomato','potato','lettuce', 'cucumber', 'carrots','apples', 'bananas', 'beans']" > <td>${food}</td> <td>${index}</td> <td>${index>3?'the first 4 items dont get this message':''}</td> </tr> </table> </template> Using this in a simple email: <html> <head></head> <body> <h1>My shopping list</h1> <enumerated-iteration></enumerated-iteration> </body> </html> This is the output source: <html> <head></head> <body> <h1>My shopping list</h1> <table> <tr> <th>Food</th> <th>Index</th> <th>Conditional Message</th> </tr> <tr> <td>tomato</td> <td>0</td> <td></td> </tr> <tr> <td>potato</td> <td>1</td> <td></td> </tr> <tr> <td>lettuce</td> <td>2</td> <td></td> </tr> <tr> <td>cucumber</td> <td>3</td> <td></td> </tr> <tr> <td>carrots</td> <td>4</td> <td>the first 4 items dont get this message</td> </tr> <tr> <td>apples</td> <td>5</td> <td>the first 4 items dont get this message</td> </tr> <tr> <td>bananas</td> <td>6</td> <td>the first 4 items dont get this message</td> </tr> <tr> <td>beans</td> <td>7</td> <td>the first 4 items dont get this message</td> </tr> </table> </body> </html> Enumerated object iteration <template> <div #each="(value, key, index) in {key1:'value1',key2:'value2', key3:'value3'}" > ${index+1}. ${key}: ${value} </div> </template> Using this component in a simple email: <html> <head></head> <body> <enumerated-iteration></enumerated-iteration> </body> </html> The output source becomes: <html> <head></head> <body> <div>1. key1: value1</div> <div>2. key2: value2</div> <div>3. key3: value3</div> </body> </html> --- ## Create a stylesheet URL: https://docs.customer.io/journeys/component-styling/ In Design Studio, define CSS to style your components.  Use our Design Studio GPT to create a custom component! Learn how to best utilize our GPT to create code for reusable, custom content. We’ve shown you have to define properties with JavaScript in the <script> tag of your custom component, but you may also want to define styles in a stylesheet. In the <style> element, you can define branding, hover styles, responsive styles, dark mode, and more through CSS or by passing in properties defined in the <script> tag. In the visual editor, you can’t modify the CSS values unless those values are set by properties in the <script> tag. Reference global styles To reference global styles, add a style element between your script and template tags and create a class with your global style. You must use the set() function. Then pass this into the template. <script> export const config = { label: "global style test", presets: [ { label: "global style test", content: `<test2></test2>` } ] } </script> <style> .foo{ background: set('globalStyles.colors.pink_khyqtq8v5rcs'); } </style> <template> <div class="foo"> content </div> </template> You’ll reference them using globalStyles.<global-style-type>.<global-style-variablename-variableid>. This might look like globalStyles.colors.pink_khyqtq8v5rcs.  Use autocomplete to reference the global style Note the variable id needed at the end of the global style reference. You can find this by using autocomplete in the component editor. Pass JavaScript into the <style> tag <style> tags support variables defined in your <script> tag. Pass them as CSS values with the set() function. If the value you want to access is inside of an object or array, you can use dot notation. In your template, you’d then pass the CSS variable. <script> export const config = { label: "custom-body", presets: [ { label: "custom-body", content: `<custom-body></custom-body>` } ] }; export const props = Component.defineProps({ background: { section: 'Call-out', label: 'Background', schema: Component.props.string(['rgb(60, 179, 113)','rgb(186, 255, 164)']).optional(), type: 'select', options: [ { label: 'green', value: 'rgb(60, 179, 113)' }, { label: 'light-green', value: 'rgb(186, 255, 164)' } ] }, color: { section: 'Call-out', label: 'Color', schema: Component.props.string().optional(), type: 'color', } }); </script> <style> .call-out { background: set('props.background'); color: set('props.color'); } </style> <template> <x-heading-1> Welcome! </x-heading-1> <x-paragraph> Introduction </x-paragraph> <div class="call-out"> Something you want to emphasize </div> </template> How to use multiple <style> tags Use multiple style elements to set conditional or standalone styles. By default, we merge all <style> tags together to reduce code bloat, but you can change this with the #isolated attribute. For example, these two <style> tags would be merged: Source: <style> .a { background: blue; } </style> <style> .b { background: red; } </style> Output: <style> .a { background: blue; } .b { background: red; } </style> Set conditions You can use conditional directives (#if, #else-if, #else) to render CSS. <style #if="variant === 'primary'"> .primary-button { background: blue; color: white; } </style> <style #else> .default-button { background: white; color: blue; } </style> Isolate styles Sometimes you need a particular <style> tag to live on its own and not merge with other <style> tags. For instance, you might want this when sending emails to Gmail. If the <style> element contains any invalid CSS, Gmail will strip all styles. But if you wrap CSS that might be invalid in a #isolated style element, Gmail will not strip the rest of the CSS. <style #isolated> .a { background: blue; } </style> <style> .b { background: red; } </style> You can also group styles together by assigning a value to the #isolated attribute. This can prevent people from receiving a half-styled email; when an email client removes a <style> block, it will also remove all styles with the same value. This practice can help reduce code bloat too. <style #isolated="outook-web"> [class~="a"] { background: blue; } </style> <style #isolated="outook-web"> [class~="outlook-hide"] { display: none; } </style> <style> .b { background: red; } </style> <style #isolated="interactive"> input:checked ~ .c { background: green; } </style> Some clients, like Thunderbird, do not support @media queries, so to work around that, use our media attribute on an isolated style to set screen breaks. We combine <style> tags with the same isolated attribute values (like #isolated="thunderbird") that also have identical media attributes; however, if the media attributes aren’t identical, then the styles will apply to different break points. <style media="(min-width:600px)" #isolated="thunderbird"> .moz-text-html .test{background:red;} </style> In the example above, we prefixed the class to specify the Thunderbird email client - .moz-text-html. You’ll want to do the same for any other email clients you want to target. --- ## Migrate components from Parcel URL: https://docs.customer.io/journeys/migrate-comp-from-parcel/ If you created components in the email editor Parcel, you can migrate them to Design Studio. You'll need to [create a new component](/journeys/create-custom-component/#create-a-custom-component-file) in Design Studio and paste the code there. Keep the following in mind as you migrate: Make sure the component tag name matches the content value in the script element. If this is the child of another component, make sure you also add the parent components to Design Studio or remove the allowedParents array. If you run into other issues, try troubleshooting with our GPT designed for creating custom components! --- ## How to collaborate URL: https://docs.customer.io/journeys/collaborate/ Collaborate with teammates to review and fine-tune your messages made in Design Studio. Overview Design Studio offers you and your teammates many tools for collaborating on your messages—you can share, export, or capture a screenshot of your work, create rounds of feedback, and access version history. You can also send a test to see how the message looks in your inboxes.  Only one person can edit a Design Studio message at a time. Design Studio doesn’t offer live, cross-team collaboration. If someone else is working on a message when you enter the visual or code editor, you’ll be prompted to Take over Editing or View Only. Share This is the same as copying the browser link of your message. From the dropdown menu next to your filename, click Share, then click Copy link. You can send the link to anyone on your team, but note they must log in to access it. Export You can easily export all of your files from Design Studio. Click the Export button in the dropdown by the filename. From here, you can choose to download a zip of all your files, an HTML file, or a PDF. Export option Description Download ZIP This includes your final HTML (the processed version of your code that includes transformers), the text version of the email, and any relevant, uploaded assets. Copy HTML This copies the final, processed HTML of the email to your clipboard. Download PDF Report This includes a PDF summary of your email based on settings you can customize. Customize your PDF Report When exporting a PDF, decide what the envelope should be and if you want a description: Email name – by default, this is the filename From name and From email address Subject – by default, this is the filename Description (optional) By default, every PDF includes a 700px-wide desktop screenshot of your full message. You can also include a: Mobile screenshot (320px wide) Dark mode screenshot Plain text version of your message Check any of the following to include additional details: Link validation – Adds numbered callouts to links in the screenshot Image validation – Checks for missing/broken images Spam score – Adds your spam score from SpamAssassin Capture a screenshot If you need a quick visual to share or review, you can take a screenshot of the preview window at any time. Go to the preview window, and click the camera icon to capture your screenshot. The screenshot includes preview settings like dimensions. The file is .png, and will open in a new tab in your browser. --- ## Submit & manage feedback URL: https://docs.customer.io/journeys/feedback/ In Design Studio, get feedback on your messages from your team. Get feedback Feedback allows other team members to leave comments, annotations, and approvals on your email. To create and share a feedback version: Click the dropdown next to your filename and choose Feedback. Or, click Share, then select Get Feedback. Name your feedback version. Add a description to explain what changed or what kind of feedback you’re looking for. Click Get Feedback to open the feedback page. In the sidebar, click Share to copy the link to your feedback page. Send the link to your team for review.  Your teammates must log in to offer feedback—Customer.io does not support anonymous feedback. Manage feedback You can submit and manage your feedback on the Feedback page. Feedback toolbar Use the feedback toolbar to preview and interact with the email. Interact: Select the cursor icon in the top menu bar to interact with the email (click on links, etc). Annotate: Click the message icon to annotate and leave comments on the email.  Comments are specific to device-type. Clicking on any comment in the sidebar will open the comment in the specified device view. Preview on different devices: Click the desktop and mobile icons to switch between a preview for desktop or mobile aspect ratios. Preview with different visual settings: Click the preview controls menu to view the email with images blocked, in light/dark mode, and with a variety of simulated visual impairments. Manage versions and comments Use the right-hand feedback panel to manage versions and comments. Switch or create feedback versions: In the top-right corner, you’ll see the feedback version you are viewing. Click the dropdown to review other feedback versions, or click New Version to create another. Review and respond to comments: Under the feedback version, you can see any previously made comments and reviews. Hover over a comment to resolve it, copy a link to share it, or delete it. Click a comment to reply to it, or view previous replies. Click the filter icon to show resolved comments or only show comments with your same settings (mobile view, blocked images, etc). As you work through implementing stakeholder feedback, you can share an updated version of your email to ensure its content is fresh. Each version is considered a “round” of feedback. You can view all versions in your version history. --- ## Manage version history URL: https://docs.customer.io/journeys/version-history/ In Design Studio, you can save, manage, and restore previous versions of your messages. Save a version Versions capture your message, settings, assets, and components at a specific point in time. You can save a version from several places: In the visual or code editors, click and select Save Current Version. On a Feedback page, click New Version. Give your version a name and (optionally) a description.  You can only save a version if something has changed since the last one. If nothing’s changed, you’ll see a message that the version wasn’t saved. If your message is connected to an automation, any time you publish changes, a new version will automatically be created. View previous versions You can view all existing versions of your message on the Version history page. Click above either the visual or code editors and select View Version History. Here you’ll find every version of the message, and you can filter by All versions or only Named versions. Select the version you’d like to view. The Version history page compares the version to its predecessor (if any exists). You can switch between a split or inline view of differences by selecting SPLIT VIEW? Additions you’ve made will be highlighted in green, and deletions will be highlighted in red. You can jump to these changes by clicking on the color next to the scroll bar. Restore a previous version You can restore an older version at any time. Click above either the visual or code editors and select View Version History. Select your desired version and click Restore. After you confirm, the system will auto-save your current email as a new version and then restore the earlier version. If your email is connected to a campaign, you’ll have to click Publish to push the changes to the campaign. If the campaign is active, then moving forward, people will receive the version you restored when they reach your email block. This will not change emails already sent. --- ## Send a test message URL: https://docs.customer.io/journeys/send-test/ To make sure your message looks right before sending it to your customers, send a test message to your inbox. You can send a test message before or after connecting a Design Studio message to an automation. Open the message in Design Studio or the connected automation. Click Send test. Add up to 25 email addresses or choose an email group. Your email address is added by default. Click Send. Prevent threading adds a unique subject line so your inbox won’t group test emails together. If you don’t want this, uncheck the box. 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. Send to an email group If you regularly send test emails to the same group of people, you can create an Email Group to expedite sending. Email groups are available across any Design Studio message. Click Send test. Click Create a new Group. Enter up to 25 email addresses. Name the group so you can easily find it for future sends. Click Save. Click the email group name to add it to your recipients. Edit an email group Within the Send test menu, hover over the group name and click the pencil icon. You can rename the group and change the email addresses. Click Save. Delete an email group Within the Send test menu, hover over the group name and click the icon. Confirm your action to permanently remove the group from Design Studio. --- ## WhatsApp templates URL: https://docs.customer.io/journeys/whatsapp-design-studio/ You can create and submit WhatsApp templates for approval in Customer.io if you send messages through your Facebook Business Account. [If you send through Twilio, you have to manage your templates on their site.](/journeys/whatsapp-content-templates/#create-a-content-template-in-twilio) How it works To manage WhatsApp templates in Design Studio, you must integrate directly with your Facebook Business Account. Then you can create WhatsApp templates and submit them for approval from Design Studio, instead of having to leave your workspace to create them with Meta. The process goes like this: From Design Studio, create a WhatsApp message from scratch and add your content including any relevant liquid syntax. Submit it for approval from Meta. You can track the approval status from the Design Studio dashboard! Unlike Meta-created templates, you don’t need to take the extra steps of assigning liquid variables to your template variables! Add the template to your campaign.  Wait to activate your campaign until Meta approves your template. Your audience may not receive WhatsApp messages unless they’re approved! Make sure the status says “Approved” before you start sending. If you’re familiar with Design Studio, you know there are Global styles—variables for fonts, colors, etc that apply across your emails and in-app messages. These styles do not influence WhatsApp messages, since they’re simple text or image-based messages. Any templates you created outside of Design Studio are not visible from the dashboard. Limitations compared to building within your Meta account We’re working to close the gap between Design Studio and Meta’s template builder, so you can do more in Customer.io. Currently, you can’t do the following in Design Studio: Add buttons, CTAs, or Quick Replies Create Authentication templates Create a WhatsApp template & submit for approval Go to Design Studio in your left-hand navigation to get started. Click Create > New WhatsApp. Add a message name—this helps you find it when adding it to a campaign. (Optional) Save to a folder. Choose a category of template. Learn more about template types in Meta’s docs. Marketing—Content that may include promotions (Note, people in North America must be opted into receiving marketing messages via WhatsApp to get this type.) Utility—Non-promotional messages related to transactions, like order updates Select the language of your message. Note, you have to create a template for each language you want to support. Click Create. Edit the From field if you need to change your sender number. Edit the To field to target the right customer attribute for phone numbers. Edit your message’s body content. (Optional) Click Insert to add a header or footer. To personalize the message, add liquid. Open the personalization panel or use the agent to help you write liquid ( Ask Agent). Learn more in our Design Studio Liquid doc. Once your template is complete, your next step is to Submit for approval from Meta. Submit for review After you complete your template, you’ll submit it for approval. If your template contains liquid syntax, add example data. Then click Submit for approval. If you added liquid to your template, you must Add variable samples before submitting. You provide a sample value for each liquid variable in your template. Make sure these values respect your customers’ privacy. Then double check that your template is complete. Once you submit, you can’t change anything while Meta reviews your submission. If your template is rejected, you can edit it and resubmit for approval. If your template is approved, you’ll have to submit a new template with your changes. You can check the status of your WhatsApp templates from the Design Studio dashboard.  It can take up to 24 hours for Meta to review your templates While Meta (WhatsApp’s parent company) typically reviews templates with an algorithm and approves or rejects them within minutes, some templates go through a manual review process that can take up to 24 hours. Make sure that your template is approved before you try to use it in Customer.io. Resubmitting a WhatsApp template for approval If your WhatsApp template is rejected, you can edit it and resubmit it for approval. Adjust your template based on the information returned from Meta. Connect to a campaign You can connect a WhatsApp template to a campaign at any time—you don’t have to wait for approval. However, make sure the template is approved BEFORE you activate your campaign. Your complete audience might not receive the message until it’s approved, due to some of Meta’s regulations. From your campaign’s workflow, follow these steps: Drag in a WhatsApp block from the Build menu. Click Add Content. Choose your template under Design Studio. Preview your message with sample data as you see fit. Unlike Meta-created templates, you don’t need to take the extra steps of assigning liquid variables to your template variables! If you realize you want to use a different template, click Change template to start over. To edit the From or To fields, go back to Design Studio and modify your template there. Translate WhatsApp templates If you want to translate your WhatsApp messages, you have to create a separate template for every translation and submit them for approval. This means you may have to set up a separate WhatsApp message for each translated template and branch your audience based on their language preferences. --- ## Campaigns, broadcasts, and transactional messages URL: https://docs.customer.io/journeys/types-of-campaigns-and-broadcasts/ We support a range of campaigns, broadcasts, and transactional messages to help you automate interactions with your audience at the right time. If you’re just getting started, checkout Start sending campaigns and workflows for a high-level overview. Campaigns Campaigns offer the most robust options for messaging automation. They offer the most flexible and specific options for triggering a campaign as well as the ability to automate messages over time, as opposed to sending messages all at once. Your campaign trigger determines who enters your campaign and when. Most triggers are based around people, like when they match certain criteria. Typically, these people are the subject of your campaign—you’ll send them messages, set their attributes, and so on. Trigger options Attribute or Segment Attribute or Segment lets you trigger campaigns based on profileThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. attributes, segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., or both. Use segments if you want to track people who match this criteria over time or reuse the criteria as a trigger for other campaigns. You can set segment criteria based on your audience’s attributes, events, etc. Use attributes alongside segments or to quickly target specific profile attributes without creating or finding a segment. This trigger is best for automations like recurring NPS surveys, onboarding drip campaigns, and inactivity reminders. Event An eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.-triggered campaign helps you respond to a person’s activity in your app or website. For instance, you could trigger a campaign based on a user abandoning their cart, viewing a specific page on your app, or completing an order. Form submission Form submission lets you take advantage of formConnected Forms in Customer.io allow you to automatically trigger campaigns, send data to other services, and add or update people when they submit forms on your website or in your app. responses to trigger campaigns. You can connect a form to your workspace or integrate with Facebook Lead Ads. Use this type of campaign to send messages to nurture new leads or respond to support requests. Object updated Use Object updated when you want people to enter the campaign every time an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. is updated in your workspace. For instance, if you were tracking accounts as objects and updated the name of one, you could notify everyone that managed the account that the account went through a rebrand. Keep in mind, only people enter into journeys, not objects. You’ll see this as “Object_type_name updated” in your trigger list. In the image above, “Course updated” is an example. When you use objects to trigger campaigns, you can choose who enters into the campaign. The audience could be: every person in the object certain people related to the object Relationship added or changed A relationship is the association between an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and a person. Use this trigger type when you want people to enter a campaign after their relationship to an object has changed. For instance, if you track accounts as objects, this could mean they joined an account (person added), or they are now a manager (relationship changed). You’ll see this as “Person added” or “Relationship changed” in your trigger list. In the image above, “Person added to Course” and “Relationship changes with Course” are examples. When you use relationships to trigger campaigns, you can choose who enters into the campaign. That is, the recipient of the message in this kind of campaign doesn’t have to be the person in the relationship that triggered it. The audience could be: the person whose relationship to the object is updated (default) every person in the object certain people related to the object Important date An important date triggers campaigns on a specific or relative date based on an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that people in your audience have. You can trigger based on any customer attributes that are properly formatted dates. This is useful for recurring campaigns like birthdays, anniversaries, subscription renewals, and payment reminders. Webhook A webhook triggers a campaign based on data from an external service. The purpose of your campaign is to manipulate this data and associate it with people, much like you can with Zapier or Segment, but entirely within a campaign. Because data, not people, is the subject of your campaign, webhook-triggered campaigns don’t typically send messages directly; rather, they let you associate data with people, which can trigger subsequent campaigns. Webhook campaigns help you perform one-to-many interactions with your audience, like notifying a group of people when you post a job or a product becomes available that a cohort of your audience is interested in. Here are some examples of campaigns: Onboarding users who just signed up for your app Abandoned cart reminders Broadcasts Newsletters Newsletters in Customer.io are a form of broadcast; a one-time send of a single message. You might send one for a special feature announcement for your power users or a simple Terms of Service update. Use newsletters if… you want to send a single email to a particular subset of customers. API-triggered broadcasts In an API-triggered broadcast, you craft a workflow to send messages and perform actions like updating users’ attributes, much like a campaign. However, the entire workflow happens once you trigger it, whereas in a campaign, you can modify the timeline with delays. With an API-triggered broadcast, you control when the broadcast is sent; you can trigger it manually in the UI or through our API. You might send one to announce a new product, share a new coupon, or encourage users to take action like a buy a ticket before a certain date. Use API-triggered broadcasts if… you want to regularly trigger a specific message to many people and take advantage of Customer.io’s different message types. Transactional messages Transactional messages are one-to-one interactions with your audience, where the “trigger” represents an individual audience action in your app like a password reset request. Because your audience does something to trigger a transactional response, you can send transactional emails to unsubscribed customers, too. Use transactional messages if… you need to respond directly to customer interactions with your app or service, like password reset info, receipts, shipping notifications, etc. --- ## Tags URL: https://docs.customer.io/journeys/tagging-campaigns/ Tags are another way to organize and compare your campaigns, broadcasts, and transactional messages. You can also assign them to [segments](#manage-tags) and data through your [data index](/journeys/using-data-index/). While you can filter by features like campaign name and trigger type, you can also use tags to group automations based on other information, like themes or goals. Create, assign, edit, and delete as many tags as you need in your workspace. Manage tags You can create, edit, and delete tags from Campaigns, Broadcasts, Transactional messages, Segments, and your Data Index. No matter where you create them, you can assign that tag to any other automation, segment, or attribute moving forward. However, you can only assign tags on individual campaigns, segments, etc. From landing pages Select the tags filter at the top of pages like Campaigns, Segments, etc. Click Manage. Here you can create, edit or delete tags and check their usage. From a draft campaign Click your campaign. Click the name. Click Add tag. Either click Manage to make changes to tags or enter the name of a new tag to create one. From a running or stopped campaign Click your campaign. Click Add tag in the Overview tab. Either click Manage to make changes to tags or enter the name of a new tag to create one. From your data index You can manage tags for your data, like people’s attributes and event properties, through the data index. You can manage them from the landing page like you can with campaigns, broadcasts, etc. To assign tags, click Add tag from the landing page or click the data, then click the pencil icon besides the attribute name, and choose a tag. Filter by tag To filter campaigns, segments, etc by tag, you can either select the tag from the dropdown at the top or click the tag under the item’s name. Generate reports by tag Tagging your campaigns and/or broadcasts based on messaging category is a great way to analyze your different messaging strategies. For example, if you’d like to understand how your marketing messages alone are performing, you can run a report on all campaigns and broadcasts with the ‘marketing’ tag. Go to the Analysis page. Add the tag(s) you’d like to view as part of the filter criteria. This will return metrics for all automations checked at the top. --- ## Campaign concepts & settings URL: https://docs.customer.io/journeys/campaigns-in-customerio/ Campaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. They're the most flexible, robust automation offered by Customer.io. This page introduces the concepts behind campaigns to help you get started. If you’re new to Customer.io, checkout campaigns, broadcasts, and transactional messages to make sure you’re creating the right kind of automation. How it works A campaign is a workflow to send messages and manipulate data in Customer.io. It consists of four major components: Trigger: this determines who enters your campaign (trigger and filter conditions) and when (frequency setting). Filters are optional. Goals: the outcome you want your customers to achieve through the campaign. You don’t need to define a goal, but it can be helpful to gauge the success and health of your campaigns. Exit criteria: these determine if people should leave your campaign early or not. Workflows: the messages you want to send and other actions you want to perform for people who enter your campaign. Your campaign starts when someone (or some data) matches your trigger condition. Then your customers move through the workflow—we call this a journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s.—until they meet your goal or exit criteria. flowchart LR a{Does a person or data meet trigger conditions?}-->|yes|c{If filter exists, does person or data meet conditions?} a-.->|no|i[person doesn't enter campaign] c-.->|no|i c-->|yes|j subgraph b [Your campaign] direction TB j{Does person meet goal/exit conditions?}-->|no|d[Send message or take action] d-->f{Does person meet goal/exit conditions?} f-->|no|g[Send message or take action] end j-.->|yes|e[exit campaign] f-.->|yes|h[exit campaign] g-->|Person finishes campaign|k[exit campaign] Campaign setup Campaign triggers Your campaign trigger determines who enters your campaign and when. Most triggers are based around people, like when they match certain criteria. Typically, these people are the subject of your campaign—you’ll send them messages, set their attributes, and so on. Trigger options Attribute or Segment Attribute or Segment lets you trigger campaigns based on profileThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. attributes, segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., or both. Use segments if you want to track people who match this criteria over time or reuse the criteria as a trigger for other campaigns. You can set segment criteria based on your audience’s attributes, events, etc. Use attributes alongside segments or to quickly target specific profile attributes without creating or finding a segment. This trigger is best for automations like recurring NPS surveys, onboarding drip campaigns, and inactivity reminders. Event An eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.-triggered campaign helps you respond to a person’s activity in your app or website. For instance, you could trigger a campaign based on a user abandoning their cart, viewing a specific page on your app, or completing an order. Form submission Form submission lets you take advantage of formConnected Forms in Customer.io allow you to automatically trigger campaigns, send data to other services, and add or update people when they submit forms on your website or in your app. responses to trigger campaigns. You can connect a form to your workspace or integrate with Facebook Lead Ads. Use this type of campaign to send messages to nurture new leads or respond to support requests. Object updated Use Object updated when you want people to enter the campaign every time an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. is updated in your workspace. For instance, if you were tracking accounts as objects and updated the name of one, you could notify everyone that managed the account that the account went through a rebrand. Keep in mind, only people enter into journeys, not objects. You’ll see this as “Object_type_name updated” in your trigger list. In the image above, “Course updated” is an example. When you use objects to trigger campaigns, you can choose who enters into the campaign. The audience could be: every person in the object certain people related to the object Relationship added or changed A relationship is the association between an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and a person. Use this trigger type when you want people to enter a campaign after their relationship to an object has changed. For instance, if you track accounts as objects, this could mean they joined an account (person added), or they are now a manager (relationship changed). You’ll see this as “Person added” or “Relationship changed” in your trigger list. In the image above, “Person added to Course” and “Relationship changes with Course” are examples. When you use relationships to trigger campaigns, you can choose who enters into the campaign. That is, the recipient of the message in this kind of campaign doesn’t have to be the person in the relationship that triggered it. The audience could be: the person whose relationship to the object is updated (default) every person in the object certain people related to the object Important date An important date triggers campaigns on a specific or relative date based on an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that people in your audience have. You can trigger based on any customer attributes that are properly formatted dates. This is useful for recurring campaigns like birthdays, anniversaries, subscription renewals, and payment reminders. Webhook A webhook triggers a campaign based on data from an external service. The purpose of your campaign is to manipulate this data and associate it with people, much like you can with Zapier or Segment, but entirely within a campaign. Because data, not people, is the subject of your campaign, webhook-triggered campaigns don’t typically send messages directly; rather, they let you associate data with people, which can trigger subsequent campaigns. Webhook campaigns help you perform one-to-many interactions with your audience, like notifying a group of people when you post a job or a product becomes available that a cohort of your audience is interested in. Goals A goal helps you track the success of a campaign based on whether a person performs an event, enters a segment, or exits a segment. If a person achieves your goal, we mark the messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. and journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. as converted. Then you can track conversion rates over time to improve your messaging strategies. We track conversions for the following message/delivery types: Conversions attributed Conversions not attributed Email Slack Message SMS Create or update person action Customer.io Push Notifications Customer.io In-app Messages Webhooks1 1You must enable webhook conversions manually. Slack and Create or update person actions are often internal or used for analytics purposes; they don’t always send messages to end-users. For that reason, we don’t attach conversions to them. You can enable webhook conversions on individual webhook actions. Exit conditions Exit conditions determine if or when a person should exit your campaign. For example, imagine that you have a campaign to re-engage users if they haven’t logged into your platform for a week; your goal is for people to log back into your service. When people do log in, you probably want them to exit the campaign so you stop sending them messages after they achieve your goal. Message settings Subscription preferences If you use our subscription center feature, you’ll set your campaign’s topicA category of message, set within your workspace’s subscription center, that people can subscribe to or unsubscribe from. Topics let your audience determine the kinds of messages they want to get from you. in the Subscription preferences setting. Keep in mind, subscription preferences only affect whether people receive certain messages, not whether they enter your campaign or go through other actions like an attribute update. If people are unsubscribed from the topic, they won’t get messages from the campaign. They would, however, continue to receive in-app messages; subscription preferences apply to email, SMS, and push. If your campaign includes non-message actions (like Create or Update Person), those actions will still apply to people who aren’t subscribed to the topic. If you don’t want to set a topic, you can use the All subscribed and unsubscribed preference. You should use this setting sparingly—for things like transactional-style campaigns and important notices. Sending messages to unsubscribed people can violate their trust—or even violate local laws and regulations (GDPR, CAN-SPAM, etc)! If you don’t use our subscription center, your campaign will send to all subscribed by default. Message limit If you’ve set a message limit in your workspace settings, you can determine whether your campaign, or individual messages in your campaign, count towards your message limit. A message limit determines the maximum number of messages you can send to a person within a time period. Workflow builder You’ll craft your workflow—the messages and actions people move through during their journey—using our Build menu. You can add a variety of items—messages, webhooks, attribute updates, time delays, and more-to set up your campaign. Individual message settings Select a message in your workflow, and the settings will appear in the right-hand panel: Settings change based on the kind of message or action you select, but common settings include: Tracking opens and clicks: whether or not to track opens and clicks for a message; this is on by default Sending behavior: whether or not the message sends automatically, queues a draft, or doesn’t send at all; we queue drafts by default Subscription preference setting: whether or not to use campaign settings or override them; this respects campaign settings by default Holdout test: a type of A/B test. Check out how to Create a conclusive A/B test result and how to Understand A/B test results for help. Subscription preference By default, your messages inherit your campaign settings. But you can change this within a message’s settings: If you enabled the subscription center for your workspace, you can change this message to send to people subscribed to a different topic or all people in the campaign regardless of their subscription status. If you have not enabled the subscription center, you can change this to send to all subscribed or all people regardless of their subscription status.  Abide by your audience’s local laws Keep in mind that you should only send messages to unsubscribed people in transactional use cases. Liquid and customer data in campaigns You can personalize data in messages using 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}}.. Reference people’s attributes in this format: {{customer.<attribute_name>}} For example, if you’re sending us a customer’s name using the attribute full_name, you can utilize it like this: Hi, {{customer.full_name}}! You can also perform more complex operations using the data you send; we have more comprehensive information here. Event attributes A few things to remember when using event attributes: They can be used in message content You can use all of the data you send with an event in your messages. For example, if you send us a purchase event with the following data: {"name":"purchase","data":{"price": "9.99", "product": "socks", "color":"blue"}} then you can send a receipt email (or push notification, or SMS, or any other action) with the product’s name, price, and color in it. Whatever you send in the event attributes is available to you; you can learn more about using event data here. They can override certain email headers If you send any of these attributes as part of your event, they will override your campaign settings: from_address, recipient, reply_to For example: If a purchase event triggers an email to send, but that purchase event contains recipient = wile.e.coyote@example.com as an attribute, then all emails triggered by that campaign will go to wile.e.coyote@example.com, no matter the settings in the campaign itself. Activate your campaign Check out this tutorial on activating a campaign: After you’ve set up your campaign, you’ll click Review items if you have setup left to do then Start campaign when you’re ready to review the entirety of your setup. Current vs future additions You can decide whether current people and future additions or only future additions should trigger your campaign. This applies to campaigns triggered by segments, attributes, objects, or relationships: Current people and future additions: People who match your campaign criteria after you start it will enter the campaign. And people who already match your trigger criteria will immediately enter your campaign. Future additions only: This only includes people who match your trigger criteria after you start the campaign. Learn about when backfilled people data can trigger campaigns. Date-triggered campaigns always trigger for both current matches and future additions. For event-triggered campaigns, people enter the campaign when they perform the event after you start the campaign. Learn about when backfilled event data can trigger campaigns. You can create a campaign triggered by a segment based on a specific event so you can include current matches, but keep in mind, you won’t be able to include any event attributes in the content of the workflow. Schedule your campaign You can schedule your campaign to start or stop at a specific date and time. This gives your team the flexibility to plan and execute time-bound marketing initiatives with fewer manual steps. For instance, you might have a seasonal campaign or limited-time offers that you want to make sure are only available during the relevant time period. Campaign metrics & reporting Once a campaign is running, you can view reports and export them to CSV from the Metrics tab. The export will reflect the dropdown selection: All-time metric totals Message metrics Journey metrics or Tracked responses for in-app messages It will also include any filters you selected. For instance, if you select “Email” for the “Last 30 days” under Delivery Metrics, your export will only include the filtered data. You can learn more about how we define each metric in Campaign and Broadcast Metrics. We can also send reporting webhooks with performance data to a URL you specify so you can receive information about events as they occur in real-time. Customer.io sends the information as JSON in an HTTP POST. Read more about setup here. Check out our Data-out integrations overview for more options. --- ## Campaign journeys URL: https://docs.customer.io/journeys/campaign-journeys/ When a person enters a campaign, they start a journey. They let you follow people's history through a campaign like the deliveries they've received, conversions, and when they entered/exited. You can find them in the *Journeys* tab of a campaign. View and troubleshoot journeys Journeys help you track people’s current or historical pathway through your campaign. This makes it very useful for troubleshooting issues like people not proceeding in your workflow or exiting campaigns when you’d expect. To access individual journeys, go to the Journeys tab of Campaigns or a person’s profile. In this campaign, you can see this person entered the campaign at 4:51pm and left it at 4:58pm, a short journey! You can see how the person was impacted by each workflow action along the way. Learn more about using this page to help you troubleshoot why people aren’t entering or why people aren’t receiving messages when you’d expect. When journeys begin If your campaign doesn’t have filter conditions, the journey starts immediately when a person matches your trigger criteria. If the campaign has filter conditions, the following happens: For legacy segment-triggered campaigns, a journey starts as soon as a person matches the campaign’s trigger conditions. If they don’t match the filter conditions, they’ll wait to continue your workflow until they meet these conditions or exit after a period of time. Learn more about how we evaluate filters for legacy segment-triggered campaigns. You know you’re using a legacy segment trigger if the trigger panel has filter conditions; the latest segment trigger does not have filters. Check out Triggers with segments for more on the differences. For campaigns triggered by an event, object, relationship, date or form, a journey starts after the trigger conditions AND filter conditions are met. Keep the following in mind: If the person does not meet the filter conditions when we receive the event, we will retry for up to 30 minutes. If the person matches your filter conditions within 30 minutes, they start a journey. When journeys end When journeys end depends on your exit conditions, which you can modify in your campaign’s settings. You know a person exited if the journey shows “Exited early” or “Finished”. The journey log will show “Left campaign” next to the timestamp. Manually end a journey You can end a journey for a person from the Journeys tab of a campaign or a person’s profile. Click the journey you want to end. Then click End this journey on the right. The journey will update to “Exited early.” And you’ll see this as the reason: “The person was manually removed from the campaign.” --- ## Create a campaign URL: https://docs.customer.io/journeys/create-a-campaign/ Campaigns are automated workflows that send people messages and perform other actions when people meet certain criteria. This page takes you step-by-step through creating a campaign from scratch.  New to campaigns? Check out campaign concepts & settings to get started. Here’s a brief video showing the main components of our campaign builder. And here’s a walkthrough on creating an onboarding campaign! Before you begin Before you begin, determine the purpose of your campaign. This will help you define the trigger and goal of your campaign. Are you trying to reach people who have just signed up? Are you interested in notifying people about changes to accounts they manage? Or maybe you want to promote a new feature in your product? Check out our recipes to see if we’ve covered your use case! Create a campaign & set a trigger To create a campaign from scratch: Go to Campaigns. Then click Create Campaign. Click Choose trigger on the canvas. Select a trigger type. Our most popular trigger is Attribute or Segment; it’s how you trigger campaigns based on a change to a person’s profile. After you save this trigger, you can change how often your audience enters the campaign through the frequency setting. Configure campaign settings Configure the campaign’s settings so you can: Easily identify the purpose of the campaign through the name, description, and tags Track the success of the campaign through a goal Limit who receives messages through subscription preferences Determine whether people exit early through exit conditions Check out this walkthrough on configuring these settings: Click “Untitled” in the top left to change the name so your team members can easily find the campaign on the landing page. (Optional) Add a description so your team members can tell what the purpose of the campaign is. (Optional) Add one or more tags to help you organize and filter your campaigns on the landing page. Click the top-left block then Set goal. You can choose “No goal” if you don’t want to set one up. This image shows the goal is achieved when a person performs the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. profile_setup_complete within 1 week of being sent any delivery from this campaign. Modify your Message settings. Click Manage under Messages to change what subscription center topic people must be subscribed to to receive messages. If you don’t have the subscription center enabled, we’ll send to people who are globally subscribed. Set a message limit if you want people to only receive a certain number of messages from you over a period of time. Click Manage under Exit. You can change when people exit early or prevent them from exiting early here. Build your workflow Build the workflow that people will follow after triggering the campaign. When a person moves through a campaign, we call that a journey. Click Build at the bottom of the workflow. Click and drag a message, data, delay, or flow control block from the panel onto your canvas. After you add your first block, you can drop subsequent blocks over any plus sign. Here’s a walkthrough of setting up an email in a campaign:  Shortcuts & useful features At the bottom, hover over the lightbulb icon to learn keyboard shortcuts, export a picture of your workflow to share with team members, zoom in and out, or add sticky notes to inform team members of important details. Review & start your campaign Before you start your campaign, you likely want to change the Sending Behavior for all your messages from Queue Draft to Send Automatically. By default, all messages are set to “Queue draft.” This means you have to manually send them after people reach your message block in the workflow. If you want messages to send automatically, click into each message and adjust the dropdown: After you’re finished building your workflow, click Start Campaign. If you haven’t fully set up your campaign, click Review items and complete setup. Depending on the type of campaign you’re making, you may need to specify whether you send to all people who meet your conditions or just people added to your workspace after you start the campaign. Click Start Campaign. Congrats! You created a campaign from scratch! You’ll land on your campaign’s overview page where you’ll see a breakdown of metrics, drafted messages, sent messages, and all of your customers’ journeys through the campaign. --- ## Campaigns page URL: https://docs.customer.io/journeys/intro-to-campaigns/ Campaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. This article describes what you can do on the campaigns page. Click Campaigns in the left-hand menu to find a list of your campaigns. You can filter for your campaigns, change the default data you see, modify the state of your campaigns, or initiate creating one. Search for campaigns You can filter to find campaigns by: name description triggerA trigger determines who enters your campaign and when. status - whether your campaign is active (running) or not (draft, stopped) topic - the subscription preferenceCustomer.io’s subscription center feature provides a way for customers to subscribe to, or unsubscribe from, specific topics. This helps you manage your audience’s preferences and make sure that they only get the messages they’re interested in. you’ve set for messages tags you’ve used to organize your campaigns in your workspace object typesAn object type is a group of objects. An object type could be Online Classes while an object within the type could be English 101. Customer.io generates a unique, immutable object_type_id. - the results include campaigns whose triggers are objects or relationships and campaigns with messages that contain object liquid Table view The campaign list contains a series of icons to help you understand at a glance what type of campaign you’re viewing. Each trigger type has its own icon to the left of your campaign name. Hover over the icon to see which type. Each campaign also lists the types of messages and actions you’ll find. Hover over each to see a list of each action. Modify the default display Click the table icon to change the view of your campaigns. This will only the change the default view for you, not other team members. You can change the display based on campaign data like name and metrics. Adjust the date range, which metrics appear, and what data to sort by. You can view 20 campaigns per page. Change state You can also change the state of a campaign from the landing page. You can’t activate a campaign from this view, but you can stop, archive, or delete them by selecting the three vertical dots to the right of each campaign. --- ## Triggers, filters, and frequencies URL: https://docs.customer.io/journeys/campaign-triggers/ Campaign triggers determine who enters your campaigns and when. This page describes different kinds of triggers and why you might use them. If you’re just getting started, we’ve created a quick video to help you understand what each campaign trigger does. To learn about object and relationship triggers, read on. How it works A campaign trigger determines who enters your campaign and when. You can set up filters to narrow your trigger criteria. People need to meet your trigger and filter criteria to enter your campaign. You can also set your exit conditions to make people exit your campaign when they quit matching your trigger and filter criteria, ensuring that your campaign is relevant to your audience. If you want to let people experience a campaign multiple times, you can set frequency settings, determining how often a person can trigger a campaign. flowchart LR a{Does a person meet trigger conditions?}-->|yes|c{Does person meet filter conditions?} c-->|yes|d[person enters campaign] a-.->|no|i[person doesn't enter campaign] c-.->|no|i What kinds of actions trigger a campaign? Your campaign trigger determines who enters your campaign and when. Most triggers are based around people, like when they match certain criteria. Typically, these people are the subject of your campaign—you’ll send them messages, set their attributes, and so on. Trigger options Attribute or Segment Attribute or Segment lets you trigger campaigns based on profileThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. attributes, segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., or both. Use segments if you want to track people who match this criteria over time or reuse the criteria as a trigger for other campaigns. You can set segment criteria based on your audience’s attributes, events, etc. Use attributes alongside segments or to quickly target specific profile attributes without creating or finding a segment. This trigger is best for automations like recurring NPS surveys, onboarding drip campaigns, and inactivity reminders. Event An eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.-triggered campaign helps you respond to a person’s activity in your app or website. For instance, you could trigger a campaign based on a user abandoning their cart, viewing a specific page on your app, or completing an order. Form submission Form submission lets you take advantage of formConnected Forms in Customer.io allow you to automatically trigger campaigns, send data to other services, and add or update people when they submit forms on your website or in your app. responses to trigger campaigns. You can connect a form to your workspace or integrate with Facebook Lead Ads. Use this type of campaign to send messages to nurture new leads or respond to support requests. Object updated Use Object updated when you want people to enter the campaign every time an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. is updated in your workspace. For instance, if you were tracking accounts as objects and updated the name of one, you could notify everyone that managed the account that the account went through a rebrand. Keep in mind, only people enter into journeys, not objects. You’ll see this as “Object_type_name updated” in your trigger list. In the image above, “Course updated” is an example. When you use objects to trigger campaigns, you can choose who enters into the campaign. The audience could be: every person in the object certain people related to the object Relationship added or changed A relationship is the association between an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and a person. Use this trigger type when you want people to enter a campaign after their relationship to an object has changed. For instance, if you track accounts as objects, this could mean they joined an account (person added), or they are now a manager (relationship changed). You’ll see this as “Person added” or “Relationship changed” in your trigger list. In the image above, “Person added to Course” and “Relationship changes with Course” are examples. When you use relationships to trigger campaigns, you can choose who enters into the campaign. That is, the recipient of the message in this kind of campaign doesn’t have to be the person in the relationship that triggered it. The audience could be: the person whose relationship to the object is updated (default) every person in the object certain people related to the object Important date An important date triggers campaigns on a specific or relative date based on an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that people in your audience have. You can trigger based on any customer attributes that are properly formatted dates. This is useful for recurring campaigns like birthdays, anniversaries, subscription renewals, and payment reminders. Webhook A webhook triggers a campaign based on data from an external service. The purpose of your campaign is to manipulate this data and associate it with people, much like you can with Zapier or Segment, but entirely within a campaign. Because data, not people, is the subject of your campaign, webhook-triggered campaigns don’t typically send messages directly; rather, they let you associate data with people, which can trigger subsequent campaigns. Webhook campaigns help you perform one-to-many interactions with your audience, like notifying a group of people when you post a job or a product becomes available that a cohort of your audience is interested in. Triggers with segments Our attribute or segment trigger lets you launch campaigns based on people’s attributes—like plan type, region, or signup date—without needing to build a segment first. And when you need more control, you can still use segments alongside attributes. This new trigger launched in April 2025. Any campaigns made with our legacy segment trigger will continue to run as expected. You can still edit their triggers and duplicate legacy segment-triggered campaigns too. Legacy segment trigger Latest segment trigger Differences between triggers: attribute or segment vs segment change The new attribute or segment trigger gives you more flexibility and control over when people enter your campaign. While it works differently from our legacy segment trigger, it’s designed to simplify setup and avoid unnecessary journeys. Here’s what’s different: Filter conditions are now part of the trigger conditions. You may need to add time-based conditions to ensure the right group of people trigger your campaigns. Filter conditions are now part of the trigger conditions In the legacy segment trigger, you could add filters to your trigger conditions to narrow your audience further. People would enter the campaign when they matched the trigger segment—but they wouldn’t move through the workflow unless they also matched the filters. This often led to unnecessary journeys: people would log as having entered the campaign, but never move forward because they didn’t meet your filters. Eventually, they’d exit the journey without receiving any messages. With the new attribute or segment trigger, you define previous filter conditions in the trigger itself. That means: People only enter the campaign when they meet all your criteria. You avoid incomplete journeys and keep your reporting clean. Add time-based logic to target the right users You may need to add time-based conditions to ensure the right group of people trigger your campaigns. The legacy segment trigger included automatic “matchtime” logic — it looked at the following to decide when people should enter a campaign: When relevant attributes were last updated When relevant events were last sent The timestamp itself of relevant date/time attributes The new trigger doesn’t do this automatically. Instead, you define when people should enter using time-based conditions in your trigger or delays in your workflow. This gives you more control and predictability. Here’s how this works in practice: Migrate users and backfill data Imagine you have a running campaign that uses a legacy segment trigger. People enter when they join the segment “Signed Up.” The segment is defined by a person’s attribute created_at is a timestamp. Then you migrate some users from another platform to Customer.io and backfill their data. If their created_at timestamps are more than 24 hours before now, those people would not trigger the campaign. With the new attribute or segment trigger, people will enter the campaign as long as they meet the trigger conditions—regardless of how old their data is. That means backfilled users could start the campaign right away. To exclude people with older timestamps, add a time-based trigger condition like Attribute: created_at is a timestamp after a relative date of 1 day ago. This ensures that only people whose created_at timestamp is within the last day will trigger the campaign. Delay people at the beginning of your campaign Imagine you have a campaign that uses a legacy segment trigger. People enter when they join the segment “Trial users” which is based on the condition plan_type is equal to trial. The first step in your campaign is a 3-day Wait until block. You start the campaign and include current matches in the audience. If someone’s plan_type updated to trial 1 day ago, matchtime logic would apply. That person would only wait 2 more days because of the time of their attribute update. With the new attribute or segment trigger, matchtime isn’t used. People enter the campaign the moment they meet the trigger condition and would wait the full 3 days. If you want to time the delay based on when someone actually started their trial, pass a timestamp attribute like trial_start_date, and use it in a condition of a Wait until block at the beginning of your campaign: trial_start_date is a timestamp after a relative date of 3 days ago. This gives you full control over when people move forward, based on the actual timing of their trial—not just when they entered the campaign. Wait until a future date Both of the previous examples showcase matchtimes in the past. Let’s take a look at one more example where matchtimes are in the future. Imagine you have a campaign that starts when someone purchases a new subscription. The campaign uses a legacy segment trigger where the segment is defined by the condition plan_start is a timestamp. Let’s say someone’s plan_start is in two days. In this setup, this person would enter the campaign before their plan starts—but they would be held at the beginning of your workflow until the timestamp (their actual plan start date) is reached. With the new attribute or segment trigger, people start the workflow as soon as they meet the trigger conditions. That means they won’t automatically wait until the plan start date unless you explicitly add that logic. If you want people to wait until a future date, use a date-triggered campaign. In some cases, adding a Wait until block at the start of your workflow (based on a timestamp like plan_start) can work too. More on matchtimes These examples include simple triggers, but if you want to calculate matchtimes for legacy segment triggers with multiple conditions, consider the following: If the conditions are joined by an AND, the matchtime is the most recent of the conditions. If the conditions are joined by an OR, the matchtime is the oldest of the conditions. Attribute or segment With the Attribute or Segment trigger, you can target a range of data through segments or quickly specify profile attributes to define your audience: Add profileThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. attributes to quickly target people who should trigger this campaign Add segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. if you want to reuse a specific set of criteria across campaigns and track membership over time Or add attributes AND segments to the trigger criteria to hone in on your exact audience  You can only directly specify profile attributes If you want to target event attributes, message data, object attributes, etc, you’d have to create a segment with that criteria. Click Attribute or Segment to get started: Then choose your trigger criteria. Choose whether people should meet All or At least one condition to start the campaign. You can specify profile attribute conditions and/or segments.  You cannot create campaigns with only a not in segment condition If you’re triggering a campaign using segments, the trigger must have at least one in condition or one profile attribute condition for you to save it. We do not trigger campaigns solely off of people not belonging to a segment. Frequency By default, people will enter your campaign once. If your campaign does not use a Filter, you can let customers enter your campaign multiple times with the Frequency setting. When you enable Frequency settings, you’ll use one of these options to determine how often a person can re-enter your campaign: Every re-match: people will enter this campaign when they match and re-match the trigger conditions; they must stop matching the trigger conditions then re-match the conditions to re-trigger the campaign. They must re-match after they’ve exited the campaign, which means if they re-match during the campaign, they will not re-trigger the campaign upon exiting. They would have to stop matching and re-match again after they exit the campaign. They must re-match after the minimum wait time has elapsed, as well. In the example below, the minimum wait time is 1 day. For example, imagine a campaign that triggers when someone joins the segment “Inactive for two weeks” and the frequency is “Every re-match.” When people enter that segment, they trigger a campaign and receive a message. Later, they exit the campaign. Then the minimum wait time elapses. To re-enter the campaign, they must exit then re-enter the “Inactive for two weeks” segment (become active, then become inactive again). At fixed intervals: people will repeat the campaign at a set interval, provided they match the conditions when the interval elapses. A person cannot re-trigger a campaign while they are actively in the campaign. The campaign will check if a person can re-enter based on their initial entry. For example, imagine a campaign has a frequency of “At fixed intervals” and two days should elapse between entry and re-entry. If a person’s first journey started March 22 at 11 am, then the campaign will check for re-entry on March 24 at 11 am. If the journey is still active, a new journey won’t begin. The next time it will check is March 26 at 11 am. If they exited the campaign at 9 am on the 26th, they would start a new journey. However, if they don’t exit until 12 pm, they won’t re-enter again until the 28th at 11 am.  We start to measure the interval when a person enters the campaign Even if your campaign includes a delay, the interval window begins when a person matches your campaign conditions.  Frequency settings apply to people you manually remove from campaigns You can manually remove people from campaigns, but this won’t necessarily cause them to re-enter the campaign. For example, if a person can only enter a campaign once ever, and you remove a person from the campaign, they’ll never re-enter the campaign. Deprecated: Segment change  Use the attribute or segment trigger to create segment-triggerd campaigns We’ve phased out our “Segment change” trigger. However, campaigns made with our legacy segment triggers will continue to run as expected! You can still edit the trigger and duplicate these campaigns too. Segments are based on audience criteria—attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. your audience has, events they’ve performed, making this one of our more flexible campaign types. Click Segment change to get started: We’ll prompt you to choose one or more segments that your audience is in or not in. If you haven’t created a segment yet, you can click Create a new data-driven segment to set conditions for a new segment.  You cannot create campaigns with only a not in condition A segment-triggered campaign must have at least one in condition for you to save it. We do not trigger campaigns solely off of people not belonging to a segment. You can set multiple segments using and or or conditions. Use and to trigger campaigns only when people meet all of your conditions. Click + Add condition to create an and condition. Use an or condition to trigger a campaign based on a person belonging to any of the segments. Add segments to the same field to create an or condition:  You can use JSON dot notation in condition logic If you store attributes or event data in JSON objects or arrays, you can use JSON dot notation in your branch conditions to evaluate these properties. Use array[] to represent any item in an array or array[0] to represent the first item in the array. See Storing and using JSON for more information about dot notation in Customer.io. Filter With segment-triggered campaigns, you can also add a segment filter. You should weave your segment filter criteria into your segment trigger conditions as much as possible, but if you find you cannot accomplish what you need without a segment trigger AND segment filter, please let us know! We want to account for this use case as we develop the next generation of campaigns. Frequency By default, people will enter your campaign once. If your campaign does not use a Filter, you can let customers enter your campaign multiple times with the Frequency setting. When you enable Frequency settings, you’ll use one of these options to determine how often a person can re-enter your campaign: Every re-match: people will enter this campaign when they match and re-match the trigger conditions; they must stop matching the trigger conditions then re-match the conditions to re-trigger the campaign. They must re-match after they’ve exited the campaign, which means if they re-match during the campaign, they will not re-trigger the campaign upon exiting. They would have to stop matching and re-match again after they exit the campaign. They must re-match after the minimum wait time has elapsed, as well. In the example below, the minimum wait time is 1 day. For example, imagine a campaign that triggers when someone joins the segment “Inactive for two weeks” and the frequency is “Every re-match.” When people enter that segment, they trigger a campaign and receive a message. Later, they exit the campaign. Then the minimum wait time elapses. To re-enter the campaign, they must exit then re-enter the “Inactive for two weeks” segment (become active, then become inactive again). At fixed intervals: people will repeat the campaign at a set interval, provided they match the conditions when the interval elapses. A person cannot re-trigger a campaign while they are actively in the campaign. The campaign will check if a person can re-enter based on their initial entry. For example, imagine a campaign has a frequency of “At fixed intervals” and two days should elapse between entry and re-entry. If a person’s first journey started March 22 at 11 am, then the campaign will check for re-entry on March 24 at 11 am. If the journey is still active, a new journey won’t begin. The next time it will check is March 26 at 11 am. If they exited the campaign at 9 am on the 26th, they would start a new journey. However, if they don’t exit until 12 pm, they won’t re-enter again until the 28th at 11 am.  We start to measure the interval when a person enters the campaign Even if your campaign includes a delay, the interval window begins when a person matches your campaign conditions.  Frequency settings apply to people you manually remove from campaigns You can manually remove people from campaigns, but this won’t necessarily cause them to re-enter the campaign. For example, if a person can only enter a campaign once ever, and you remove a person from the campaign, they’ll never re-enter the campaign. Object change You can trigger a campaign when an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. is updated in your workspace. While an object triggers the campaign, only people move through it. See Audience below to define who enters the campaign. On the trigger step of setting up a campaign, you will see a list of all object types in your workspace. Underneath each object type, you’ll see object triggers listed as “Object_name updated,” like “Account updated.”  To use an object trigger, you must create at least one object. If you select Object updated, you must specify which attributes to target. This type of campaign triggers when specific attributes change, not when any attribute changes. For instance: You can use a variety of operators like “equal to” or “exists.” You cannot use operators like greater than, less than, etc. If you need other operators, consider adding a segment filter or passing your information to Customer.io so the existing operators work.  We only evaluate trigger conditions before a person enters a journey This means a person could continue to move through your object-triggered campaign if the object were deleted or their relationship to the object was removed or changed. If they shouldn’t, add filters to your campaign (see below). It also means messages could fail to send if you reference this deleted relationship or object in liquid, so make sure you provide a liquid fallback. Frequency For now, campaigns with custom object triggers only support the “every re-match” frequency. Stay tuned for more frequency options in upcoming releases. Audience You have the flexibility to decide who your audience is. The audience could be: every person related to the object certain people related to the object based on profile or relationship attributes  An object or relationship can’t fan out to more than 1,000 people. If an update triggers journeys for more than 1,000 related people, none of them start a journey. You’ll see “Failed Journeys for Object/Relationship Campaign” in your activity log. This limit is per trigger event, not per campaign—your campaign can have more than 1,000 total recipients as long as each individual trigger doesn’t fan out to more than 1,000 people. Learn more about the fan-out limit. Last, you’ll review your audience before you start your campaign. Select whether the campaign should trigger for current matches and future additions or only future additions. For instance, if you trigger a campaign when the plan of an Account object changes to premium, selecting “Current people and future additions” means people who are already related to accounts with premium plans will enter.  A person can enter this campaign more than once If a person is related to multiple objects that meet the trigger conditions, they enter the campaign once for each qualifying object. For example, if three accounts related to the same person update their plan to premium, that person enters the campaign three times—once per account. Keep this in mind when you design your workflow and messages. Filters You can apply filters to further refine who should enter and stay in the campaign. We evaluate filters when the person first meets the trigger criteria and before action items in your workflow. For example, if your campaign is designed to nurture leads, you might filter out people who’ve already paid for your services. Exit Conditions You can decide when a person exits an object-triggered campaign. You can find these under Exit in Campaign settings. You can choose that: they never exit. they only exit when they match the conversion criteria. they exit when they stop matching filter criteria. they exit when they stop matching filter criteria OR match the conversion criteria. We evaluate exit conditions before every journey and before action items in your workflow. We do not evaluate whether people meet the trigger criteria after they’ve entered the campaign. Relationship change You can trigger a campaign based on a relationship - the association between an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and a person. While a relationship triggers the campaign, only people move through it. See Audience below to define who enters the campaign. On the trigger step of setting up a campaign, you will see a list of all object types in your account. Underneath each object type, you’ll see relationship triggers listed as “Person added” or “Relationship changed.”  To use a relationship trigger, you must create at least one object. If you select Person added, you can optionally refine the audience by relationship attributes to funnel the right people into the campaign. Otherwise, the campaign will trigger when any person is added to any object within the specified object type. If you select Relationship changed, you must specify which relationship attributes to target. For instance: You can use a variety of operators like “equal to” or “exists.” You cannot use operators like greater than, less than, etc. If you need other operators, consider adding a segment filter or passing your information to Customer.io so the existing operators work. You cannot target any change to a relationship; you must specify what a relationship attribute is specifically changing to or changing from (role is equal to admin or role is not equal to admin, for instance).  We only evaluate trigger conditions before a person enters a journey This means a person could continue to move through your relationship-triggered campaign if the object were deleted or their relationship to the object was removed. For example, imagine a campaign triggered by a relationship matching role equal to admin for an object. If a person matches this criteria then their role changes to member, the person would continue their journey. If they shouldn’t, add filters to your campaign (see below). It also means messages could fail to send if you reference this deleted relationship or object in liquid, so make sure you provide a liquid fallback. Frequency For now, campaigns with relationship triggers only support the “every re-match” frequency. Stay tuned for more frequency options in upcoming releases. Audience You have the flexibility to decide who your audience is. The audience could be: the person that was added to the object (default) every person in the object certain people related to the object based on profile or relationship attributes  An object or relationship can’t fan out to more than 1,000 people. If an update triggers journeys for more than 1,000 related people, none of them start a journey. You’ll see “Failed Journeys for Object/Relationship Campaign” in your activity log. This limit is per trigger event, not per campaign—your campaign can have more than 1,000 total recipients as long as each individual trigger doesn’t fan out to more than 1,000 people. Learn more about the fan-out limit. Last, you’ll review your audience before you start your campaign. Select whether the campaign should trigger for current matches and future additions or only future additions. For instance, if you trigger a campaign when a person is added to an Account as an Admin, selecting “Current people and future additions” means people who are already admins of an account will enter.  A person can enter this campaign more than once If a person has multiple relationships that meet the trigger conditions, they enter the campaign once for each qualifying relationship. For example, if a person is added to three different accounts as an admin, they enter the campaign three times—once per relationship. Keep this in mind when you design your workflow and messages. Filters You can apply filters to further refine who should enter and stay in the campaign. We evaluate filters when the person first meets the trigger criteria and before action items in your workflow. For example, if your campaign is designed to nurture leads, you might filter out people who’ve already paid for your services. Exit Conditions You can decide when a person exits a relationship-triggered campaign. You can find these under Exit in Campaign settings. You can choose that: they never exit. they only exit when they match the conversion criteria. they exit when they stop matching filter criteria. they exit when they stop matching filter criteria OR match the conversion criteria. We evaluate exit conditions before every journey and before action items in your workflow. We do not evaluate whether people meet the trigger criteria after they’ve entered the campaign. Form submission You can connect forms—Facebook Lead Ads, Jotform, Typeform, your own custom web forms, etc—to your workspace. You can trigger a campaign when someone fills out your form. This can help you nurture new leads or respond to support requests. When you select the When someone fills out a form on your website option, you can either connect a new form or select an existing form—if you’ve already added forms to your workspace. Filters You can apply segment-based filters to determine whether someone in the campaign should receive messages. For example, if your campaign is designed to nurture leads, you might filter out people who’ve already paid for your services. Frequency Much like event-triggered campaigns, a person enters your campaign every time they fill out your form by default. You can limit the campaign frequency if you don’t want to send your audience a campaign every time they fill out your form. Toggle frequency to limit how often people move through your campaign. Once ever: people will only enter the campaign the first time they fill out your form. Once within a time period: people can only enter the campaign once within a time period, no matter how many times they fill out your form. Event Event-triggered campaigns help you respond to your audience’s behavior in your app or on your website—like encouraging people to complete their purchase when they abandon your cart, or messaging people who visit a product page on your website to let them know when the product goes on sale. When you use this kind of trigger, you can choose the event that you want to use to trigger your campaign. If you don’t see the event, you can type the event name in the box. If the event occurred recently, click View instances to see recent event examples. You can narrow your trigger criteria based on properties in your event. If you have a particular event attribute you want to use as a filter—like if you only want to target people who bought a specific product—click Add event data filter and set the event properties you want to match for your trigger.  You can use JSON dot notation in condition logic If you store attributes or event data in JSON objects or arrays, you can use JSON dot notation in your branch conditions to evaluate these properties. Use array[] to represent any item in an array or array[0] to represent the first item in the array. See Storing and using JSON for more information about dot notation in Customer.io.  You can only preview the last 50 events When you use an event filter, we’ll show you the last 50 occurrences of an event to help you preview people matching your event criteria. This limitation applies only to the preview as you set up your campaign. WHen you start your campaign, anybody who matches your criteria will still enter in your campaign if they match the trigger criteria, regardless of this preview limitation. Event triggers preview the latest 50 events When you use an event filter, we’ll show you the latest 50 occurrences of an event within the last 30 days to help you preview people matching your event criteria. As you add filters, you’ll notice that we’ll show you the total number of events (and people) out of the 50 available preview events that would match your criteria. This is to help you better understand who would trigger your campaign, but it is not an accurate count of people who will trigger your campaign! This 50-event limitation only applies to the preview as you set up your campaign. When you start your campaign, anybody who matches your criteria will still enter in your campaign. Frequency By default, a person enters an event-based campaign every time they perform the trigger event. You can limit the campaign frequency if you don’t want to send your audience a campaign every time they perform your event. Toggle frequency to limit how many times people can move through campaigns and set your frequency options. One time: people will only enter the campaign the first time they perform the event. For example, if you’d like to congratulate someone the first time they complete a lesson in your eLearning app, you can use this setting. This way, you’ll ensure they don’t receive the email again once they’re a seasoned student and flying through lessons regularly. On every event: people will enter the campaign every time they perform the event. Once within a time period: people will only receive the campaign once within a time period. For example, you want to send an email to someone when they receive notifications in your app. You only want them to receive the email once a day, regardless of how many application notifications they receive. Setting Frequency to “at most once within 24 hours” will ensure that, no matter how many new notifications people get, they will only get an email once. Important date This option lets you trigger a campaign based on a date-time attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that people in your audience have. The attribute must contain a date in either Unix timestamp or ISO 8601 format. If a person doesn’t have this attribute, or the attribute isn’t in the right format, they’ll never trigger the campaign. You can trigger your campaign using a relative or static date based on this attribute: Relative: Remind your audience to renew their subscription 14 days before their subscription anniversary. Static: Wish your audience a happy birthday on their birthdate. When you set up a date-triggered campaign, there are four things you need to choose. When you pick your date-time attribute, you’ll see a preview panel that lets you test your choices with people from your workspace. Determine how frequently people enter your campaign. These values are all based on your date attribute. Only once, on this date: A person triggers the campaign based on the entire date, including month, day, and year. This date must be in the future. Every month: A person can trigger the campaign once per month on the day of the month in their date attribute. The month and year are ignored. For example, if the date attribute is June 5th, 1977, the campaign will trigger every month on the 5th. If the current month doesn’t include that day (e.g. there is no September 31st) then Customer.io will send on the last day of the month. Every year: A person triggers the campaign once per year on the month and day in their date attribute. The year is ignored. For example, if the date attribute is June 5th, 1977, the campaign will trigger once per year on June 5th. Set whether to trigger your campaign on the exact date, or at an offset before or after. on: Trigger the campaign on the recurrence date before: Numbers of days prior to the recurrence date to trigger the campaign after: number of days after the reccurence date to trigger the campaign Pick your audience’s date-time attribute. Customer.io schedules your campaign based on the date value in this attribute, but we don’t use the time. You’ll pick the time in the next step.  Attributes updated on the same day as the campaign When you set the Person’s attribute value on the same day specified as the trigger, if it is set before the specified trigger time for the campaign, it will fire on that same day since the date in the attribute match the criteria and the trigger time has not passed yet. Let’s say today is July 1st, 2020 and you update a Person’s value for the trigger attribute to July 1st. If the time specifed in the trigger condition has already passed, the Person will not trigger this campaign today. For the campaign to be triggered, the attribute value must be set before the specified trigger time for the campaign. For example, if the campaign is supposed to trigger at 1pm on the date, then the campaign will not trigger if the attribute is updated at 2pm on the same date. Pick the time of day that you want to trigger this campaign. We won’t use the time stored in the date attribute. But, if you store your audience’s timezone as an attribute, you can select the user's time zone to trigger your campaign in your audience’s time zone. Supported date formats We can trigger campaigns based on a Unix timestamp or a date-time in the ISO 8601 format. These are the ISO 8601 date-time formats we support: ISO 8601 Format Example YYYY-MM 2024-03 (This will default to the first of the month.) YYYY-MM-DD 2023-11-15 YYYY-MM-DDThh:mm:ssZ 2023-11-15T23:14:37Z YYYYMMDDThh:mm:ssZ 20231115T231437Z YYYY-MM-DDThh:mm:ss.ms 2007-11-22T12:30:22.321 YYYY-MM-DDThh:mm:ss+/-< UTC offset > 2024-02-07T12:30:22-08:00 Nested date attributes When picking the date attribute that will trigger your campaign, you can also type in nested date-time attributes, such as appointments.follow_up_date or account_details.renewal_date. Unfortunately, we cannot preview the number of people that will receive a message when you use a nested attribute. Instead, we’ll show you how many people have the parent attribute (i.e. appointments or account_details). Webhook Most campaigns are triggered by people, and people are the subjects of the campaign—you set their attributes, send them messages, etc. Webhook-triggered campaigns are different: they’re triggered by data, and data (that may or may not be related to people in your audience) is the subject of the campaign. In general, you’ll use these campaigns to manipulate incoming data and associate it with people in your audience. For example, if people are interested in a product that’s out of stock, you might use a webhook to notify Customer.io when that item is back in stock. In your campaign, you can send an event to everybody who was interested in the product, triggering a campaign to let these people know that their product is back in stock! When you set up a campaign and select the Webhook option, you’ll get a webhook URL. You’ll provide this URL to the service that you want to collect data from. Your campaign runs whenever this URL is called. The data from your external service can take any shape. You’ll manipulate the data and associate it people as a part of your campaign. This lets you perform Zapier-like transformations on your data without having to build an integration. Right now, you can’t filter or otherwise limit webhook-triggered campaign triggers within Customer.io. You’ll have to make sure that your external service is set up to call your webhook-triggered campaign’s webhook URL with a relatively uniform data set and only when you want it to call the URL. Because webhook-triggered campaigns aren’t associated with people—at least not directly—they don’t have many of the options that you’ll see in other types of campaigns. They have a much more streamlined workflow: data comes in, you convert it to the format you want, and the campaign ends. You won’t set conversion criteria, a frequency, etc. Change trigger type Sometimes you’re building a campaign and realize you want people to start a journey based on different criteria. That might mean updating a trigger to include an additional segment or changing the trigger type all together—like moving from an event-triggered campaign to a segment-triggered one. Changing trigger type can impact the rest of your workflow, and you’ll see warnings if the change could cause messages to fail or conditions to not evaluate. For instance, if an email included liquid that pulled in data from your event trigger, like the product a person purchased, then you changed the trigger type, you’ll see a warning calling attention to the email you need to update. Or if your workflow updates a person’s attributes based on data in the event trigger, you’d see a warning calling attention to that action block. You’ll also see these warnings in the review modal so you know to address them before you activate your campaign. To change the trigger type, go to your campaign’s workflow: Select the trigger block. Click Change trigger type. Select the new trigger type. Fill in the trigger details. You can only modify the trigger type when the campaign is in a draft or stopped state. This ensures you have time to review and update any workflow items impacted by a change to your trigger. --- ## When to use filters URL: https://docs.customer.io/journeys/campaign-triggers-and-filters/ This article covers the difference between triggers and filters for all types of campaigns. How it works Both triggers and filters are sets of conditions that determine who enter your campaigns. A trigger determines who’s eligible to enter a campaign and when. A filter is additional criteria that a person or triggering data must meet for people to enter or remain in the campaign. For example, you might want to send a message when someone views the pricing page (event trigger: page view) of your website, but only send that message to people who aren’t on a paid plan (filter: free plan). You should think about triggers and filters separately to help differentiate the condition that triggers a campaign from the state of the user going through it (or the data they’re related to). In the case of legacy segment-triggered campaigns, using a segment filter with a segment trigger may not matter when there’s no delay at the beginning of your campaign. How we evaluate filters For most of our campaigns (see the exception for legacy segment-triggered campaigns below), we evaluate filters: at the start of a campaign If trigger conditions are met but not filter conditions, we recheck up to 30 minutes. during the campaign - specifically, before workflow actions if you include filters in your exit conditions: They stop matching the filters or They match the conversion criteria or they stop matching the filters flowchart LR a{Are trigger conditions met?}-->|yes|l{Are filter conditions met?} l-.->|no, person waits 30 min|g{Are filter conditions met after 30 min?} l-->|yes, person enters campaign|c g-->|yes, person enters campaign|c g-.->|no|i subgraph p [If exit conditions include filters, and a person reaches an action:] direction LR c{Are filter conditions met?}-->|yes|d[Send message or take action] end c-.->|no|k[Person exits campaign] a-.->|no|i[Person doesn't enter campaign] Legacy segment-triggered campaigns You know you’re working in a legacy segment-triggered campaign if you can add a filter in the trigger panel: Legacy segment trigger Latest segment trigger When people meet the trigger condition, they start a journey. However, they won’t move through your workflow until they meet filter conditions. We pause their journeys until they meet your filter conditions, or they exit after a period of time. If they continue forward, we will re-evaluate filter conditions only if the campaign uses one of these exit conditions: They stop matching the trigger segment or filters They match the conversion criteria or they stop matching the trigger segment or filters flowchart LR a{Are segment trigger conditions met?}-->|yes, person enters campaign|l{Are filter conditions met?} l-.->|no, person waits|g{Is there a delay at the beginning of the workflow?} l-->|yes, person moves to next campaign action|c g-->|yes, person completes the delay then moves to next campaign action|c g-.->|no, person enters a grace period|h{Are filters met after grace period?} h-->|yes, person moves to next campaign action|c h-.->|no|m[Person exits campaign] subgraph p [If exit conditions include filters, and a person reaches an action:] direction LR c{Are filter conditions met?}-->|yes|d[Send message or take action] end c-.->|no|k[Person exits campaign] a-.->|no|i[Person doesn't enter campaign] Looking for more info on grace periods? Learn about how they impact legacy segment-triggered campaigns.  You cannot change the Frequency on segment-triggered campaigns that include filters. The frequency is always “One time;” people only enter the first time they match the trigger criteria. Types of filters For all campaigns except webhooks you can add one or more segment filter conditions. A segment filter checks for people in or not in a segment. For example, you might want to send a message when someone views the pricing page (event trigger: page view) of your website, but only send that message to people who aren’t on a paid plan (filter: free plan). In legacy segment-triggered campaigns, you should weave filter criteria into the trigger conditions. But see below to understand when to include a separate filter. For object and relationship campaigns, you can also add object or relationship filters. An object filter refines your audience based on the attributes of the object they’re related to. A relationship filter refines your audience based on the attributes on people’s relationships to the object.  We check filters, not triggers, during an object campaign For object and relationship-triggered campaigns, we do not check trigger criteria during a campaign after the initial match. Therefore, if you want specific object or relationship attributes checked during people’s journeys, you must include them as filters and choose an exit condition that checks filters. FAQs Do people still get a journey for the campaign if they don’t meet the filter? For legacy segment-triggered campaigns, yes. For all other campaigns, they won’t if they don’t match the filter after a 30 min pause. Check out Journeys for more information. Legacy segment trigger vs segment filter You might be wondering, “When do I put my filter conditions in my segment trigger criteria?” In most cases, you should! But if you want people to trigger a campaign based on segment criteria AND filter them out after a certain time period, you’ll want to create both a segment trigger and a segment filter. Think of this as a way to ensure people move through this campaign only when it’s relevant to them and not accidentally later on. Consider the example below - people have just signed up for your service and should receive a special offer if they perform certain actions within a timeframe. Let’s say you want to send a campaign to people who have viewed your pricing page at least once within 10 days of signing up. Your goal is to convince them to upgrade with a special “newbie” offer. Here’s how to set it up: Trigger a campaign based on people joining the “Signed up” segment. Filter your campaign based on “has viewed pricing page at least once”. Set your exit condition to include filters. Add a 10-day delay to the beginning of your workflow, followed by your message with a special offer. This means: as soon as someone signs up, they enter the campaign and the 10-day clock starts ticking. After 10 days, the campaign will send messages if they meet the filter criteria (viewed the pricing page at least once). Otherwise, people would not get this offer and would exit the campaign. Whether they move through the whole campaign or not, these people will never be eligible for this campaign again because people can only enter a segment-triggered campaign with a filter the first time they match the criteria. If you had combined both conditions into the trigger, the 10-day campaign clock would not start ticking until people had both signed up AND viewed the pricing page once. This combination of conditions could happen at any time, no matter when they signed up, so your customer could receive this newbie offer a year later. --- ## Goals & conversion criteria URL: https://docs.customer.io/journeys/campaign-conversions/ A **goal** is what you want your audience to accomplish during your campaign, like purchase a product, sign up, or subscribe. When you set up a goal, you define **conversion criteria**, which are the rules that determine when we mark a message or person as converted. How goals work A goal is what you want your audience to accomplish during your campaign, like purchase a product, sign up, or subscribe. When you set up a goal, you define conversion criteria, which are the rules that determine when we mark a message or journey as “converted.” The conversion criteria can be one of (and only one of) the following things: an eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. you want people to perform a segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. you want people to join a segment you want people to leave When you set conversion criteria, you also set a conversion window (up to 90 days) that your customers can match the conversion criteria after they are sent, open, or click a tracked link in a message. If a person matches your conversion criteria within the time frame, we’ll record a conversion and attribute it to your campaign. If a person does not match the conversion criteria, or they match it outside the time frame you set, we won’t record a conversion.  Multiple campaigns can convert on the same criteria We count conversions independently for each campaign. If you use the same conversion criteria across multiple campaigns, a person who goes through both campaigns and reaches the goal will achieve a conversion for each campaign. Conversion metrics You can check the converted metric to determine the percentage and number of journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. and delivered messagesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. that achieved your goal—and the number that haven’t. Use your conversion metrics to figure out how well your messages perform and develop strategies to increase the value of your messages over time. Example of converted metrics on the Overview tab of a running campaign flowchart LR a[Person receives message]-->b{Do they match conversion criteria?} b-->|yes|c{Are they within the time frame?} c-->|yes|d[message/journey is converted] c-.->|no|e[message/journey is not converted] b-.->|no|f[message/journey is not converted]  We do not retroactively count conversions If you edit or change your conversion criteria after you start a campaign, we won’t retroactively apply your conversion criteria. Updating conversion criteria will not update your conversion metrics for messages that were previously marked as converted or not converted; your updated conversion criteria and metrics will only apply to new conversions. Types of messages or actions that can record conversions We track conversions for the following message/delivery types: Conversions attributed Conversions not attributed Email Slack Message SMS Create or update person action Customer.io Push Notifications Customer.io In-app Messages Webhooks1 1You must enable webhook conversions manually. Slack and Create or update person actions are often internal or used for analytics purposes; they don’t always send messages to end-users. For that reason, we don’t attach conversions to them. You can enable webhook conversions on individual webhook actions. Set up conversion criteria You can set a goal when you create a campaign or add it to campaigns you’ve already created. However, if you set conversion criteria for a live campaign, it will only apply to people who enter the campaign after you set the goal. You cannot apply a goal retroactively to people who’ve already entered or completed the campaign. Click the upper-left menu while editing a campaign. Then click Manage under Goal: Click Set goal. Select the type of conversion criteria—performs event, enters segment, or leaves segment. (Optional) If your conversion criteria is an event, click Add event data filter to ensure that your event contains (or doesn’t contain) specific properties or values before counting a conversion. Under Conversion, set the maximum time (up to 90 days) after a delivery that you’ll count a conversion. Select whether to count a conversion after a person: Is sent any deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. (i.e. not attempted or failed) from this campaign Opens any delivery from this campaign Message opens may not be reliable for certain message types, like SMS or email, with some clients. You might want to use the receiving or clicking a tracked link in options to ensure more accurate reporting with these message types. Clicks a tracked link in any delivery from this campaign If a link isn’t tracked, Customer.io can’t know whether or not a person clicks it, and therefore cannot record a conversion. See our page on link tracking for more information. Save your changes. Next, decide whether you want people to exit your campaign when they match the conversion criteria. Event-based goals Events let you track the things that your audience does. You can use an event as a campaign goal. For example, if your campaign is focused on abandoned carts, your goal event might be a purchase event. You can only use a single event as the goal for any given campaign.  Event names in conversion criteria are case sensitive While event names are generally not case sensitive in Customer.io, event names used in conversion criteria are case sensitive. This means purchase and Purchase are treated as distinct events when you set up conversion criteria. Make sure the event name in your conversion criteria exactly matches the event name you send to Customer.io. Event-based filters You can also set up a filter to limit a conversion based on an event’s properties. This lets you make sure that your conversion event contains (or does not contain) specific properties before you count it as a conversion. Using our purchase example above, imagine that you want to record a conversion when your audience uses a specific coupon code or makes a purchase over a certain dollar amount. You could set a filter for that! Click Add event data filter under the goal to begin. You can join multiple filter conditions (for a single event) depending on whether you select all or at least one in the dropdown: All: all conditions need to be true to match the conversion criteria. Click Add event data filter to create another statement in an AND conditional, if you need. At least one: only one condition must be true to match the conversion criteria. Click Add event data filter to create another statement in an OR conditional, if you need.  Event-based conversions are based on processing time, not timestamps. We mark a delivery associated with an event-based goal as converted when we receive the event. You cannot backdate an event (with a timestamp) so that it triggers a conversion. For example, if you send an event outside the conversion time window, but it has a timestamp that would’ve fallen within the window, we won’t count that conversion. Why filter by event data You can compare properties from the conversion event to a person’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or even to properties from the incoming trigger event. This helps you make sure conversions are attributed to the exact data relevant to your campaign and each person’s journey. For example, imagine you run online courses, and you want to start a campaign when a person begins one of your classes. Your objective is to help a person finish the course they started, so you want to record a conversion when the person finishes it. If your events use a course_id to differentiate between similar events for different classes, you can determine when someone has finished the course they started and mark a conversion! In this example is an event-triggered campaign with a frequency of “Every re-match.” This means a person can enter this campaign for each course they start. It’s helpful then to set the course_id of the event associated with the goal to the course_id of the event that triggered the campaign because it ensures the conversion is consistent with each journey. Segment-based goals  If you capture events, try an event-based goal. Segments let you track conversions based on people’s attributes, which is useful if you don’t have an integration that sends events. You can add people to segments based on events, too, but if you already capture events, you may find it easier to simply set up an event-based conversion criteria. You can set a goal based on whether someone “enters a segment” or “leaves a segment.” If you choose “enters segment,” we check whether a person is IN the segment. If you choose “leaves segment,” we check whether a person is NOT IN the segment. Consider that a person can be IN or NOT IN a segment before they enter your campaign: If a person already matches your conversion-criteria segment when they enter your campaign, and progress through the campaign, then messages that you send to that person won’t record a conversion. If the campaign has the exit criteria, “They match the conversion criteria,” people will enter the campaign then exit before any action because they already meet the conversion criteria. We calculate if someone is NOT IN a segment when they’ve never belonged to the segment as well as when they leave a segment; this means “exit segment” corresponds to people who have never belonged to the segment and who left the segment they belonged to.  Add your conversion segment as a filter to your campaign trigger. If you use a segment as your conversion criteria, you might want to add your segment as a filter for your campaign so only people who have not performed the goal enter the campaign. For instance, if your goal is “leaves segment: Has not recently logged in” then you would add a segment filter of “IN: Has not recently logged in.” This way, only logged in users enter the campaign. Conversion timing A person can match the conversion criteria after being sent, opening, or clicking a tracked link in a delivery. Conversions based on a person performing an event are counted when we receive the event, not when the event occurred.  Holdout tests can show conversions You can use holdout tests to test the impact of your messages. When you use a holdout test, we show conversions on the holdout group when people who don’t receive the message perform the expected action. This helps you measure the true impact of your messages by comparing conversion rates between people who received messages and people who didn’t. flowchart LR z[1st message]-->b y[2nd message]-->b b{does person match conversion criteria?} b-->|yes|c{are they within the time window?} c-->|yes|d{which message did person open last?} c-.->|no|e[message/journey is not converted] b-.->|no|f[message/journey is not converted] d-.->g[If person opened 1st message last, the 1st message converts.] d-.->h[If person opened 2nd message last, the 2nd message converts.] If you allow conversions after being sent messages, conversions will occur in the order that you send messages. For example, if a person receives two messages, and matches your conversion criteria within your time frame of both messages, we’ll attribute the conversion to the second message—the most recently received message. If you set conversions to occur after opening or clicking a tracked link in messages, people will achieve your goal against the most recent message that they opened or clicked. If a person meets conversion criteria, like performing an event, immediately after an open or click, it’s possible a conversion isn’t logged: For clicks, this can happen because it takes up to 10 seconds for us to screen out machine clicks. For opens or clicks, this can happen if we process the open or click AFTER the conversion criteria is met. For example, let’s say your conversion criteria is “performs event: subscription_activated” and the conversion window is based on clicking a tracked link: At 8:07:42 AM, a customer clicks a tracked link in a message. At 8:07:49 AM, the customer performs the conversion criteria event subscription_activated. At 8:07:53 AM, we finish processing out machine clicks, and the click clears the queue. Customer.io won’t log a conversion because the click was processed after the conversion event was performed. Exit condition: match the conversion criteria Exit criteria determine if a person should leave your campaign early. Some conditions allow people to exit early when they match the conversion criteria. By default, a person continues through a campaign until they stop matching the campaign filter criteria—even if they perform your campaign’s goal action. (For a legacy segment-triggered campaign, people will exit when they stop matching the trigger or filter conditions by default.) However, you might not want a person to continue a campaign if they match the conversion criteria of the campaign. For example, if your goal is for someone to complete a purchase, you probably don’t want to continue reminding people about the items in their cart after they make a purchase! To view or modify your exit criteria, click the upper-left menu while editing a campaign. Then click Manage under Exit. You can specify that people should exit: only if they match the conversion criteria if they match the conversion criteria or they stop matching the filters if they match the conversion criteria or they stop matching the trigger (attribute or segment-triggered campaigns only) Learn more about exit conditions and how we evaluate them during an active journey. --- ## Exit conditions URL: https://docs.customer.io/journeys/campaign-exit-conditions/ Exit conditions determine if or when a person should exit your campaign. The options available depend on the type of campaign you're building—attribute or segment, event, etc. By default, a person continues through a campaign until they stop matching the campaign filter criteria. (For an attribute or segment-triggered campaign, people will exit when they stop matching the trigger conditions by default.) To view or modify your exit criteria, click the upper-left menu while editing a campaign. Then click Manage under Exit. These are your options for exit conditions: They match the conversion criteria: This setting causes people to exit your campaign after they perform the conversion action (your audience performs an event, enters a segment, or leaves a segment). They stop matching the filters: When people stop matching filter criteria, they exit immediately. Campaigns triggered by an attribute or segment don’t have filters. Instead, you’ll see They stop matching the trigger because they don’t have filters. They match the conversion criteria or they stop matching the filters: People exit the campaign immediately after they match your conversion criteria OR after they stop matching the campaign’s filter criteria. Campaigns triggered by an attribute or segment don’t have filters. Instead, you’ll see They match the conversion criteria OR they stop matching the trigger because they don’t have filters. People don’t exit early, they move through the entire workflow: People won’t exit your campaign early. They’ll complete their entire journey, even after they meet your goal or stop matching your campaign’s trigger or filter conditions. --- ## Schedule a campaign URL: https://docs.customer.io/journeys/schedule-campaigns/ You can schedule your campaign to start or stop at a specific date and time. Scheduling gives your team the flexibility to plan and execute time-bound marketing initiatives with fewer manual steps. For instance, you might have a seasonal campaign or limited-time offer that you want to make sure is only available during the relevant time period. How it works When you schedule a campaign, Customer.io automatically manages it based on your specified times: Start time: When the campaign starts to trigger and process journeys Stop time: When the campaign stops accepting new people Scheduled campaigns display a “Scheduled” label next to their status indicator. The “Scheduled” label shows that a start and/or stop time is set, while the status (like “Running”, “Draft”, or “Stopped”) shows the campaign’s current state. The “Scheduled” label displays as long as the campaign has a start or stop time scheduled in the future. For instance, a campaign with the status “Draft” and the label “Scheduled” means the campaign has not started but has a scheduled start or stop time that has not passed. You can hover over the label to see any scheduled times. Add a schedule After you finalize your workflow, click Start Campaign to review your campaign and schedule it.  The schedule reflects the time zone of your operating system It follows the time zone of the team member who is logged in and made the schedule, not the people or data triggering the campaign. In the review modal, click Schedule. (Optional) Specify a start time. For campaigns triggered by an attribute, segment, object, or relationship, you’ll also specify whether current and future matches or only future matches should trigger the campaign. (Optional) Specify a stop time. Decide what should happen to active journeys when the stop time is reached: do they exit immediately or finish their journey? Learn more about how these options impact your messages and users. Click Schedule Campaign. Learn more about rescheduling campaigns and impacts on live campaigns below. Update a draft campaign’s schedule Follow the steps above to add a schedule. You can modify the start time until the campaign activates. Return to your workflow and edit the time in the review modal. Once the campaign starts, the campaign switches from “Draft” to “Running” but will continue to show as “Scheduled” to indicate there’s a start or stop time associated. Schedule a running campaign When a campaign is in a running state, it may have active journeys—people may actively be moving through your campaign workflow. If you want to add or change the stop time, you must also decide what happens to active journeys when the stop time is reached: do they exit immediately or finish their journey? Go to the Campaigns list page and click inline with your campaign name. Choose Schedule stop. Specify a stop time.  The schedule reflects the time zone of your operating system It follows the time zone of the team member who is logged in and made the schedule, not the people or data triggering the campaign. Decide what should happen to active journeys when the stop time is reached: do they exit immediately or finish their journey? Learn more about how these options impact your messages and users. Click Save Schedule. Reschedule a stopped campaign Consider whether you want to restart this existing campaign or duplicate it to start a new one. Do you want past journeys as part of your data in this campaign? If so, rescheduling makes sense.  The schedule reflects the time zone of your operating system It follows the time zone of the team member who is logged in and made the schedule, not the people or data triggering the campaign. Go to the Campaigns list page and click inline with your campaign name. Choose Schedule. (Optional) Specify a start time. For campaigns triggered by an attribute, segment, object, or relationship, you’ll also specify whether current and future matches or only future matches should trigger the campaign. (Optional) Specify a stop time. Decide what should happen to active journeys when the stop time is reached: do they exit immediately or finish their journey? Learn more about how these options impact your messages and users. Click Save Schedule. Reschedule an archived campaign Instead of rescheduling an archived campaign, consider duplicating the campaign and starting fresh. Otherwise, if you want to preserve journeys from the previous run of the campaign, you must first unarchive it then follow the steps above for stopped campaigns. Delete a schedule If the campaign is in a draft state, you can delete the start or stop time the same way you added it—through the review modal in the workflow. If the campaign is running, you can remove the stop time: Go to the Campaigns list page and click your campaign name. Click Actions and choose Schedule stop. Click Remove next to the stop time. Click Save Schedule. --- ## Change the state of a campaign URL: https://docs.customer.io/journeys/campaign-statuses/ Campaign states determine whether your customers can enter your campaigns. Campaigns can have one of four statuses: running - active draft - inactive; all campaigns start in a draft state. stopped - inactive archived - inactive Running is the only active status. This means customers can enter your campaign and move through your workflow, which could include messages, update actions, and more. You can edit these campaigns while they’re live, but make sure you understand how live edits impact your customers. Draft, stopped, and archived are inactive statuses. Customers cannot enter your campaign. You can make changes to draft and stopped campaigns, but not archived ones. You can unarchive a campaign to change the state to stopped and continue editing. You can restart any campaign in a stopped state. Scheduled campaigns Scheduled campaignsdisplay a scheduled label alongside the campaign status. The scheduled label shows that a start and/or stop time is set, while the status (like running, draft, or stopped) shows the campaign’s current state. For instance, a campaign with the status running and the label scheduled means the campaign has started and people may be moving through your workflow. Stop a campaign To stop a campaign, click the campaign and select Stop now or Schedule stop from the Actions dropdown. You can also access this from inline with the campaign name on the list page. Stopping a campaign prevents people from entering the campaign. While the campaign’s stopped, you can edit it. And messages that were already sent from the campaign will continue to generate metrics normally. What happens if people are in the campaign? If the campaign has active journeys in progress, you can either: Force people to exit immediately. In this case, we stop sending messages to anybody who has an active journey and recall in-app messages that haven’t been seen (opened) yet. See What happens to messages when people exit immediately? for more information. Allow people to finish their journey and exit the campaign naturally. Your campaign will sit in a Stopping state until all people have exited the campaign, and then it’ll switch to Stopped. What happens to messages when people exit immediately? When you stop a campaign using the Exit immediately option, we’ll stop sending messages immediately. But all emails, SMS messages, push notifications, webhooks, and Slack messages that are already Sent are likely to be delivered to your audience. We don’t control the delivery providers for these message types, so we can’t recall these messages after they’re sent. In-app messages native to Customer.io are an exception to this rule: any in-app message that has been “Sent” but not “Opened” is recalled. We control the delivery mechanism for these messages, so we can stop them before they make it to your audience. flowchart LR a(Stop campaign)-->b{Set to exit immediately?} b-.->|no|c(People with an active journey keep receiving messages) b-->|yes|d{Will sent messages be delivered?} d-->|email, push, SMS, Slack, and webhooks|e(Normal delivery) d-.->|In-app messages|f(Cancelled) Restart a campaign To restart a campaign, click the campaign and select Restart or Schedule from the Actions dropdown. You can also access this from inline with the campaign name on the list page. When restarting a campaign triggered by an attribute, segment, object, or relationship, you must also decide who will enter the campaign: Current people and future additions: choose this option if everybody who currently matches your trigger conditions should enter your campaign. This includes anyone currently in any trigger segments but will also take your campaign frequency settings into account. Future additions only: choose this option if only people who match your campaign trigger conditions after the campaign is restarted should enter. Archive a campaign You can archive a campaign to preserve historical data and copy the structure or content of the campaign for future use. Archiving moves campaigns from your list of Active campaigns to Archived on the campaigns page. To archive a campaign, click the campaign and select Archive from the Actions dropdown. You can archive stopped and running campaigns, not draft. After you archive a campaign: People can no longer enter the campaign. We end active journeys. We delete unsent drafts. We pause A/B tests. You can no longer edit the campaign’s workflow, trigger, goal, and exit criteria. You can still search for and copy workflow items from an archived campaign as well as start new emails from those in archived campaigns (under “Start from existing email”). To remove archived content and prevent archived workflows from being reused in the future, delete the campaign instead. Unarchive a campaign To unarchive a campaign, click the campaign and select Unarchive from the Actions dropdown. After you unarchive a campaign: The campaign moves back to the Active tab, and its status updates to stopped. Any archived segment used in the campaign’s trigger, filters, goal, or exit criteria becomes active. (You can filter for active vs archived segments on the segments page.) Duplicate a campaign Duplicate a campaign to create your next campaign faster or to run campaigns in parallel for experimentation. You can duplicate entire campaigns to preserve settings like trigger conditions, goals, and exit criteria in addition to their workflows. However, if you duplicate a webhook-triggered campaign, we will generate a new webhook URL for the copied campaign.  You can only duplicate campaigns within a workspace While you can’t duplicate a campaign across workspaces, you can copy your workflow to another workspace. You can duplicate a campaign that is in a state of running, draft or stopped. To duplicate an archived campaign, you must first unarchive it. No matter the state of the campaign you’re copying, you must activate the duplicate campaign before anyone can enter it. You can duplicate a campaign from the campaigns page or an individual campaign’s overview. From the campaigns page: Click campaign settings inline with the campaign you want to copy. Click Duplicate. From an individual campaign’s overview, click the Actions dropdown and select Duplicate. The duplicate campaign uses the original campaign’s name prefixed with [COPY] and appears at the bottom of the campaign list in a draft state. You need to activate it before people can enter the duplicated campaign. Duplicate legacy segment-triggered campaigns In April 2025, we introduced the attribute or segment trigger as a more flexible version of our legacy segment trigger. If you duplicate a campaign that uses the legacy segment trigger, the copy continues to use the legacy trigger, which is processed differently than the latest trigger type. Learn more about the differences to determine if you want to keep the old behavior or switch to the new one. You can change a legacy segment trigger to the latest trigger by selecting the trigger on the canvas, clicking Change trigger type, and selecting Attribute or segment. If you keep the legacy segment trigger, here are the options for editing it. Segments are based on audience criteria—attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. your audience has, events they’ve performed, messages they’ve received, etc. You can choose one or more segments that your audience is in or not in. If you haven’t created a segment yet, you can click Create a new data-driven segment to set conditions for a new segment.  You cannot create campaigns with only a not in condition A segment-triggered campaign must have at least one in condition for you to save it. We do not trigger campaigns solely off of people not belonging to a segment. You can set multiple segments using and or or conditions. Use and to trigger campaigns only when people meet all of your conditions. Click + Add segment condition to create an and condition. Use an or condition to trigger a campaign based on a person belonging to any of the segments. Add segments to the same field to create an or condition: Filter With segment-triggered campaigns, you can also add a segment filter. You should weave your segment filter criteria into your segment trigger conditions as much as possible, but if you find you cannot accomplish what you need without a segment trigger AND segment filter, please let us know! We want to account for this use case as we develop the next generation of campaigns. Delete a campaign To delete a campaign, click the campaign and select Delete from the Actions dropdown menu.  Deleting a campaign is a permanent action When you delete a campaign, we delete all message content, settings, and metrics. Consider archiving instead of deleting if you want to save any part of the campaign. --- ## Why aren't people entering my campaign? URL: https://docs.customer.io/journeys/troubleshoot-campaign-entrance/ Start here to figure out why people are entering your campaign earlier or later than you'd expect. Note that people who are unsubscribed from topics or from all messages can still trigger a campaign; they just won’t receive messages from that campaign. See issues with messages for more information. 1. Check if the campaign is live If no one is entering your campaign, check that it’s live first. You’ll see “Running” next to the campaign title. If it’s still in a “Starting” state, people may not be able to trigger the campaign yet. If you haven’t started the campaign yet, and it’s triggered by a segment, profile attribute, object, or relationship, make sure you choose the right audience when going live: Current + Future additions or Future only. 2. Check your entrance criteria Each campaign has one type of trigger: segment, event, object, form, etc. Each trigger comes with its own set of conditions like filters, frequency and audience settings. The first step is to check that these conditions match the people you’re troubleshooting. You’ll want to have both the campaign and a person’s profile open to troubleshoot. Go to your campaign, click Workflow, and then select the trigger block to view the conditions. In a separate tab, open the profile of a person that didn’t enter as you’d expect. If possible, locate a person who started matching the criteria within the past 30 days. Our activity logs only look back 30 days so this will help you figure out when people started/stopped matching criteria. Attribute or segment triggers For campaigns triggered by attributes and/or segments, follow these steps: Check whether the person matches the Trigger Conditions. Do their attributes match? Does their segment memberships reflect the conditions you set? If no, consider updating your conditions. Do they also match the Filters? Did they match the segment filters within 30 minutes of matching the trigger conditions? If no, they won’t move through the campaign. Consider modifying or removing the filters.  This only applies to legacy segment-triggered campaigns If you see Filters under the trigger conditions, then you’re using the legacy trigger. Check your Frequency setting. If it’s set to One time, the person can only enter this campaign once. Check their journey history to see if they’ve already entered the campaign. If it’s set to Every re-match, the person can enter more than once. They must stop matching your trigger and filter conditions then re-match to enter your campaign again. If they haven’t stopped matching, they won’t re-enter. If they’ve stopped matching and re-matched, they’ll enter again. If it’s set to At fixed intervals, people only enter the campaign if they meet trigger and filter conditions at the next interval. If they don’t meet the conditions, they won’t re-enter. Check out some examples on how frequency settings work. If this doesn’t help you locate the problem, check the start state of the campaign. If you’ve edited a campaign’s entrance criteria while it’s live, learn more about how this impacts journeys. Event triggers For campaigns triggered by events, follow these steps: Check whether the person matches the Trigger Conditions. Did the person perform the event you set? Was the trigger event processed after the campaign started? People only enter event-triggered campaigns when they perform the event after the campaign is live. You can find the start time of a campaign under Overview > Details. Timestamps for events are listed on the person’s activity log. Does the event match the event data filters? If no, consider adjusting the filters. Do they match the Filters? Did they match the segment filters within 30 minutes of the trigger event? If no, they won’t move through the campaign. Consider adjusting the filters. Check your Frequency setting. If it’s set to One time, the person can only enter this campaign once. Check their journey history to see if they’ve already entered the campaign. If it’s set to On every event, the person can enter more than once. People will enter the campaign every time they perform the event. If it’s set to Once within a time period, the person can enter more than once. They’ll only re-enter after the time period has passed and no more than once within that time period. If you’ve edited a campaign’s entrance criteria while it’s live, learn more about how this impacts journeys. Object updated triggers For campaigns triggered by objects like “Course updated”, follow these steps: Check that the person has a relationship to an object that triggers the campaign. If they don’t, they won’t enter the campaign. If they do, does the object match the Trigger Conditions? Check the Audience setting. If it’s set to “Certain people in the object”, check that the audience conditions match the person or relationship. Check the Filters. Check that the filter conditions match the person, relationship, and/or object. An object update can only fan out to 1,000 people. If a trigger fans out to more than 1,000 people, no one starts a journey. If you think this might be the case, learn how to work within the fan-out limit. If this doesn’t help you locate the problem, check the start state of the campaign. If you’ve edited a campaign’s entrance criteria while it’s live, learn more about how this impacts journeys. Relationship added or changed triggers For campaigns triggered by relationships like “Person added to Course” or “Relationship changed with Course”, follow these steps: Check that the person has a relationship to an object that should have triggered the campaign. If not, they won’t enter the campaign. If yes, are there relationship conditions? Check that the Trigger Conditions match the relationship between the person and object. Check the Audience setting. If it’s set to “Certain people in the course”, check that the audience conditions match the person or relationship.  A relationship update can only fan out to 1,000 people. If a trigger fans out to more than 1,000 people, no one starts a journey. This limit is per trigger, not per campaign. Learn more. Check the Filters. Check that the filter conditions match the person, relationship, and/or object. A single relationship update can only fan out to 1,000 people. If a trigger fans out to more than 1,000 people, no one starts a journey. If you think this might be the case, learn how to work within the fan-out limit. If this doesn’t help you locate the problem, check the start state of the campaign. If you’ve edited a campaign’s entrance criteria while it’s live, learn more about how this impacts journeys. Form triggers For campaigns triggered by form submissions, follow these steps: Check your Trigger Conditions. Did you select the right form names? Check the event data filters. Are the criteria too narrow, preventing people from entering the campaign? Check the Filters. Is the person you expected to enter in or not in the segments specified? Are filter conditions updated by the form submission itself? If the campaign filter includes segments with attributes updated via the form submission, you could run into a race condition. This could happen because we’re evaluating the person immediately after they submit the form that triggers the campaign. To work around this, add a short time delay after the trigger to allow us time to update and evaluate the profile. Remember that after someone submits a form that triggers a campaign, we will check that people match filter conditions for up to 30 minutes. Check the Frequency setting. If it’s set to One time, the person can only enter this campaign once. Check their journey history to see if they’ve already entered the campaign. If it’s set to On every form submission, the person can enter more than once. People will enter the campaign every time they fill out your form. If it’s set to Once within a time period, the person can enter more than once. They’ll only re-enter after the time period has passed and no more than once within that time period. Otherwise, there might a problem with the form integration itself. Go to Integrations, search for your form name, and check that your form is connected and sending the information you’d expect. Date triggers For campaigns triggered by dates, follow these steps: Check the Trigger Conditions. Did you select the right date attribute? Check the attribute dropdown to see if there are similar attributes. Does the person you’re troubleshooting have the date attribute? Is the value of the date attribute in the right format? It must be a Unix timestamp or a date-time in the ISO 8601 format. If not, they won’t trigger the campaign! Learn more about supported date formats. Check the date settings. If you selected a relative date before or after the date attribute, did you specify the correct timeframe? Are you sending in the user’s timezone or a set timezone? If you’re sending in the user’s timezone, we check the language attribute on the person’s profile to determine the correct time to send. Did this person have the language attribute? If not, they trigger the campaign in the fallback timezone. Check the Filters. Is the person you expected to enter in or not in the segments specified? Check the Frequency setting. If it’s set to Once, the person can only enter this campaign one time. Check their journey history to see if they’ve already entered the campaign. If it’s set to Monthly, they will re-enter every month based on the start date in the trigger. If it’s set to Yearly, they will re-enter every year based on the start date in the trigger. Learn more about date attributes and frequency settings in Campaign triggers. If you’ve edited a campaign’s entrance criteria while it’s live, learn more about how this impacts journeys. 3. Check the start state of your campaign When you start a campaign triggered by an attribute, segment, object, or relationship, you choose whether people who already match your conditions should trigger the campaign immediately. If a person entered the campaign earlier than you’d expect, it may be because you selected “Current + Future additions only” when starting the campaign. If you chose this, people who matched your entrance criteria before the campaign started will enter. If a person didn’t enter and you thought they would, you may have selected “Future additions only.” If you chose this, only people who matched your entrance criteria after the campaign started will enter. If you’re not sure which option you selected when starting the campaign, you’ll have to reach out to support. We don’t currently surface this in activity logs or the campaign UI. You can stop and restart the campaign to choose “Current people + future additions” if you want to make sure people who already match enter. However, keep in mind that if your frequency setting allows for entrance more than once, a person could re-enter the campaign and receive the same set of messages again. To compare a person’s activity to a campaign’s start time, locate your campaign’s start time under Overview > Details. Compare this to the timestamp of the person’s activity log on their profile. --- ## Why aren't people receiving my message? URL: https://docs.customer.io/journeys/messages-not-sending/ Start here to figure out why people aren't receiving messages at the right time. Note that people who are unsubscribed from topics or from all messages can still trigger a campaign; they just won’t receive messages from that campaign. Check your message’s settings Select the message block to get started. Check your message’s sending behavior Select Settings to check your message’s sending behavior. By default, messages are set to queue as drafts in your campaigns. Update them to send automatically then send any drafts manually to ensure all of your campaign’s audience receives the messages. Check the subscription preference and message limit Select Settings to check your message’s subscription preferences and message limit settings. By default, they inherit your campaign’s settings, but it’s possible to override them. Is the person subscribed to receive this message? Has the person reached the message limit? If so, they won’t receive this message. This message would be marked as “Undeliverable”. If the message inherits the campaign-level settings, click the campaign title and view Messages to see which subscription preference was assigned and what message limit is in place. Check whether the message is a holdout test Select Experiment. If “Make this message a holdout test” is selected, then people won’t receive the message. Check message conditions You can add action conditions to a message to limit who receives it. If a person doesn’t meet the action conditions, they won’t receive the message and will move onto the next item in your workflow. The person’s journey for the campaign will show they skipped the message. Check if the message failed to send If the message has a status of “Failed”, then you might have a liquid error. Check the message content to see if any errors are present. Check if the email address is suppressed A suppressed email won’t receive emails from Customer.io. If you manage email deliveries through Customer.io, check your suppression list in > Workspace Settings > Email > Suppression List. --- ## Edit triggers, filters, or frequencies URL: https://docs.customer.io/journeys/campaign-changes/ Learn how changes to triggers, filters, or frequency settings in live campaigns would impact your customers. Both trigger and filter conditions must be met for people to start moving through your campaign’s workflow. Some campaigns types — event, form, important date, legacy segment, and attribute or segment — have a frequency setting which controls how many times the same person can enter a campaign. By default, it’s once.  If you edit a trigger or filter and expect people to re-enter a campaign, make sure you check your frequency setting. Force people to re-enter a campaign after making changes If your campaign is triggered by segment membership, attribute changes, objects, or relationships, changes to campaign triggers and filters aren’t retroactive: only people who match your new conditions after you make changes will enter your campaign. If you want to re-evaluate everybody in your workspace against your new conditions, you can stop and restart your campaign.  Be careful when changing live campaigns Changing live campaigns can cause unexpected behaviors. You should always test your changes carefully and consider the frequency settings before forcing a match. Stop the campaign. Edit your triggers and filters—this might mean editing a segment. Restart the campaign. Choose Current People and future additions to include people who match your updated conditions. A person will only ever receive one copy of a message each time they flow through a campaign. But forcing people to re-enter a campaign they have already completed means they might receive messages they already received in the past! If that’s not desired, consider choosing Future only instead. Restart with Current and future matches Choose Current and future matches if you want people who already match your conditions to enter the campaign. (Re-entry is subject to your campaign’s frequency settings.) This option is available for campaigns triggered by attributes, segments, objects, and relationships. For campaigns triggered by attributes or segments, people can only have one journey in the same campaign at a time; they can’t have concurrent journeys. So after a person exits your campaign, they may be eligible to re-enter based on the Frequency settings: One time: People who have already been through the campaign will not re-enter the campaign. At fixed intervals: People will re-enter the campaign at the next interval if they meet your trigger conditions. Every re-match: If you choose Current + Future additions, people who already match your trigger conditions will re-enter. If you choose Future only, people will only re-enter after they stop matching then re-match your trigger conditions. For campaigns triggered by objects or relationships, people can have one or more concurrent journeys in a campaign triggered by an object or relationship. The frequency setting is always “every re-match” so if you choose Current + Future additions, people who already match your trigger conditions will re-enter. If you choose Future only, people will only re-enter after they stop matching then re-match your trigger conditions. Event-triggered campaigns Change the trigger event If you change the trigger event, people currently in your workflow will continue their journey. Moving forward, the campaign will only trigger based on the new trigger event. Change the trigger’s event data filter(s) If you change the trigger’s event data filter, people currently in your workflow will continue their journey. Moving forward, the campaign will trigger when the trigger event is performed and matches the new trigger event data filter. Change the segment filter(s) If you change which segment is the filter or the conditions of the segment filter, we re-evaluate people currently in your campaign based on its exit conditions. So if you change the segment filter from “In Signed Up” to “In Paying Customers” and the exit condition is “They stop matching the filters,” we’ll re-evaluate the segment filter before people’s next actions (message, update action, delay, etc). People who don’t match the campaign’s new segment filter will exit the campaign. People who do match your campaign’s filter will continue the campaign. Moving forward, after an event triggers the campaign, people will only begin your workflow if they match the new segment filter conditions. Campaigns triggered by segments or attributes In May 2025, we introduced the “Attribute or segment” trigger, which allows you to trigger campaigns based on profile attributes and/or segments. Unlike the legacy “Segment change” trigger, this newer trigger doesn’t support filters. If you created your campaign after April 2025, you use the “Attribute or segment” trigger and can’t separately add filters. If you created your campaign (or duplicated one) created before then, the campaign uses the “Segment change” trigger, and you can still add filters. Without filters, there’s one less check that could influence how people enter or re-enter your campaign. Otherwise, we evaluate frequency in the same way when editing these live campaigns. To learn more about the differences between these triggers, check out Triggers with segments. Legacy segment trigger: change the trigger or filter segment(s) Only future matches will trigger the campaign. Only people who match the new trigger conditions AFTER you save your changes will trigger the campaign. People must also match any new segment filter conditions to start your workflow.  You can stop your campaign if you want to trigger for current matches You can stop the campaign, change the trigger, and restart your campaign. When you restart, you’ll have the option to trigger your campaign for people who already match your new conditions. Learn more about force matching with stop and restart. If you change the segment (assign a different segment or change the underlying conditions of the segment), the campaign only re-evaluates people currently in your campaign if the trigger or filter is part of your exit conditions. If the exit condition is “They match the conversion criteria,” and you change which segment is assigned to the trigger or filter, then people who don’t match your trigger or filter could still complete the campaign. Attribute or segment trigger: Change the trigger attributes or segments Only future matches will trigger the campaign. Only people who match the new trigger conditions AFTER your save your changes will trigger the campaign.  Do you want people who currently match your criteria to enter the campaign? You can stop the campaign, change the trigger, and restart your campaign. When you restart, you’ll have the option to trigger your campaign for people who already match your new conditions. Learn more about force matching with stop and restart. A campaign only re-evaluates people currently in the campaign if the trigger is part of your exit conditions; changing the attribute or segment that triggers the campaign or the conditions of the segment itself won’t always cause the campaign to re-evaluate people with active journeys. For instance, if the exit condition is “They match the conversion criteria,” and you change which segment is assigned to the trigger, then people who don’t match your trigger could still complete the campaign. Change the frequency You can change the frequency with which people re-enter a campaign while a campaign is live. Frequency changes work the same for both legacy segment-triggered campaigns and campaigns triggered by attributes or segments. Change Impact One time to Every re-match People who have never matched the trigger conditions can enter and re-enter. People who already completed a journey through the campaign can only re-enter AFTER the re-match period has passed THEN they unmatch trigger conditions followed by rematching. For people currently in a journey to re-enter, they must exit first before they can potentially re-match. One time to At fixed intervals People who have never matched the trigger conditions can enter and re-enter. People who already completed a journey through the campaign can only re-enter AFTER they unmatch trigger conditions followed by rematching and the interval has passed. For people currently in a journey to re-enter, they must exit first then the interval must pass. Every re-match to One time People who have never matched the trigger will only enter the campaign once. People who previously completed a journey and re-match conditions will not re-enter. People still in a journey will follow your exit conditions. Once they exit, they will not re-enter. Every re-match to At fixed intervals People who have never matched the trigger will enter when they match and re-enter based on the fixed interval. People who previously completed a journey will not re-enter until they unmatch and re-match the trigger conditions. People currently in a journey when the frequency changes will re-enter after they exit and the fixed interval passes; they do not have to re-match trigger conditions like those whose journeys had already ended. At fixed intervals to One time People who are scheduled to re-enter this campaign will not do so, but those who are already in the campaign will follow your exit conditions. After they exit, they will not re-enter. At fixed intervals to Every re-match People who are scheduled to re-start your campaign will not do so. They will need to stop matching the trigger conditions and re-match them to enter again. The time of a fixed interval This immediately changes the time when people can start their next journeys. For example, if you change the interval from 7 to 9 days, people will re-enter the campaign 9 days after their initial entry. After day 9, the person will re-enter the campaign if they finished their previous journey and match the trigger/filter conditions. Otherwise, we will recheck their eligibility to re-enter the campaign after another 9 days, and so forth. The minumum wait for Every re-match This immediately changes the time when people can start their next journeys. For example, if you change the minimum wait from 7 to 9 days, a person can re-enter after 9 days have passed since the last time they entered the campaign. If a person re-matches while in an active journey, they will not immediately start a new journey after exiting the campaign. Rather, the person must exit the campaign and pass the minimum wait time then re-match the conditions to re-enter the campaign. Date-triggered campaigns Change the trigger conditions People currently in the campaign will follow your exit conditions; we do not re-evaluate date trigger conditions after they enter the campaign. People will enter the campaign when they match the new date trigger conditions. Change a person’s value that the trigger date evaluates When frequency is Once, changing the value of a person’s date attribute will NOT cause them to re-enter the campaign. When frequency is Monthly: if a person triggered the campaign (i.e. Oct 5th), then their trigger date value is updated to a date in the future in the same month (i.e. Oct 10th), the person WILL re-enter the campaign. They will re-enter after they finish their current journey, even though it’s the same month. When frequency is Yearly: if a person triggered the campaign, then their trigger date value is updated to a later date in the same year, the person WILL re-enter the campaign. They will re-enter after they finish their current journey, even though it’s the same year. Change the segment filter(s) If you change which segment is the filter or the conditions of the segment filter, we re-evaluate people currently in your campaign based on its exit conditions. So if you change the segment filter from “In Signed Up” to “In Paying Customers” and the exit condition is “They stop matching the filters,” we’ll re-evaluate the segment filter before people’s next actions (message, update action, delay, etc). People who don’t match the campaign’s new segment filter will exit the campaign. People who do match your campaign’s filter will continue the campaign. Moving forward, after date triggers the campaign, people will only begin your workflow if they match the new segment filter conditions. Object & relationship triggers Below, you’ll learn how editing a live campaign with these triggers impacts your customers. You also have the option to stop and restart the campaign after you’ve made these changes, which may be useful if you want to re-trigger the campaign immediately for people who currently meet the audience criteria. Learn more about the impacts of stopping and restarting a campaign. “Object updated” trigger For object-triggered campaigns, you can change the conditions for the trigger and filter as well as the audience that will move through the campaign. The frequency setting is always “every re-match,” so people will be eligible to re-enter your campaign. Change the trigger conditions If you update the conditions of the trigger, people currently in the campaign continue to move through it. We do not check trigger conditions after a person starts a journey in an object-triggered campaign. After you change the trigger conditions, the object must meet the new conditions for related people to go through the campaign again. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. An object must change, based on the new criteria, for people to start another journey. Change the filter conditions Changing the filter conditions impacts who will enter the campaign moving forward. Both trigger and filter conditions must be met for people to move through the campaign. Changes to filters will impact people currently in the campaign only if your exit conditions include filters—either: “They stop matching the filters” or “They match the conversion criteria or they stop matching the filters” If the campaign uses one of these exit conditions, then we’ll re-evaluate the filters before the next action in the person’s journey (a message, update action, delay, etc). Then people who don’t match the campaign’s new filters will exit the campaign, while people who do match your campaign’s filter will continue forward. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. An object must change AND the new filter criteria must be met for people to start another journey. Change the audience For object-triggered campaigns, you can change between two options: If you change from “every person related to the object” to “certain people related to the object”, people currently in the campaign will continue forward. We only evaluate the audience criteria when the campaign is triggered. When the campaign is triggered in the future, people must meet the latest audience criteria to start a journey. If you change from “certain people related to the object” to “every person related to the object”, people currently in the campaign continue forward. We do not check whether people meet the audience criteria AFTER they enter the campaign, only when the campaign is triggered. When the campaign is triggered in the future, every related person will get a journey. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. Consider whether each object update might fan out to more than 1,000 people. If so, the trigger is blocked and no one starts a journey. Learn how to work within the fan-out limit. Relationship triggers Relationship triggers include the “Person added to object” and “Relationship changed with object” triggers. You can change the conditions for the trigger and filter as well as the audience that will move through the campaign. The frequency setting is always “every re-match,” so people will be eligible to re-enter your campaign. Change the trigger conditions If you update the conditions of the trigger, people currently in the campaign continue to move through it. We do not check trigger conditions after a person starts a journey in a relationship-triggered campaign. After you change the trigger conditions, the new conditions must be met for the campaign to trigger again. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. A relationship must change, based on the new criteria, for people to start another journey. Change the filter conditions Changing the filter conditions impacts who will enter the campaign moving forward. Both trigger and filter conditions must be met for people to move through the campaign. It will impact people currently in the campaign only if your exit conditions include filters—either: “They stop matching the filters” or “They match the conversion criteria or they stop matching the filters” If the campaign uses one of these exit conditions, then we’ll re-evaluate the filters before the next action in the person’s journey (a message, update action, delay, etc). Then people who don’t match the campaign’s new filters will exit the campaign, while people who do match your campaign’s filter will continue forward. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. A relationship must change AND the new filter conditions must be met for people to start another journey. Change the audience For relationship-triggered campaigns, you can change between three options: The person that was added to the object Every person in the object Certain people related to the object based on profile or relationship attributes No matter which option you change to, people already in the campaign will continue forward. We do not evaluate audience criteria AFTER the campaign is triggered. Since the frequency is always “every re-match,” people who have already completed a journey through the campaign, or are currently in one, can start another journey. Consider whether each new or updated relationship might fan out to more than 1,000 people. If so, the trigger is blocked and no one starts a journey. Learn how to work within the fan-out limit. --- ## Edit goals URL: https://docs.customer.io/journeys/goal-changes/ Learn how changes to goals in live campaigns would impact your customers and metrics. Changing conversion criteria does not change past conversion data If you change conversion criteria, we do not retroactively count conversions. Conversion metrics only include people who convert after you add or change conversion criteria. Updating conversion criteria will not change your conversion metrics for messages that were previously marked as converted. Change the underlying configuration of a campaign’s conversion-criteria segment People currently in your campaign who already matched the new goal conditions will not be retroactively marked as converted. Rather, they must unmatch then re-match the segment conditions within your conversion window to be marked as converted. Any messages sent after a person matched the updated segment conditions in your goal will not be marked as converted because the person joined the segment before those messages were delivered. How changing conversion criteria impacts exit conditions If your campaign’s exit criteria checks for whether people match your conversion criteria, people who already matched your new conversion criteria will not exit the campaign. They must match the conversion criteria—perform an event or enter or leave a segment—AFTER you change your conversion criteria for them to exit your campaign. --- ## Edit workflows URL: https://docs.customer.io/journeys/workflow-changes/ Learn how changes to your campaign workflows impact customers. Change or delete delays Shorten a delay If you shorten a delay, like from 7 days to 5 days, all people waiting in that particular delay are re-evaluated. You will see a processing circle while this is happening. People who have already waited for more than the new delay (in our case, more than 5 days) will immediately move to the next action. Everyone else continues waiting in the delay for the required period. Lengthen a delay If you lengthen a delay, like from 5 days to 7 days, people currently waiting will continue to wait until the new time period has elapsed. Delete a delay People automatically move to the next action in the workflow. If the next item is a delay or a time window, they wait the required number of minutes/hours/days or until the time window is open. If the next item is a message or other item (attribute update, webhook), it is triggered immediately. Change a random delay Any journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. that were currently waiting and now fall outside the maximum time to wait will move forward. Change or delete time windows Limit or extend a time window People adapt to the new time window. If the time window wasn’t open before the change, but it is now, everyone previously waiting will move to the next action in the workflow. As an example, let’s say it is currently Monday at 09:30. There is a time window currently set to allow sending on Tuesday and Thursday from 09:00 to 12:00, and we alter it to allow sending on Monday from 09:00 to 12:00 also. People currently waiting in the delay will immediately proceed to the next action in the workflow. If, instead, we were to alter it to allow sending on Monday from 10:00 to 12:00, all people waiting in the time window would be scheduled to move to the next action in the workflow at 10:00 (i.e. in 30 minutes), instead of waiting until Tuesday at 09:00. Delete a time window All the people waiting for the time window to open will immediately move to the next action in the workflow. Change messages or actions Move an existing message People who received the message previously won’t receive it again, even if the content is different now. If the message is set to “Queue Draft,” the campaign does not draft another message for people who already have a draft, even if the draft was deleted. Delete a message People waiting in any delay/time window before that message will move to the next action following the deleted message (if one exists). Edit message/action content While we autosave changes to a message in a draft campaign, we do NOT autosave changes to a message in a live campaign; you must click Save. After you save a message, we’ll automatically update drafted messages to reflect your changes, but we cannot update messages that have already been sent. People who received the message previously won’t receive an updated version, even if it changed position in the workflow. Change sending behavior If you update a message or other workflow item in a live campaign to Send Automatically, people will receive the message or move through the action as soon as they reach it in your workflow. If you update a message or other workflow item to Don’t Send, people skip it and move to the next action in the workflow, if there is one. If you update a message or other workflow item to Queue Draft: Messages and other workflow items (attribute updates, webhooks) are created under “Drafts.” You’ll have to manually send them. People move to the next action in the workflow even if the drafts aren’t sent. Turn off link tracking After you click an email block, click Settings to open the panel. By default, “Track opens and link clicks in this message” is checked. Click the box to uncheck it, then click Save. Moving forward, we will not record opens or clicks for links in this message. Change subscription preference If you only use our global unsubscribe functionality (not our subscription center), you’ll see two options for sending messages: All subscribed people and All subscribed and unsubscribed. By default, we only send to subscribed users in a campaign and your messages inherit this. Change to “All subscribed and unsubscribed” If you change this behavior at the campaign level, unsubscribed people will start receiving messages in this campaign. If you change this behavior at the message level, unsubscribed people will start receiving only this message.  Our global subscription status applies to email, push, SMS, and WhatsApp, not in-app messages. --- ## Webhook campaigns URL: https://docs.customer.io/journeys/webhook-triggered-campaigns/ Webhook-triggered campaigns let you use your JSON data in Customer.io, without having to reshape it using Zapier or another product first. Where our traditional workflows trigger based on a person, a webhook trigger lets you start a campaign when you receive JSON data (in any shape) from a specific source. How it works Normally, you trigger campaigns when a person performs an event or meets some criteria you set. This type of campaign lets you use webhooks to trigger workflows when you receive data from an external source. This video helps explain how it works: When you set up a webhook-triggered campaign, Customer.io generates a Webhook URL. You’ll provide this URL to the service or platform that you capture data from and set up rules determining when that service or platform will call your webhook, sending data to Customer.io. The data that your service or platform sends to Customer.io can take any JSON shape—arrays, objects, etc. As a part of your workflow, you’ll transform the incoming data to fit your needs. You might use a webhook to capture data from an external service and convert it to: An event that triggers a campaign to send messages in Customer.io. This is the most common use case. An attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. update for a person. Another service—using Customer.io as a bridge between data platforms, like what Zapier and Segment do! sequenceDiagram Participant A as Incoming service Participant B as Customer.io Participant C as Person C->>A: Person does something in your system A->>B: Trigger webhook rect rgb(229, 254, 249) Note over B,C: Update attributes B->>B: Identify person with webhook data end rect rgb(255, 242, 248) Note over B,C: Send messages with the Create Event Action B->>B: transform webhook data to event B->>C: event triggers campaign end Reference customer attributes You cannot reference existing customer attributes in webhook-triggered campaigns because people aren’t the subject of the campaign: the webhook is. The campaign acts on the incoming webhook data, so there’s no person/customer to reference. Any attempt to reference customer data will return "" (or undefined when using JavaScript) within a Create/Update Person action. For example, the following Liquid is written to return the customer.custom_attribute value and append trigger.custom_attribute, or simply use the trigger.custom_attribute value if there is no value stored at customer.custom_attribute. {% if customer.custom_attribute == blank %}{{ trigger.custom_attribute }} {% else %} {{ customer.custom_attribute | append: trigger.custom_attribute }} {% endif %} However, within a webhook-triggered campaign, this Liquid will only return trigger.custom_attribute, regardless of the customer’s custom_attribute value. You can use a Create/Update Person action within the webhook-triggered campaign to set attributes for people based on a match with the incoming webhook. You just won’t be able to retrieve the current attribute values.  If any step in your data campaign needs to reference an existing customer attribute, send an event from the data campaign. Then use an event-triggered campaign to perform any logical evaluations based on the metadata passed in that event. Why can’t I send an email or message in my campaign? In most campaigns, a person is the subject of the journey through your workflow. But in a webhook-triggered campaign, the incoming webhook is the subject of the campaign. We don’t know who (if anybody) your data is meant to represent. This means that you can’t send a message from a webhook-triggered campaign because there’s no person to send a message to. But you can associate data from your webhook with a person in your workspace and trigger a secondary campaign that sends messages to a person—as long as there’s a value in your webhook that represents a person like an ID, email address, phone number, etc. If your incoming webhook has this kind of data, you can use the Send Event action. This lets you transform your incoming webhook data into an eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. associated with a person. This event can trigger downstream campaigns, so you can send messages to people based on your incoming webhook data.  You can also update people In a webhook campaign, you can use the Batch Update action to update one or more people based on an identifying value in your incoming webhook—like all people associated with an account, or the person associated with a specific phone number. flowchart LR a(Incoming Webhook)-->|Webhook Triggers campaign|c subgraph b [Webhook Campaign] c{Does the webhook data represent people?} c-->|yes|d(Send Event) end d-->|Event triggers second campaign|f subgraph e [Event-Triggered Campaign] f(Send Email) end Create a webhook-triggered campaign As a part of campaign setup, you’ll receive a webhook URL that you need to provide to the external service that you want to capture data from. Before you begin, make sure that you have access to the service you want to capture data from, and that that service can send webhooks. Go to Campaigns and click Create Campaign. Click Webhook. Copy the Webhook URL and provide it to your external service. If possible, trigger the webhook; this sends representative data that you can use when setting up your campaign. Click Save & Next. Set up your workflow. From here you can: Create Events to trigger another campaign or add people to segments. Create or Update People in your workspace. Send a Batch Update to update attributes or send events for multiple people. Use the Send data action to send a webhook. This forwards data to another service outside Customer.io: Click Next at the top right. Review your workflow, then click Start Campaign at the bottom of the page.  You can send Slack notifications in a webhook-triggered campaign! Because you trigger a webhook-based campaign with data that isn’t attached to a person in Customer.io, you can’t use most message types in this kind of campaign—you’d have nobody to send them to! However, because Slack messages typically have a static destination—a channel or a specific DM—you can trigger a slack message. This provides a way to notify a group of people via Slack when this workflow is triggered. Send event The Send Event action lets you transform your incoming Trigger data to an event so you can trigger messaging campaigns or add people to segments. In general, the webhook that triggers your campaign should contain an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., like an email or ID, so you can create events for people in your workspace. (If your incoming webhook does not have an identifier for a person, you can still create an event, but you’ll have to set a static identifier and update the same person everytime this campaign is triggered.) When you create an event, you must specify an Event Name (the name key) and Event Attributes (contained by the data object). Each of these fields can be one of four types: Static value: A value that is the same in every event. Trigger attribute: A property from your Trigger data. You can start typing to find a key in the incoming data or click the value in the Trigger data. 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}}.: Use liquid to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. JavaScript: Use JavaScript to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. Use JavaScript over liquid if you want to modify incoming JSON. To create an event in your webhook-triggered campaign: Drag Send Event into your workflow and click it. Set the Event Name. The event name helps you identify this action in your workflow; it’s not the name value for the event. Click Add event to set up your event. Under Who do you want to update?, select the Workspace containing the people you want to attach events to. Select the type of Identifier and set the identifier’s value for people in that workspace. You can identify someone from a trigger attribute, liquid, or JavaScript. Set a static value to always send events for the same person. Set the Event Name. This is the name value for the event that you’ll reference outside of this campaign in other campaign triggers, segments, etc. Add Event Attributes. These are values that appear in the data object of your event. You can use these properties to filter people into different campaigns or reference them in messages using 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}}.. Save your changes and click Done. Now you can finish building your webhook-triggered campaign. Create or update a person If your incoming webhook contains an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.—like an email or ID—you can create or update a person as a part of your workflow. If the identifier matches a person in your workspace, this action updates that person; if the identifier does not match a person, this action creates a new person. Drag Create or Update Person into your workflow. Give the action a Name and click Add Details. You can also set Action Conditions if you want to limit when you create or update people based on an attribute in your incoming webhook. Select the workspace you want to add or update people in. Select the type of identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for the person you want to find or create then pick a value from your incoming webhook data. Click Add attribute and set attribute values for the new person. Attribute represents the name for the new attribute. Value lets you select where your value comes from and the value itself. For example, if you want to use an event property as a new attribute value, you’ll select event attribute and then the specific attribute in the event that you want to set as a person’s attribute. Keep in mind, you cannot reference customer attributes in webhook-triggered campaigns. Click Save Changes when you’re done, and finish your webhook-triggered campaign. Batch update Other actions in a webhook-triggered campaign assume a one-to-one relationship between your data and a person. A batch update lets you associate your incoming data with a group of up to 1000 people. For example, if multiple people can belong to an account and you trigger your webhook-based campaign when the account status changes, you can use a batch update to associate the new account status with all the people in your workspace belonging to that account. flowchart LR a[incoming webhook]-->g subgraph g [Webhook-Triggered Campaign] direction LR c[batch update] c-.->|send event or update attributes|d[matching person 1] c-.->|send event or update attributes|e[matching person 2] c-.->|send event or update attributes|f[matching person n] end The batch update action works much like a Send event or Create or update person action, except that you’ll set criteria determining the group of people you want to set attributes for, or send events to. See the batch update page for help setting up a batch update. Liquid variables When you create an event or set a person’s attributes, you can manipulate values from your Trigger data with 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}}.. When you use liquid, you’ll reference incoming data in the trigger object. For example, you would access the id from the trigger data below using {{trigger.identifiers.id}}. You can also use liquid to modify the trigger data. You might want to do this if you need to convert timestamps, append values, or access a specific set of values from an array. See the liquid tag list for a list of ways you can transform data.  When you reference an array or object, use | to_json By default, liquid maps objects and arrays to strings (or an integer, where applicable). Use | to_json when you reference an object or array in your outgoing webhook to avoid errors and maintain the original shape of your trigger data! Trigger Data Outgoing Webhook (identify) { "name": "purchase", "identifiers": { "id": "abcd-1234", "email": "person@example.com" }, "purchased_at": "Fri, 04 Feb 2022 23:49:39 GMT", "total": 123.45, "items": 2, "tax": 10.45, "purchase": [ { "product": "shoes", "sku": 1234, "qty": 1, "price": 73.00 } { "product": "socks", "sku": 5678, "qty": 4, "price": 40 } ] } { "email": "{{trigger.identifiers.email}}", "last_purchased_at": "{{trigger.purchased_at | date: %s}}", "last_purchased": "{{trigger.purchase | map product}}" } Use JavaScript in your webhook-triggered campaign When you use the Create Event or Create or Update Person actions, you can also manipulate values from your Trigger data (or snippetsA common value that you can reuse with Liquid in messages and other workflow actions—like your company address. You can store Liquid inside a snippet, making it easy to save and reuse advanced values and statements across your messages.) with JavaScript rather than Liquid. Select JavaScript as the value type, and write return statements for your attribute or event data. For example, if you wanted to access a person’s id using the example above, you would use return triggers.identifiers.id;. See our JavaScript quick reference guide for more examples to help you take advantage of JavaScript in your workflow.  You can’t use Liquid inside JavaScript When you use the JavaScript option, you must manipulate values with JavaScript. If you try to return a snippet value that contains Liquid, you’ll receive an error. --- ## Send event URL: https://docs.customer.io/journeys/data-campaign-event-action/ You can convert the incoming webhook for a webhook-triggered campaign into an event associated with a person—in any workspace within your account. This makes it easy to trigger campaigns or add people to segments based on things that happen outside of Customer.io without having to talk to a developer and write your own integration.  You can send events from other kinds of campaigns too! You’re most likely to create events in webhook-triggered campaigns—that’s how you associate incoming data with a person. But now you can send events from any campaign, making it easy to trigger parallel campaigns. You can send an event for the current person or someone else entirely. How it works Webhook-triggered campaigns start with arbitrary data from an incoming webhook. This data isn’t associated with a person, and it can take any shape. If you want to send messages to a person, add people to segments, and do other things based on data from your incoming webhook, you’ll need to associate this data with a person. That’s what an event does! The Send Event action makes it easy to generate an event from your incoming webhook data, so that you can trigger other campaigns and organize segments downstream from your webhook-triggered campaign. This action even lets you send an event to another workspace, making it easy to manage people and events across your workspaces. sequenceDiagram participant A as Person participant B as External System participant C as Customer.io A->>B: Does something B->>C: Incoming webhook C->>C: Send event Note over A,C: Your event can add people to segments or trigger campaigns C-->>C: Add person to segment C-->>C: Trigger campaign C-->>A: Response message (Optional "Thanks for your response!") The Send Event action To send an event, your incoming webhook must contain a value that you use to identify people. When you create an event, you associate a value in your incoming webhook with an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people in one of your workspaces. If a person with this identifier doesn’t already exist, this action will add them to your workspace. When you create an event, you must specify an Event Name (the name key) and Event Attributes (contained by the data object). Each of these fields can be one of four types: Static value: A value that is the same in every event. Trigger attribute: A property from your Trigger data. You can start typing to find a key in the incoming data or click the value in the Trigger data. 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}}.: Use liquid to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. JavaScript: Use JavaScript to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. Use JavaScript over liquid if you want to modify incoming JSON. To send events from your webhook-triggered campaign: Drag Send Event into your workflow and click it. Set the Event Name. The event name helps you identify this action in your workflow; it’s not the name value for the event. Click Add event to set up your event. Under Who do you want to update?, select the Workspace containing the people you want to attach events to. Select the type of Identifier and set the identifier’s value for people in that workspace. You can identify someone from a trigger attribute, liquid, or JavaScript. Set a static value to always send events for the same person. Set the Event Name. This is the name value for the event that you’ll reference outside of this campaign in other campaign triggers, segments, etc. Add Event Attributes. These are values that appear in the data object of your event. You can use these properties to filter people into different campaigns or reference them in messages using 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}}.. Save your changes and click Done. Now you can finish building your webhook-triggered campaign. Use liquid when creating events When you use the Liquid value type or the JSON editor, you can manipulate trigger data to fit your event using 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}}.. This gives you more control over the data you use in your event, by letting you modify trigger data to better support your event in Customer.io. In 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}}., you access properties from your trigger data using JSON dot notation, beginning with trigger. For example, you would access the id attribute from the trigger data below using {{trigger.identifiers.id}}.  When you reference an array or object, use | to_json By default, liquid maps objects and arrays to strings (or an integer, where applicable). Use | to_json when you reference an object or array in your outgoing webhook to avoid errors and maintain the original shape of your trigger data! { "name": "purchase", "identifiers": { "id": "abcd-1234", "email": "person@example.com" }, "purchased_at": "Fri, 04 Feb 2022 23:49:39 GMT", "total": 123.45, "items": 2, "tax": 10.45 } Liquid with arrays and objects By default, liquid maps arrays and objects to strings (or, if an array contains only integers, liquid maps it to a single number). You need to use to_json to maintain the shape of arrays or objects in your event. Otherwise, you won’t be able to access properties inside the array or object in campaigns or segments that use your event. With to_json Without to_json For example, if you want to create an event with the purchase array from the trigger data below, you would format your liquid like this: {{trigger.purchase | to_json}}. The to_json tag also maintains the original data type of items inside the object—strings, numbers, etc. { "name": "purchase", "identifiers": { "id": "abcd-1234", "email": "person@example.com" }, "purchased_at": "Fri, 04 Feb 2022 23:49:39 GMT", "total": 123.45, "items": 2, "tax": 10.45, "purchase": [ { "product_name": "shoes", "sku": 1234, "qty": 1, "price": 73.00 }, { "product_name": "socks", "sku": 5678, "qty": 4, "price": 40 } ] } Modify values with liquid When you add trigger data to your event with liquid, you can modify or even transform values to better support the things you want to do in Customer.io See the liquid tag list to see all the ways you can modify these values. But, using the event above, here are a few quick examples of what you can do: Reformat the purchase_at ISO date-time to a Unix timestamp using date %s. "created_at": "{{ trigger.purchased_at | date: %s }}" outputs 1644018579 Prepend or append the event name with a static value with append or prepend. "name": "{{trigger.name | prepend "online_" }}" outputs: online_purchase Map the product names from the purchase array to a single key using map. "products_purchased": "{{trigger.purchase | map: 'product_name'}}" outputs: shoes, socks Use JavaScript when creating events You can manipulate values that you set in your event with JavaScript. When you use the JavaScript option, you’ll either produce a return statement for each data attribute in your event, or switch to the JSON editor and return an object containing all your event data properties. You can set properties from any source available in your campaign—trigger data, attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., event properties, snippetsA common value that you can reuse with Liquid in messages and other workflow actions—like your company address. You can store Liquid inside a snippet, making it easy to save and reuse advanced values and statements across your messages., etc. For example, if you wanted to access a person’s id from the object we used in our Liquid example above, you would use return triggers.identifiers.id;. See our JavaScript quick reference guide for more examples to help you take advantage of JavaScript in your workflow.  You can’t use Liquid inside JavaScript When you use the JavaScript option, you must manipulate values with JavaScript. If you try to return a snippet value that contains Liquid, you’ll receive an error. --- ## Batch update URL: https://docs.customer.io/journeys/batch-update-webhook-campaigns/ A batch update lets you apply data to a group of up to 1,000 people matching your criteria. For each person your batch update matches, you can update attributes or send events (to trigger a message campaign, for example). How it works You can perform a batch update in any campaign, but you’re most likely to use it in a webhook-triggered campaign, which starts with arbitrary data from an incoming webhook. This data isn’t associated with anybody, and it can take any shape. A batch update lets you associate this data with one or more people—either as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.. For example, if you support multiple users in a single account, you might send a campaign that triggers (via webhook) when an account state changes—it’s cancelled or upgraded. In your webhook-triggered campaign, you can use a batch update to set attributes for everybody belonging to the account, so that you can reflect their account’s status at an individual level. You can also trigger campaigns based on the account status change, sending a message to everybody involved with the account! This process makes it easy to message or update a group of people based on a single piece of incoming data—a true one-to-many interaction. flowchart LR a[incoming webhook]-->g subgraph g [Webhook-Triggered campaign] direction LR c[batch update] c-.->|send event or update attributes|d[matching person 1] c-.->|send event or update attributes|e[matching person 2] c-.->|send event or update attributes|f[matching person n] end  Try out object-triggered campaigns! You can trigger campaigns based on a change to an object instead of relying on webhooks. Depending on the structure of your data, this may be a better option for you! Set up a batch update If you’re using a batch update in a webhook-triggered campaign, you may want to send a test webhook before you begin. We’ll display your incoming test webhook in your batch update to help you understand the data that you can use in your batch update. Check out Batch Update for all campaign types to finish setting up batch update actions. --- ## Follow up on NPS responses URL: https://docs.customer.io/journeys/satismeter-data-campaign/ Set up a webhook-triggered campaign to trigger follow-up responses for people who respond to your Satismeter NPS surveys! As a part of this guide, you’ll: Set up a webhook-triggered campaign, capturing a response from Satismeter when someone fills out your survey and converting it to an event. Set up an event-triggered campaign (using the Satismeter event) to message people based on their net promoter score (NPS). sequenceDiagram participant A as Customer participant B as Satismeter participant C as Customer.io A->>B: Customer gives NPS rating B->>C: Webhook forwards response C->>C: Convert to event C->>A: Email customer with response Before you begin You’ll need access to your Satismeter account and your Customer.io Site ID and API Key credentials. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. Satismeter events have the same general shape, but we’ll base this tutorial off the following example data: { "event": "completed", "response": { "answers": [ { "id": "00000000000000", "label": "How would you rate the support?", "value": 5 }, { "id": "00000000000000", "label": "Select your favourite feature from the list", "value": "Dashboard filters" }, { "id": "00000000000000", "label": "Do you have any comments for us?", "value": "You are awesome!" } ], "category": "promoter", "completed": true, "created": "2015-07-31T19:48:07.648Z", "feedback": "You are awesome!", "id": "00000000000000", "ip": "88.100.78.186", "location": { "city": "Springfield", "country": "US", "long": "Springfield, United States", "short": "Springfield" }, "method": "In-app", "project": "00000000000000", "rating": 6, "referrer": "https://app.satismeter.com/sample", "user": { "email": "homer@simpson.com", "name": "Homer Simpson", "traits": {}, "userId": "1234" } } } Set up your webhook-triggered campaign This workflow will convert your incoming Satismeter event—sent when someone fills out your NPS survey—to a Customer.io event. We’ll use this event to trigger a campaign to respond to people who filled out your survey depending on their net promoter score. Go to Campaigns and click Create Campaign. Click the trigger block then choose Webhook. Copy the Webhook URL. In Satismeter, go to Settings > Integrations > Webhooks and click Add Another Webhook. Paste the Webhook URL that you copied from Customer.io in the Webhook URL field. Select the Survey completed Trigger, and then click Save Changes. If possible, fill out your Satismeter form as a test. This will send test data back to Customer.io, which can help you set up your campaign. Return to Customer.io and click Save and build workflow. Drag Send Event into your workflow and click it. Set an Event Name and click Add event. Under Find or add person, select email and then click the email value in in your sample Trigger data. For the Event Name, set Static value to NPM survey submitted. Under Event Attribute, add an attribute called rating and click therating in your Trigger data. Click Save Changes. Now you’re ready to set up your campaign to message people who responded to your NPS survey! Set up your event-based campaign You’ll trigger your campaign from the nps survey submitted event you set up in your webhook-triggered campaign.  Tag your campaigns In this tutorial, you need two campaigns to achieve your goal. If you apply a tag to both your webhook-triggered and event-based campaigns, you can easily relate and find your campaigns on the Campaigns page. Go to Campaigns and click Create Campaign. Click the trigger block, and click Event as the trigger type. Set nps survey submitted as the trigger event and then filter the event data for dissatisfied customers. In this example, we want people who rated us below 7. In your workflow, add your message. In our case, we’re only responding to people who gave us a score of 7 or less. However, if you don’t filter by rating, you might: Create a multi-split branch by rating (people who gave you a rating less than 7, between 7 and 8, or above 8) to send different messages to people based on their NPS score. Send a single, more generic message, using 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 differentiate different ratings. When you’re finished, click Save & Next, and finish configuring your campaign. Now, when someone fills out your Satismeter NPS survey, you can send them a response automatically! --- ## Sync customers from Stripe to Customer.io URL: https://docs.customer.io/journeys/stripe-data-campaign/ Use Customer events from Stripe to trigger a campaign syncing customers from Stripe with leads in Customer.io. Then you can send them messages in Customer.io! Customer.io is great for tracking leads and sending people messages. Stripe is great for managing customer and purchase data. With this process, you’ll capture Stripe’s customer.created event in Customer.io to represent people who have become customers. From here you might do things like thank people for making their first purchase or offer them coupons!  This process works for other Stripe events This tutorial is based on customer events, but after you sync customer data between Stripe and Customer.io, you can easily adapt this process to things like dunning emails based on Stripe’s invoice.payment_failed event or other use cases! sequenceDiagram participant A as Lead/Customer participant B as Stripe participant C as Customer.io A->>B: Signs up for subscription B->>C: Webhook containing subscription trigger C->>C: Identify person C->>C: Convert webhook to event that triggers campaign C->>A: Send messages (Welcome to our service!) Why would I sync Stripe customers to people in Customer.io? Imagine that you manage leads in Customer.io. People give you their email addresses indicating that they’re interested in your service, and you might send them promotional messages to get them to make their first purchase, subscribe to your service, and so on. When they become customers, as marked by a purchase in Stripe, you’ll probably want to sync their data to Customer.io—because you’ll send people different campaigns depending on whether they’re leads or customers. When someone becomes a customer, you’ll send a Stripe webhook called customer.created to Customer.io. This event includes the customer’s Stripe ID and their email! We’ll use the email from the event to find and update the right person in Customer.io—or create a new person if we can’t find a person matching the email from the incoming event. This effectively syncs our Stripe audience with our Customer.io audience so we can effectively differentiate between leads and customers. We can also send follow-up events from Stripe that trigger campaigns in Customer.io. For example, you might send your customers messages through Customer.io when a customer’s subscription is about to renew or when a payment fails in Stripe! sequenceDiagram participant a as Person participant b as Customer.io participant c as Stripe a->>b: Fills out a form and becomes a lead b->>a: Send campaign encouraging conversion a->>c: Person converts to customer/subscriber/user c->>b: customer.created webhook b->>b: Webhook-triggred campaign sets id on person note over a,c: Person is now identifiable by both stripe ID and email Before you begin You’ll need access to your Stripe account and your Customer.io Site ID and API Key credentials. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. This process assumes that you track leads in customer.io by email address and use their Stripe customer ID as their ID in Customer.io. People who have an ID are customers who’ve made a purchase; people without an ID are leads who you want to convert. As a part of this guide, you’ll set up a webhook-triggered campaign using Stripe’s Connect Webhook feature to send customer.created events to Customer.io. We’ll use these events to find a person in Customer.io by their email address and set their ID to the Stripe customer ID. { "id": "cus_NffrFeUfNV2Hib", "object": "customer", "address": null, "balance": 0, "created": 1680893993, "currency": null, "default_source": null, "delinquent": false, "description": null, "discount": null, "email": "wile.e.coyote@example.com", "invoice_prefix": "0759376C", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null, "rendering_options": null }, "livemode": false, "metadata": {}, "name": "Wile E Coyote", "next_invoice_sequence": 1, "phone": null, "preferred_locales": [], "shipping": null, "tax_exempt": "none", "test_clock": null } Set up your webhook-triggered campaign This workflow will identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. people and apply a Stripe ID to their profile. This gives you a way to represent leads and conversions in Customer.io, and syncs your audience from Stripe to Customer.io if someone makes a purchase without becoming a lead in Customer.io first. If you use your Stripe ID as the ID in Customer.io, you can easily differentiate between leads and customers and send them different messages. Go to Campaigns and click Create Campaign. Click the trigger block then choose Webhook. Copy the Webhook URL. In Stripe, go to Developers > Webhooks and click Add endpoint. Paste your Webhook URL into the Endpoint URL field. Click Select events, select Customer > customer.created, and then click Add events. This tells Stripe to trigger your webhook when someone creates a new subscription. Click Add endpoint. Turn Test mode on, if it isn’t already, and then Send a test event. This provides Customer.io with some test values that you can reference when creating your campaign. Return to Customer.io and make sure that you received test data. If so, click Save and build workflow. Drag a Create or update person block into your workflow and click it. Set the Name of the action to Identify customer. Click Add Details. Under Find or create person, make sure that email is selected and then click the data.email property in the Trigger data. This identifies your new person by their Stripe email. Under Edit attributes set an attribute called id where the Trigger attribute is data.id. Click Save Changes. Click Save Changes and finish setting up your webhook-triggered campaign. Remember, this is just one way you can use Stripe data in Customer.io. Now that people in Customer.io have a Stripe ID, you can pull in other events from Stripe and use them to trigger campaigns in Customer.io. What else can I do with Stripe events? Most Stripe webhooks identify people by their Stripe Customer ID. Now that you’ve synced that ID to Customer.io, you can use other Stripe events to trigger campaigns in Customer.io. For example, you can capture subscription information, payment schedules, and more. You can use people’s Stripe activity to send messages to your customers based on their purchases, subscription statuses, payment histories, and more! --- ## Campaign for syncing Mixpanel cohorts URL: https://docs.customer.io/journeys/mixpanel-data-campaign/ Send cohort data from Mixpanel to Customer.io Journeys so you can add people, index events, and more. In Mixpanel, you can organize users into cohorts, which are akin to segments in Customer.io. Mixpanel cohorts compile users based on criteria like what events they’ve performed. You can send cohort data from Mixpanel to Customer.io through webhooks, and you can trigger campaigns to update people, index events, and more. Visit Mixpanel’s documentation on webhooks for more info. Before you begin Mixpanel You must have a paid Mixpanel plan to create custom webhooks. View other prerequisites here. Customer.io In your Customer.io workspace, create a webhook-triggered campaign. Select the trigger block then choose Webhook. Next, set up a custom webhook in Mixpanel. Set up Mixpanel Log into Mixpanel. Go to Data Management > Integrations in the top navigation. Select Custom Webhooks from the list of integration options. Create a Connection. If you already have connections, select the connection dropdown then click Add Connection at the bottom. Provide a name. Then copy/paste the URL under Trigger Conditions from your campaign in Customer.io. Enter your Track API credentials under Username and Password. Make sure you use credentials for the correct workspace. Go to Data Management > Cohorts. Create a cohort you want to send to your workspace or select an existing cohort. Click the three dots followed by Export to…. Choose the name of your custom webhook then the sync frequency: one time or recurring. Click Begin Sync when you’re done. This will populate test data in your workspace to help you set up your workflow.  Mixpanel will not send more than 1,000 users per call. For info on retries, custom properties, and more, visit Mixpanel’s documentation. Finish setting up your webhook-triggered campaign Go to your webhook-triggered campaign. Drag a Send and Receive Data block onto the canvas. We refer to this as a Webhook Action. Click Add Request in the left hand pane. Create a POST request targeting our track v2 endpoint for multiple requests. If you’re in the EU, make sure you use our endpoint with track-eu in the URL. Add your Track API credentials to the Authorization header: Basic {{ ‘siteID:appKEY’ | base64 }}. Make sure you use credentials for the correct workspace; these need to match those you added to Mixpanel. Add the following JSON to the body: { "batch": [ {% for items in trigger.parameters.members %} { "type": "person", "identifiers": { "id": "{{items.<id>}}" }, "action": "event", "name": "{{ trigger.parameters.mixpanel_cohort_name }}", "attributes": {{ items | to_json }} } {% unless forloop.last %}, {% endunless %} {% endfor %} ] } Make sure your identifiers match your General workspace settings. For instance, if you identify people by email, you’ll want to replace with email. On the left, you’ll see the test data sent from setting up your cohort in Mixpanel. Click Save. Click Review items if you have outstanding steps to complete. Then click Start Campaign to review your settings and go live! If you set up your cohort to send once, go back to Mixpanel and export the cohort again. After the webhook triggers the campaign, you’ll see new people and events created in your workspace. New people are identified based on the id provided in the payload. An event with the cohort name and attributes will be added to your Data Index and associated with all people in this payload. Add event attributes to people’s profiles You add event data to people in a couple of ways: You can add a Create/update person action after the webhook action in this campaign. You can create a campaign triggered by an event (the cohort in this case) to craft a new workflow that could also include updating profiles based on event data. Create a segment from a Mixpanel cohort After you send cohort data from Mixpanel to Customer.io through a webhook-triggered campaign, you can create segments that correspond to these cohorts to track the population over time. To create a segment that targets a Mixpanel cohort: Go to Segments. Click Create Segment. Give your segment a name and description so you know which cohort it relates to. Choose Data-driven segment so that your segment audience stays in sync with your cohort data from Mixpanel. Recall that cohort data comes over as an event. Select Add condition or group then choose Event from the dropdown. In the event_name field, select your cohort name. Then choose whether you want to target people who have or have not performed this event. Hover over the condition and click Refine to filter the audience further. Save your changes. You can use this to trigger a campaign, but to reference any event attributes (attributes of the cohort in this case) in messages, you’ll need to create an event-triggered campaign or store event data on people’s profiles. --- ## Slack notification for support tickets URL: https://docs.customer.io/journeys/zendesk-data-campaign/ Set up a webhook-triggered campaign to notify your Support team about high priority tickets. As a part of this guide, you’ll set up a webhook in Zendesk to your Customer.io workspace when someone opens a high-priority ticket. You’ll use the webhook to trigger Slack notifications that alert your Support. Zendesk doesn’t support native Slack notifications when tickets are created, and this provides a handy way to help your Support team stay on top of tickets! sequenceDiagram participant A as Customer participant B as Zendesk participant C as Customer.io participant D as Your Support Team A->>B: High priority ticket B->>C: Webhook from ticket API C->>D: Slack to Support channel Before you begin Before you get started, you must have set up the message channel(s) that you want to use to notify your Support team. You’ll also need: Access to your Zendesk account. Integrate your Slack account with your Customer.io workspace Your Customer.io Site ID and API Key credentials. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. We’re basing this tutorial on Zendesk’s Ticket Created event. Our aim is to notify people when a ticket is created, and use @here to alert people when a ticket has high priority. Zendesk offers a robust system of triggers, so you can trigger webhooks under other conditions. While you determine the conditions that trigger your webhook in Zendesk, you can use 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 make sure that your incoming data contains the values you’re looking for; your Zendesk data does not need to conform to a specific shape before you can send it to Customer.io. { "version": "0", "id": "31187aa5-67d2-4eea-a921-38535bf6ec3b", "detail-type": "Support Ticket: Ticket Created", "source": "aws.partner/zendesk.com/9242270/default", "account": "123456789012", "time": "2019-05-20T22:55:31Z", "region": "us-east-1", "resources": [ "Support Ticket" ], "detail": { "ticket_event": { "meta": { "version": "1.0", "occurred_at": "2019-05-20T22:55:29.721021468Z", "ref": "1-1234567890", "sequence": { "id": "35D52F7D44640033CCCE4A5F1ADDB2AA", "position": 1, "total": 9 }, "actor_id": 20978392 }, "type": "Ticket Created", "ticket": { "id": 35436, "created_at": "2019-05-20T22:55:29.721021468Z", "updated_at": "2019-05-20T22:55:29.789534301Z", "type": "question", "priority": "low", "status": "new", "requester_id": 20978392, "submitter_id": 76872, "assignee_id": 235323, "organization_id": 10002, "group_id": 98738, "brand_id": 123, "form_id": 6876543, "external_id": "TEST1234", "tags": [ "enterprise" ], "via": { "channel": "web" } } } } } Set up your webhook-triggered campaign In this example, we’ll create a campaign to notify your Support Team when someone logs a ticket. We’ll use a True/False Branch to send a different notification with an @here alert if the ticket has high priority. While we’re basing our example on Ticket Created events, you can perform this process with almost any event from Zendesk! Go to Campaigns and click Create Campaign. Click the trigger block and choose Webhook. Copy the Webhook URL. Click Save and build workflow. In Zendesk, create your webhook: Go to > Webhooks > Webhooks, click Actions, and select Create Webhook. Paste the Webhook URL that you copied from Customer.io in the Webhook URL field. Click Test webhook to populate sample data in Customer.io. Click Next, click Add Trigger and determine the conditions that trigger your webhook. In our case, we’re going to trigger the webhook when we receive a high-priority ticket (noted by detail.ticket_event.ticket.priority). Click Test webhook to send a test event to Customer.io. This will provide you with some sample data you can reference. Drag a True/False Branch into your workflow. Click it and set the condition to detail.ticket.priority is equal to the value high. This lets us set up separate slack notifications for high priority tickets. Drag a Slack Message into your workflow for each branch—one for True and one for False. Select the Slack action in your workflow and click Add Content. In the To field, enter the channel or person you want to send a direct message to. In general, the To field expects a variable, but your Support team probably has a static channel (like #support)! Enter your message. You can access variables from the ticket using 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}}. in the trigger object. Following our example data above, you can link to the incoming ticket using {{trigger.detail.ticket_event.ticket.id}}. The example below also sends an @here alert to notify people when the ticket priority is high! Click Save and Done, and repeat the steps above for your other Slack message. When you’re finished, your campaign will look like this: Click Review Items to finish any outstanding setup steps. Click Start Campaign to review your settings and go live! --- ## Grace periods URL: https://docs.customer.io/journeys/grace-periods/ A **grace period** is a period of time people may wait to enter your campaign. Historically, it was also a period of time people might wait to *exit* your campaign. This article explains how grace periods work now and how they used to work in case you're troubleshooting historical journeys through campaigns. Overview You may notice a grace period when viewing a person’s journey. A grace period is a period of time people may wait to enter a campaign. Historically, a grace period was also a period of time people might wait to exit your campaign. Grace periods were available across multiple campaign types and could impact people entering or exiting a campaign. Now, they only impact people who enter campaigns with our legacy segment trigger. We’re phasing grace periods out so entrance and exit behaviors are more straightforward. This article explains two things: How grace periods currently work:  As of November 17, 2025, only legacy segment-triggered campaigns have grace periods. No other campaign type will hold people in grace periods moving forward. You know you’re using a legacy segment trigger if the trigger panel has these two options: You can only specify in/not in segments; with our latest segment trigger, you can also add attributes directly. You can add Filters. Filters are not available on our latest segment trigger. How grace periods used to work: This is helpful if you’re viewing a historical journey with grace periods and want to understand what that meant. This includes when a grace period happened after people met your exit conditions, including in legacy segment-triggered campaigns. How grace periods currently work People may wait in grace periods only if they have journeys in a legacy segment-triggered campaign. All other campaign types do not have grace periods. If you’re viewing a historical journey and see the person waiting in a grace period for any other campaign type, see How grace periods used to work. That section also explains how people would wait in grace periods based on exit conditions for legacy-segment-triggered campaigns. How to identify our legacy vs latest segment trigger You know you’re using a legacy segment trigger if the trigger panel has these two options: You can only specify in/not in segments. You can add Filters. Legacy segment trigger Latest segment trigger If you’re creating a campaign from scratch and choose “Attribute or Segement” as the trigger type, you are using the latest segment trigger. There is no way to create a campaign from scratch using the legacy trigger. However, if you duplicate a legacy segment-triggered campaign, it will continue to use the legacy trigger type, and people could have grace periods when they enter the campaign. When we hold people in entrance grace periods People might only wait in grace periods after entering a campaign with a legacy segment trigger. This happens when people match the campaign’s trigger conditions, but not the filter conditions. If they don’t meet the filter conditions by the time they reach an action that impacts a person, they enter into a grace period. An action that impacts a person is a message delivery, attribute update, manual segment update, collection query, create event action, or batch update. A person would move through a delay before we re-evaluate filters. If they match the filter conditions by the end of the grace period, they will move forward. Otherwise, they’ll exit the campaign. Prevent grace periods If you only want people to trigger legacy segment campaigns when they meet all of your entrance criteria, you have a couple options: Incorporate your segment filters into your trigger conditions. Create a new segment-triggered campaign with the conditions you want. People never have grace periods in these campaigns. Note, you can’t simply duplicate a legacy segment-triggered campaign because that creates a new campaign with the old trigger. Creating a segment-triggered campaign from scratch will use the latest trigger with no grace periods. With both options, people must meet all your trigger conditions before starting a journey in your campaign and won’t wait in a grace period. If you create a new segment-triggered campaign, check out more of the differences between the old and new triggers. You may want to add time-based logic to ensure you’re targeting the right people. How grace periods used to work We’re phasing out grace periods so entrance and exit behaviors are more straightforward. Historically, a grace period was a period of time people might wait to enter or exit your campaign. This was meant to ensure that people who suddenly stopped/started matching your entrance or exit conditions would continue through your campaign, but this has proven more confusing than helpful over time. Grace periods were available across multiple campaign types and could impact people entering or exiting your campaign: Trigger type Possible grace period after entering a campaign Possible grace period before exiting a campaign Legacy segment trigger yes yes Latest segment trigger no no Object updated no yes Person added to object no yes Relationship changed with object no yes Important date no yes Event no no Form submission no no Webhook no no Now, people may only wait in a grace period when they enter a campaign with a legacy segment trigger. But since this is our legacy segment trigger, and our latest segment trigger doesn’t have grace periods, eventally grace periods will be completely phased out of campaigns. When we used to hold people in exit grace periods Campaigns used to have exit grace periods, but we’ve deprecated this functionality across all campaign types to make exit behavior more predictable. You may come across exit grace periods when looking at a historical journey. It may say “Waited in a grace period” before they officially left the campaign. This would happen if the campaign let people exit early. When someone matched the conditions to exit, we’d first hold them in a grace period. Specifically, if people could exit your campaign early when they no longer matched filter conditions, we would first hold them in a grace period before ending their journey. We did this to ensure people met your conditions for a period of time and should actually exit. If they rematched filters within that timeframe, they’d move forward; otherwise, they’d exit. For legacy segment-triggered campaigns, the early exit condition checked when they no longer matched filter or trigger conditions. So if you’re troubleshooting a historical journey in this type of campaign, keep in mind they only had to stop matching trigger OR filter conditions, not both, to wait in a grace period then exit. --- ## Our Recipe Book URL: https://docs.customer.io/journeys/recipes/ Campaigns are incredibly flexible, but it can sometimes be hard to map your particular use case to a campaign workflow. The recipes we've collected provide some common use cases that you can use to achieve a particular goal or to get ideas for your own campaigns. Recipes typically have some Ingredients that we’ll tell you about: a few things that need to be in place to use the recipe. These are things like attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. that you need to have in place to use the recipe as written. But you can always adapt a recipe to fit your needs. Just add a dash of your own use case; a pinch of your data; and a splash of your creativity. Send a Welcome Email Birthday and Anniversary Campaigns Cart Abandonment Double Opt-in Onboarding Campaign Optimize emails with Just Words Trial Expiration Reminders Cohort Tests Trigger campaigns based on roles RSS Feed Email Campaign Reminders for multiple upcoming trips --- ## Send a Welcome Email URL: https://docs.customer.io/journeys/send-a-welcome-email/ Introduction The way you make customers feel in the first few days after they’ve signed up for your site or service can influence their level of engagement with your company long-term. This moment of first impression is a crucial moment. In this tutorial, we’ll walk you through creating your welcome email in Customer.io (and also assumes that you’ve completed your integration). Ingredients Every customer in your Customer.io account must have a created_at attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that is a timestamp Method Most of the time, you’ll want to send your welcome series to all new users who sign up for your product or service. In Customer.io, this takes a few simple steps. Create your segment for new signups By default, you’ll have a segment in your account called “Signed Up”, which includes all users with a created_at timestamp attribute. Check out this segment by heading to Segments in the navigation. If you’ve deleted this segment, don’t worry-it’s easy to recreate. In the Segments section of the app, click on Create Segments, and enter your segment name. Now, the segment builder will probably have this default rule set up already: Make sure the rule is looking for the attribute created_at is a timestamp and then hit Save Changes.  The timestamp you send to Customer.io should be a Unix style timestamp. This is what your Signed Up segment should look like after saving: Set up your campaign trigger Now for the campaign! In the navigation, head to Campaigns, then click Create Campaign. Click Choose trigger then Segment change. Select the segment Signed Up: Set up your welcome email Add your email Drag an email from the Build menu onto your workflow. Click the email to begin editing. Change the name so it’s purpose is clear to your teammates. Then click Add Content to start writing your email. Write your welcome message Fill in your message content, choose your email layout (rich text or code editors only), and review your email’s envelope - to/from fields, subject line, and more. Personalize each email for your audience using liquid tags to add a customer’s name and otherwise tailor the content. In our example, we added our customer’s first name and plan details.  Don’t know what to say? We’ve got you covered. Here are some welcome copy templates you can adapt and use.  Preview with sample data Search to find a user with the attributes specified in your liquid syntax so you can preview the message. Edit your email’s behavior Back on your workflow, click the email to review settings. By default, messages are set to Queue Draft, which means we’ll draft the email for your audience but wait for you to manually send them. If you want the email to send right away after you start your campaign, change this to Send Automatically. Add your delay Depending on your vision for your new customer experience, you may want to send your first welcome email right away, or give people some time to explore your product before you send your first message. Let’s add a short delay before we send our welcome. In the workflow, click and drag a Time Delay just before the email: For our example, we’ll set just a 10 minute delay before sending the welcome email. Click to edit the worfklow item, change the delay time, then save: If you don’t want to add a delay, that’s it! Move onto the next step. Optional: add conversion criteria Next, you can set conversion criteria. This provides a way to track an action that you hope the new user will take after reading your message. Conversion criteria mark messagesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. and journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. as converted when a person performs an event, joins a segment, or leaves a segment. Track conversions to see how many of your messages and journeys have the desired effect with your audience. If you don’t want to set conversion criteria, click No goal. Optional: change your exit conditions In the case of this example, you can leave the Exit settings as they are by default. That is, a person will exit the campaign if it’s time to send them a message and they don’t match your triggers and filters. Review and start your campaign! Almost there! Review your welcome campaign and check to see if there are any errors or anything missing. If you have any steps to complete, you’ll see Review items at the top of your workflow. Otherwise, click Start Campaign to review. Before you start your campaign, you must choose to either send your campaign to all people who ever signed up, or just new users who are signing up after the welcome campaign starts. For welcome messages, you probably want to choose to send to Future matches only. Otherwise people who already exist in your system and signed up in the past will suddenly get a random welcome email once you set your campaign live. Finally, click Start Campaign, and that’s it! If you’ve set your email’s sending behavior to draft to check how your campaign will run, remember to send the drafts and change the email sending behavior to “Send Automatically” when you’re ready. Once emails start sending, you can head to the campaign’s overview page to see how the campaign is performing. Other considerations We’ve covered a welcome campaign that is email-based, but there are many more ways that you can reach out to customers in their first few days after signing up. You may want to include a combination of emails, in-app messages, push notifications, SMS, Slack notifications, or all of the above. Here’s a quick example of a multi-step welcome workflow, once a user signs up and enters the campaign: Wait 2 hours Send first email When the user has opened that first email, send the next one using Action Conditions Depending on an action they take in the second email, send them an SMS with a Twilio Action When the user receives the SMS, notify your sales team that they’ve reached this point in the funnel and need personal outreach Slack Action or Webhook to your CRM. After the sales team has been notified, update an attribute on the customer to indicate that they have been contacted by the sales team. Create or update person action Wrap Up Welcome emails are a win-win for both your company and your customers. If you have any questions about how to set up a welcome email series, please get in touch! --- ## Birthday and Anniversary Campaigns URL: https://docs.customer.io/journeys/birthday-and-anniversary-campaigns/ Introduction Sending messages on birthdays, anniversaries, and other recurring dates is a great way of keeping customers engaged with your company throughout the year. Customer.io makes this simple. Let’s get started sending your customers a birthday discount, celebrating their time as a customer, or reminding them of something that reccurs on a long time scale. This recipe documents a yearly anniversary campaign, but the same steps work for any type of date. Ingredients A date of each person’s anniversary stored in an attribute on their profile  Do not send zero as a value for your date field Ensure that your system does not send zero (0) or other false default values for people when you do not have an actual date for them. Incorrect date values may still be interpreted by our system and may cause your campaign to send messages you do not want to send. For example, zero (0), as a timestamp, is equal to January 1, 1970, 0:00:00 which will cause yearly campaigns to trigger every January 1st and monthly campaigns to trigger on the first of every month. Method Storing the attribute If you already have the date attribute stored on the people in your account, then you can skip this step. When you create a new person in Customer.io, you can store their anniversary or birthday date as one of their attributes. This is an example of how you might do that with the _cio.identify() function in the JavaScript snippet: <script type="text/javascript"> _cio.identify({ id: '1899', // must be unique per customer email: 'john@example.com', created_at: 1743259962, // Custom user attributes first_name: 'John', plan_name: 'free', anniversary_date: 1481067323 }); </script> Here, I’ve used anniversary_date but you can name this date attribute however you like. Note, we are using UNIX timestamps in this recipe. Once the anniversary_date attribute has been added, when you check out this person in Customer.io, their attributes look like this: Create the campaign You will use this anniversary_date attribute as the trigger for your campaign. Customer.io will use the month and day from this attribute to trigger the campaign once per year. From Campaigns, click Create Campaign. Click Choose trigger. Select Important date as the trigger type. Pick anniversary_date from the date attribute dropdown. Set the desired time of day to trigger the campaign. Click Save.  We recommend setting this to midnight in the user’s time zone If someone just signed up and their anniversary_date is a timestamp today at 11am and your campaign triggers at 12pm, then that person would enter your anniversary campaign. This may be desired if it’s a birthday campaign! But wouldn’t make sense for an anniversary campaign that’s meant to be sent yearly. Midnight in the user’s time zone ensures the trigger time will most likely have passed when the person signs up and will schedule them for NEXT year’s occurrence. Scroll down to Frequency on the trigger panel. Choose Yearly from the dropdown so this campaign sends yearly on your customer’s anniversary date. Click Save. If you set the trigger to midnight in a user’s time zone, then drag a Time Window onto your workflow. Set this to the time of day you want to reach your audience. Drag a message block into your workflow and add your content. Remember to update the sending behavior to Send automatically so you don’t have to manually send drafts. Review and start your campaign. Wrap Up This recipe covered the basics, but you can also: Choose to send the campaign a certain number of days before or after the anniverary date. For instance, if you want to give 3 days of notice for a promotion they can use on their anniversary. Build a countdown workflow leading up to a date. Follow this same recipe, but use Delays in your workflow to create a drip of messages. Send messages for events that occur monthly rather then yearly. In this case, Customer.io will use only the day of the month as the recurrence criteria. If you need any additional help setting up your campaign, please get in touch and we’ll be happy to help! --- ## Cart Abandonment URL: https://docs.customer.io/journeys/cart-abandonment/ Whether you’re an e-commerce company or a product company, the techniques used for cart abandonment can help you increase completion rates in any flow where you have drop off. One of the most powerful concepts you’ll learn is how to store context on a profile for easy recall and personalization later. How it works For this recipe, we’ll send emails to customers who have abandoned their carts after a certain amount of time. We offer two methods through event-triggered or segment-triggered campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria.. We recommend using event-triggered campaigns when possible as you’ll have more flexibility for targeting information related to carts than if the information were stored on a person’s profile (segment-triggered campaign). Method (Recommended): Event-Triggered Campaign For this recipe, we’ll walk through how to send cart eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. data to your workspace and create different pathways for your customers based on the actions they’ve taken and time that’s elapsed since updating their cart. Ingredients Your Track API credentials from your Customer.io workspace. The ability to send event data when items are added and removed from a person’s cart. Send Event Data for Cart Updates To remind people about their abandoned carts, you’ll create an event that captures updates to cart items including adding, removing, and changing the attributes of an item, like quantity. Send this data with a name like cart_updated so it’s easy to find within your workspace. You’ll either need to: Send the contents of the cart with each event (shown in this recipe) or Set up a webhook in your campaign to retrieve the complete contents of the cart Your cart event might look something like this: { "name": "cart_updated", "timestamp": 1677700459, "data": { "currency": "usd", "added": { "product": "something-else", "qty": 2, "price": 19.99 }, "already_in_cart": [ { "product": "socks", "qty": 2, "price": 23.45 }, { "product": "sneakers", "qty": 1, "price": 99.99 } ] } } For this recipe to work, you’ll send a person’s entire cart with the event. This should include all items currently in the cart as well as the timestamp of the event. Include any other information that you want to query with 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}}. within the reminder email, such as item price, color, or quantity. Set the Trigger for Your Campaign After sending your event, you’ll create an event-triggered campaign. Set the trigger as the event you just made: cart_updated. Create a Workflow Using Wait Until Now you’re ready to create an event-based workflow for cart abandonment. While the goal of the workflow is to remind a person to purchase the items in their cart, you’ll also need to make sure a person does not receive too many reminder emails nor receive them too soon. To make sure you’re targeting the right users at the right time, let’s use Wait Until to create 3 different pathways for people who just updated their cart (that is, just entered the campaign). Set a Condition where the cart has updated since the trigger event. Next, set another Condition where a purchase has occurred since the cart was updated. Finally, select Max Time and enter a time period that the workflow should wait to send a person a reminder email. In this example, we use 45 minutes. Create Your Reminder Email Finally, under the Max Time path, add your reminder email. Use liquid to customize the email with each customer’s cart information. When a person enters the workflow, the clock starts counting to 45 minutes. If people update their carts or purchase items after the event trigger, they exit the campaign. Otherwise, when the counter reaches 45 minutes, your customers will receive an email reminding them to purchase the items in their cart. Method: Segment-Triggered Campaign For this recipe, we’ll walk through storing and updating cart data on your customers’ profiles so you can send messaging around cart abandonment. At a high-level, you’ll set the following logic: When a person has an item in their cart, you’ll populate their cart attribute. When a person updates their cart, you’ll update their cart attribute. When a person completes a purchase, you’ll clear their cart attribute. Then you’ll create a segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. for people who have the cart attribute which will trigger a campaign reminding them to checkout. Ingredients Your Track API credentials from your Customer.io workspace. The ability to update profile data programmatically when items are added and removed from a person’s cart. Set Up Your Cart Store Cart Data on People’s Profiles To get started, store people’s cart data as an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. For this recipe, you’ll store this data in an attribute called cart using the following JSON: { "cart": [ { "item_name": "Hat", "item_price": "22" }, { "item_name": "Scarf", "item_price": "11" } ] } Update Cart Data When a Person Adds or Removes Items Next, you’ll set the “cart” attribute whenever a person adds or removes items using an identify call. You can use your language of choice to update a person’s “cart” attribute, but we’ve added examples using our JavaScript source integration and our Classic JavaScript SDK below: JavaScript Source (recommended) JavaScript Source (recommended) cioanalytics.identify('1234', { cart: [ { item_name: "hat", item_price: 22 }, { item_name: "scarf", item_price: 11 } ] }); Classic JS SDK Classic JS SDK _cio.identify(1234, { cart: [ { item_name: "hat", item_price: 22 }, { item_name: "scarf", item_price: 11 } ] }); By storing the cart attribute on a person, you’ll be able to use 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 personalize messages for your audience based on the items in their cart. Clear Cart Data When a Person Completes a Purchase If there’s nothing in a person’s cart, i.e. the person purchased, or otherwise emptied their cart, then you don’t want to send them an email reminding them to purchase. This step removes the cart attribute from a person to ensure you notify the correct people. When you set the cart attribute to an empty string, you’ll remove it from a person’s profile. JavaScript Source (recommended) JavaScript Source (recommended) cioanalytics.identify('1234', { cart: "" }); Classic JS SDK Classic JS SDK _cio.identify(1234, { cart: "" }); Create Your Segment Now that you’re sending and storing cart data on people’s profiles, create a segment for people who have a cart attribute using the exists condition. Create Your Campaign Set the Trigger for Your Campaign Create a campaign that targets the segment you just made – people whose cart attribute exists.  People will match this campaign immediately when they add an item to their cart. So make sure you add a delay in your workflow that gives customers enough time to check out. Below the trigger segment, allow people to re-enter the campaign at a Frequency you specify. This ensures they enter the Cart Abandonment campaign for all future carts. Set Your Exit Conditions By default, people will exit when they stop meeting the trigger condition. If a person’s cart is empty or does not exist, they will exit the campaign and will not receive a reminder email. This prevents your campaign from sending messages to your customers if they complete their purchase before the delay ends. If you set a goal for the campaign, you can change the exit criteria such that people exit when they meet the goal too. Create Your Workflow Using a Delay You’ll add a delay before your first email as well as between your first email and future messages related to cart abandonment. This prevents customers who intend to purchase from immediately receiving a reminder email anytime they change their carts. Create Your Reminder Email Finally, drag an email into your workflow under your delay. This is the email you’ll send to anyone who continutes to meet the campaign trigger condition (cart exists) after 45 minutes. Craft the email content with any relevant information. Use liquid to reference the items in your customers’ carts, their prices, and more: Hi {{customer.first_name}}! Your cart contains: {% for item in customer.cart %}{{ item.item_name }} - {{ item.item_price }}{% endfor %} In our simple example, this will render like the email below: You can use this basic structure to store lots of data as customer attributes and merge in a picture of the item in the cart, a link to the item, and more. --- ## Double Opt-In URL: https://docs.customer.io/journeys/double-opt-in/ This document will guide you through the basics of setting up double opt-in functionality for your users. A double opt-in is a good practice to follow in order to improve your deliverability and/or comply with local laws like CASL in Canada. Introduction This document will guide you through the basics of setting up double opt-in functionality for your users. A double opt-in is a good practice to follow in order to improve your deliverability and/or comply with local laws like CASL in Canada. Ingredients A double_optin attribute to track each person’s status A data-driven segment, either for people without the double_optin attribute or for new users depending on your strategy A segment-triggered campaign where we’ll send an email with a link people can click to opt in Another data-driven segment for people who have clicked the opt-in link link Method Create a segment to trigger the campaign First and foremost, we’ll need an attribute for each person that tracks whether or not they’ve been through the double-opt in process, and what they’ve chosen. Let’s call this attribute double_optin. Then, we’ll need a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. to trigger the double opt-in campaign. For this example, we’ll use a condition of “double_optin does not exist”. You can use a different segment if you want—like a data-driven segment for new users. Start creating your double opt-in campaign Create a segment-triggered campaign: Go to Campaigns. Click Create Campaign. Click Choose trigger. Select Segment change. Use the segment you created in the previous step. Drag an email into your workflow. Add your content including a link that your audience can click to confirm that they want to opt-in.  Make sure the double opt-in URL is unique You can use any URL for the link, as we’ll track the clicks on a link matching a specific URL. Make sure it’s a unique URL either by creating a dedicated page to link to, or by adding a URL variable such as ?double_optin=true to the URL. Create a segment for users who click the link in your email Now that we have the email created, we’ll need a second data-driven segment for people that clicked the opt-in link. Create your new segment before you finish setting up your campaign. Finish creating your double opt-in campaign Now that we have that second segment, we can complete the setup of our campaign. We’ll add a Wait Until workflow action and use the segment we just created as the condition that we want to wait for. We’ll also add a maximum wait time of 1 week before moving to the next action. This lets us consider people who haven’t clicked within 7 days of receiving the email as not opted-in. Thanks to Wait Until conditions we can define different flows based on which condition is matched first. If the user enters the segment we created (and therefore has clicked the email), we’ll set the attribute to true. If they haven’t entered the segment after 1 week, we’ll assume they haven’t perform double opt-in and set the attribute to false. To do that, we’ll add a Create or Update Person action in each branch to update the double_optin attribute. Overview of the final result Your campaign should look like this! When you’re done, activate your campaign to begin processing double opt-ins. Use the double opt-in attribute to filter messages The double_optin attribute is a custom attribute on a person’s profile. Customer.io doesn’t automatically check this attribute before sending messages—setting the attribute alone doesn’t prevent messages from going to people who haven’t opted in. To enforce double opt-in across your messaging, create a data-driven segment for people where double_optin equals true. Then use this segment to control who receives your campaigns and newsletters: Campaigns: Add the segment as a filter in your campaign’s entry criteria so that only people who completed double opt-in can enter the campaign. Newsletters: Set the segment as the recipient of your newsletter to make sure you only send to people who opted in. --- ## Onboarding Campaign URL: https://docs.customer.io/journeys/onboarding-campaign/ Introduction After someone signs up for your app, the task of onboarding them begins! Show how they can get value from your app and improve your user activation and retention rates. Thoughtful onboarding email campaigns are a win for everyone. This recipe will show you how to set up targeted drip series using segment triggered campaigns in Customer.io to keep people interested, learning, and making progress. Ingredients Choose a specific behavioral goal that is key to your onboarding process Optional: basic knowledge of Liquid Method While you can have several onboarding campaigns for your customers, each campaign should be focused on nudging them towards a particular goal. For this example, we’ll use a fictional subscription service based on the Acme Corporation from Looney Tunes. Each month, subscribers get a box of roadrunner-catching products. The specific goal we have for Acme customers, like Wile E. Coyote, is to get them to fill out their profile so they can get tailored product selections. Let’s get started! Prep for the campaign Before getting started on creating this campaign, let’s do some prep work. You’ll want to decide on a specific behavioral goal and think about how many messages you’d like to create to nudge people towards that goal. Create a segment-triggered campaign In the navigation, click Campaigns, followed by Create Campaign. Give your campaign a name that makes its purpose clear so you can track it later (in this case, we’ve used “Onboarding Series #1: Profile Set-up”). Click Choose trigger on your workflow then select Segment change from the panel. Define your trigger Your trigger defines who will receive this campaign. In our case, that’s members of Acme’s Hunters Club service who haven’t yet filled out their profile. Choose your segment from the dropdown: We provide all accounts a segment called Signed up which is composed of anyone whose created_at attribute is a timestamp, so we’ll use that for this campaign. Need a segment? You might need to create a segment that doesn’t exist yet. For example, you may have deleted the “Signed up” segment or want to reach a more specific audience for your particular onboarding series. In that case, choose Create a new data-driven segment from the trigger dropdown. This is the condition for Customer.io’s “Signed up” segment: Set up your workflow Add your first email Drag and drop an email from the Build menu into your workflow. Click the email to to begin editing. Change the name so it’s purpose is clear to your teammates. Then click Add Content to start writing your email. Write your email’s content Fill in your message content, choose your email layout (rich text or code editors onl), and review your email’s envelope - to/from fields, subject line, and more. Personalize each email for your audience using liquid tags to add a customer’s name and otherwise tailor the content. In our example, we added our customer’s first name and plan details.  Don’t know what to say? We’ve got you covered. Here are some welcome copy templates you can adapt and use.  Preview with sample data Search to find a user with the attributes specified in your liquid syntax so you can preview the message. Once you’re happy, save your email, and click Done. You’ll be taken back to your workflow. Edit your email’s behavior Let’s update the email to Send automatically so after we start the campaign, we don’t have to manually send drafts. Click the email in your workflow. Click Settings to expand the menu. Click Send automatically from the Sending behavior dropdown. Click Save. Add a delay Timing is key in onboarding. We want people to have a chance to explore the app before prompting them to fill out a profile. So from the workflow builder sidebar, I can click and drag a Delay before they receive my next message: I’ve set mine to one day, but you can set yours to whatever you like (days, minutes, or hours). Add another email Now it’s time to really start building out our campaign. I want to add another email, one which prompts the customer (if they still haven’t’t filled out their profile) to do so. Once again, I drag an email into the workflow after the delay. Optional: Set up a time window Say I know that my users often have some downtime on weekend mornings— that’s when they browse products most often. So maybe that’s when I want to ping them. Customer.io lets you do that, by adding a Time Window in the workflow. Drag and drop it in, and click to set it up: This way, I can be sure that they’ll get the message when it’s most appropriate for them, and most helpful! And so on… With this in mind, keep setting up your workflow. Add emails, delays, and time windows as appropriate. You can see my Acme example here: Good onboarding emails are well-timed, focus on a behavioural goal, and offer different techniques and framings to give value to the customers (and the company). My first email is ‘Why you should set up your profile,’ with a view to giving Wile E. Coyote better product recommendations if I know more about him. The final one is ‘How to level up your roadrunner-catching skills,’ appealing to customers’ desire to catch more roadrunners— and prompting them to fill out their profile in the process. Set up your goal Once you’re done with your campaign setup, you can set a goal to help you understand whether or not your messagesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. and journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. have their desired effect. Conversions can be a more reliable way to track success than opens and clicks. In this case, a conversion is the behavioral goal we decided at the beginning: whenever a customer fills out a profile. I have a segment that identifies those customers: Which I then use to define my conversion criteria: Review! It’s time to review your onboarding campaign; check it out at a glance and see if there are any errors or anything missing. Check timing, email names, and sending behaviours until they’re exactly what you need. And finally, choose who to send to! Current or future matches? By default, campaigns are forward-looking. Anyone who matches your trigger in the future will receive the onboarding campaign. However, you can choose to include existing matches, too—it’s up to you! For my example, I only want future matches to receive my new onboarding campaign: Get sending! Click start, and that’s it! You can head over to the campaign’s overview page after the emails start sending, to get an insight into its performance. If you’ve set your emails’ sending behavior to draft to check how your campaign will run, remember you’ll have to set to “Send Automatically” to reach your customers. Wrap Up Getting started in any app and making that app stick in a user’s day-to-day can be a struggle, and that’s why thoughtful onboarding campaigns are so critical. If you have any questions about the process or are wondering how to apply it to your service (assuming you don’t offer a roadrunner-catching product subscription box), please send us a message! --- ## Optimize emails with Just Words URL: https://docs.customer.io/journeys/just-words/ Automatically A/B test and optimize emails on the fly with Just Words. How it works Just Words lets you test many variations in one experiment and auto-refresh underperforming content automatically. You’ll create a campaign with an email in Customer.io and connect it to a template in Just Words. Then Just Words will automatically ingest data from Customer.io and update the template with the best performing variation(s). As a part of this process, you’ll add a random cohort branch to a campaign with two paths. One path will be the control, and the other will be the Just Words branch. If you’re setting up a new campaign, this is easy. If you have a live campaign that you want to optimize, you’ll set up your random cohort with 100% of the participants going to the email currently in your campaign; we call this the control path. Setting things up this way ensures that people continue getting messages while you complete this process. 1. Link your Customer.io account to Just Words In Customer.io, go to Settings > Workspace Settings > Manage API Credentials and create a new App API key. Copy this key. In Just Words, add the API key to your integrations. You can verify that the API key is correct by clicking Verify. This automatically creates a Reporting WebhookWe can optionally send real-time data about a select set of activities to any public endpoint you choose. Once your team has such an endpoint ready to receive data, it’s as simple as adding that endpoint to your Workspace configuration in Customer.io. in Customer.io to start ingesting data into Just Words. 2. Create your Customer.io campaign Before you create templates in Just Words, you need to have a campaign—and an email within that campaign—that you want to optimize. You don’t need to start the campaign, but it’s best to populate your complete campaign and an email with your expected copy. This ensures that Just Words has the context it needs to help you optimize your email. 3. Create your Just Words template In Just Words, click Add Template to create a new Just Words template. Select the Customer.io campaign containing the email that you want to optimize. Select the email within the campaign that you want to optimize. Click Analyze Campaign. Just Words will pull in details about the email and the campaign, and they’ll ask you to confirm some basic information to make sure they’ve understood your campaign and message correctly. On the Personalization page, configure the Variables that you want to optimize in the email. These can be things like the email’s subject, the preheader, or even the entire body of the email. Just Words’ AI can even personalize emails based on the user and trigger attributes within Customer.io. When you’re done configuring your variables, click Save Template. You’ll see a page where you can generate new Variants. Click Generate treatment variants to generate a handful of new variants, or you can prompt Just Words with specific instructions. In the Copy Studio, you can generate and edit as many variants as you’d like to test. Once you are happy with the emails, you can “Approve” each variant and press the Save button. It will automatically generate HTML variations with Liquid logic. 4. Connect your Just Words template into a Customer.io campaign Now you’re ready to incorporate your Just Words template into your campaign. In Customer.io, go to the campaign and email you wanted to optimize. Add an A/B test split with 50% going to Path A and 50% going to Path B. If the campaign is already live, you can set 100% to Path A until you’re done with this process. Make sure that Path A contains your original email. In Path B, add a Send and Receive Data block (a webhook) and a new email below it. Select the webhook and go to the Response tab. Set the jw attribute to {{ response | json }}. Save the webhook. If you return to Just Words, go to the Template Configuration page and find the newly created webhook and email. When you click Save Changes, you’ll see Just Words reflect the changes to your campaign. If you refresh the Customer.io page, the tiles should now be updated. If you had set 100% to Path A, you can set it to 50/50 to “launch” the JW branch. Congrats! You’ve just created your first Just Words experiment! 🚀 --- ## Trial expiration reminders URL: https://docs.customer.io/journeys/trial-expiration-reminders/ Introduction Sending emails to remind people that their trial is ending is simply good customer service. Don’t put the burden on them to remember. The benefit is that it brings you back to front of mind as the clock starts ticking and can help nudge upgrades to happen sooner rather than later. In this recipe, you’ll learn how to use a trial expiry date to trigger a campaign with 3 emails. There will be 2 trial reminders, 7 days and 1 day before the expiration, and then a notice that the trial has ended. Ingredients Timestamp attribute on your users for the trial expiration date Method As with any email series in Customer.io, you’ll want to set up the proper foundation to make sure you have the right data and segments to create a ‘set and forget’ campaign. For trial expiry emails, this is just a few simple steps. Set up a trial expiry date attribute Before creating the campaign, you’ll want to store when the customer’s trial expires as an attribute. As with all dates in Customer.io, store this on the person’s profile as a unix timestamp, like this: trial_expires: 1476350359 Segment customers whose trial is expiring Once you have users in Customer.io with the date their trial expires as an attribute, you can set up your segment. Here we’re grouping together everyone whose trial expires 7 days from the date they enter this campaign. Why 7 days? For this example, it’s because we want our first reminder to go out 7 days before the trial expires. So, we set the segment to match people once their trial is 7 days from expiration.  Brush up on timestamp rules for building segments. Next, create a segment looking for everyone on your free plan or who hasn’t upgraded (this ensures you don’t accidentally send messages to someone still in their trial period but who has since upgraded). Something like: Create your campaign Once you’ve set up your segments, it’s time to set up your campaign. Go to Campaigns. Click Create Campaign. Click Choose trigger. Select Segment change. Use the segments you created in the previous step. Create your workflow: drag time delays and emails into the workflow: Set conversion criteria to track users who upgrade their plan. In this case, we set the goal as “leaves the segment Trial users within 1 week of being sent any delivery from this campaign.” Other considerations If you have a 14 day trial, an alternative way to do this is to send a message 7 days after signing up. But if you want to test different trial lengths, this gives you a way to use the date the trial expires and send messages based on that. Need some tips on how to approach your email content? Find out: Upsell email examples that convert: how to woo your customers. Wrap Up Trial expiry emails are a great way to ensure that your new customers move seamlessly through your funnel and convert quickly. Even if customers don’t convert right away, your trial expiry email is still a great way to make an impression on new customers and keep your product fresh in their mind when they are ready to upgrade in the future. If you have any questions on how to set up a trial expiry email, don’t hesitate to reach out! --- ## Cohort tests URL: https://docs.customer.io/journeys/cohort-testing/ When you want to A/B test something other than email content, try a cohort test! Randomly assigning people to cohorts gives you a way to perform tests independently of individual messages. Cohort testing is great for things like: Testing timing: different delay times before sending a message Single vs multi-touch: sending a user one email vs. several emails Blackhole/holdout testing: where you want to send a message or no message at all. Check out our recipe specifically for holdout tests. Ingredients You’ll need: A basic understanding of liquid The ability to create campaigns If you decide to assign cohorts via API, you’ll also need: A terminal window and basic knowledge of cURL (or a friendly developer) Your site ID and API key Method To create a cohort test: Randomize and assign cohorts to your users via a campaign or our API. Create a campaign to run your cohort test. For our example, we’ll run a campaign that sends an email five minutes or ten minutes after a person performs an event. Step 1: assign a cohort attribute In this example, we’ll trigger a campaign for users who signed up for your product and assign them a cohort attribute to divide them into groups. This can be used for any campaign moving forward!  You can use our API to create and assign a cohort attribute instead of a campaign. If you use our API, skip to creating your test campaign. Go to Campaigns and click Create Campaign. Enter a name. Click Choose trigger and select Segment change. Specify users in the segment Signed Up and click Save. Drag a Create or Update Person block onto the canvas. Click the block then click Add details. Under “Who do you want to update?” select “the person in the workflow.” Under “Which attributes do you want to add, change, or remove?” add liquid to randomize cohort assignment. To create two groups: Set a new attribute called cohort with a Liquid value and use the code below to generate a number, 0 or 1. If the outcome is 0, it sets the cohort attribute to A. If it’s 1, it’s set to B. {% capture rValue %}{% random 1 %}{% endcapture %}{% case rValue %}{% when '0' %}A{% else %}B{% endcase%} To create more than two groups: Here’s an example that assigns one of 10 possible options (A to J): {% capture rValue %}{% random 9 %}{% endcapture %}{% case rValue %}{% when '0' %}A{% when '1' %}B{% when '2' %}C{% when '3' %}D{% when '4' %}E{% when '5' %}F{% when '6' %}G{% when '7' %}H{% when '8' %}I{% when '9' %}J{% endcase %} Click Save Changes then Done. On the review step, let’s choose to update Current people and future additions, though you can choose “Future additions only” if you don’t want to update users already in your database. This sets us up for future cohort testing, as anyone who signs up in the future will randomly get an A or B value, in addition to our existing people. Now, we’re ready to test! Step 2: create a campaign to test your cohorts In this example, we’ll create a campaign to test how our cohorts’ behaviors change when we delay sending an email at different intervals. We’ll send the same email to people who view a product, but for one cohort, we’ll send after five minutes, and for the other, after 10 minutes. We’ll create a campaign triggered by the event viewed_product, and the workflow will contain two time delays before an email, where each time delay has a condition targeting a different cohort. Go to Campaigns and click Create Campaign. Enter a name. Select Event as the trigger. Enter the event name, in this case, viewed_product. Specify your message settings and your goal and exit criteria. On the workflow step, drag two Time Delay blocks onto the canvas followed by an Email block. Click a delay block and specify “Wait 5 minutes.” Then add an attribute condition where cohort is equal to the value A. Save your changes. Click the other delay block and specify “Wait 10 minutes.” Then add an attribute condition where cohort is equal to the value B. Click the email block and create your message. Remember to update the sending behavior to Send Automatically. Click Review items at the top if you have outstanding steps to complete then click Start Campaign for a final review. As soon as people view a product, they’ll enter the campaign and wait to receive the email based on the time delay that matches their cohort attribute. Notes When to assign the cohort attribute You can assign a cohort value within your testing campaign by combining both campaigns into one. This might be useful if you want to run A/B tests in some campaigns and A/B/C/D in others. However, assigning cohorts separate from testing cohorts lets you trigger tests based on different criteria. Testing one cohort per campaign or newsletter If you want to test multiple campaigns against each other, you can create a segment for each cohort you want to test then add this as a segment trigger condition or filter. If you want to test multiple newsletters against each other, add an attribute condition and target a specific cohort or add a segment condition that targets a specific cohort. --- ## Trigger campaigns based on Roles URL: https://docs.customer.io/journeys/role-based-messaging-campaign/ When you send campaigns customized by people's roles, you'll have a better chance of motivating them to take action than if you sent generic messages to everyone. Method Determine how you’re storing data in Customer.io. This will determine the type of campaign you should make. Are you storing role information on people as profile attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.? Or are you storing role information on people’s relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. to objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. (like Accounts, Companies, Courses)? Role stored as profile attribute Build a segment If you store role data as attributes on people, you can create a segment to trigger a campaign for people that belong to it. Go to Segments. Click Create Segment. Name your segment so it’s easy to find later - like “Marketing role.” Choose Data-driven Segment. Add an attribute condition. In this example, the role is stored as this attribute person_employment_role. If you have similar roles, you might include them all in one segment. For instance, maybe you have “marketing_manager” and “product_marketer” and people with any of these roles should receive the same messaging. Change the top condition to “Any of the following conditions match” and include all roles in the segment. Create a segment-triggered campaign Now you can use your role-based segment as a trigger for a campaign. Start by creating a campaign and choose Segment change as the trigger type. Select the segment you made above: Only users with marketing roles will enter this campaign, so you can customize your content for this specific audience. If your segment includes people with multiple kinds of marketing roles, you can personalize messaging further with liquid conditionals or action conditions based on specific roles. Role stored as relationship attribute If you’re storing role data on a relationship to an object, you can use the relationship attribute to trigger campaigns. For example, if you have Account objects in your workspace, you could trigger a campaign based on the creation of an account and filter the audience to people who are admins of the account. You could also trigger a campaign when someone becomes an admin on the account. For instance, maybe you want to nurture admins of accounts to encourage them to upgrade their account. Checkout our recipe on upselling power users to begin. Wrap Up Role based messaging is an ideal way to increase engagement by only showing people content relevant to them. Customization based on role is only the beginning! Consider tailoring your messaging based on location, interests, or even company size to create more engaging emails. If you have questions about how to apply this to your business, we’re here to answer! --- ## RSS Feed Email Campaign URL: https://docs.customer.io/journeys/rss-feed-email-campaign/ Introduction With an RSS Feed Email Campaign, you can send new posts to your audience whenever you publish content. This makes use of our API Triggered Broadcast campaigns and a Zapier workflow. Ingredients Zapier account publicly accessible RSS Feed URL Method Create an API Triggered Broadcast Campaign Learn how in our guide for API Triggered Broadcasts. Set up Zapier Workflow For those of you new to Zapier, it’s a handy tool that helps you connect your apps and automate workflows. For this recipe, you’ll be setting up a Multi-Step Zap in Zapier, which lets you automate a sequence of tasks in a workflow with a single trigger. (Multi-Step Zaps are available on paid plans.) Once you’re in Zapier, click “Make a Zap!” in the top menu to get started. Set up the trigger First, we’ll choose the built-in app “RSS by Zapier” and select “New Item in Feed” as the trigger. Then, enter your Feed URL. Take a minute and make sure that the “What Triggers a New Feed Item” field says “smart” — which means you’ve chosen the recommended Different Guid/URL option. (This makes sure that when you make changes to a post, it won’t cause this Zap to re-trigger.) Publish Date to Unix Timestamp Next, we’ll take the human readable timestamp (in our feed, that was our published date and time) and convert it to a Unix timestamp by running a bit of code. We’ll do that by choosing another built-in app “Code by Zapier”, choose to “Run JavaScript”, and add our code. Here’s the code we used, which may vary depending on your date format. var created_at = +new Date(input.time)/1000; output = {created_at}; (An alternative in this step is to use the Zapier formatter to convert a datetime value into a Unix timestamp.) Convert body to JSON Next, we’ll convert the article to a JSON object. Choose the built-in app “Code by Zapier” and the “Run JavaScript” option again. For the Input Data field, enter “body” and then from the dropdown, we were able to select our raw content text, which allowed Customer.io to parse the HTML as-is in the designed email. Here’s the code we used: var object = input.body var escaped_body = JSON.stringify(object) output = {escaped_body}; Send Webhook to API Triggered Broadcast In the final Zapier workflow step, choose the built-in app “Webhooks by Zapier” to send a webhook to Customer.io. Create a “Custom Request”. In the “Edit Template” step, choose POST as your HTTP method. Enter your URL. It will look like this, with your campaign ID filled in: https://api.customer.io/v1/api/campaigns/<your-campaign-id-here>/triggers In the “Data” step, we’ll add our data, in JSON format. In our example, we’re adding headline, date, and text data, as well as defining our recipient segment. Here's some more information on how to format this broadcast data! Select “no” for the “Unflatten option”. Next we’ll add our bearer credentials by filling in our App Key. In the “Headers” step, fill in “Content-Type” and application/json" as the key, value pair. Wrap Up API Triggered Broadcasts are a great way to save time for recurring messages like blog post updates. If you have questions about how to apply this recipe, let us know! --- ## Reminders for multiple upcoming trips URL: https://docs.customer.io/journeys/reminders-for-multiple-upcoming-trips/ Introduction This recipe is useful for multiple upcoming trips but you can also adapt it for these use cases: Encourage a shopper to purchase their abandoned cart even when it’s possible for the user to have multiple carts. Let a customer know that one of their many saved coupons is expiring soon. Notify users when one job of many they’ve applied for is closed. The idea is that sometimes people can have multiples of one thing - trips, carts, coupons, and job applications - and you may want to target a specific trip, cart, etc during a campaign. This recipe helps you do that! We’ll create a campaign that reminds people to book a hotel for their upcoming trip. Since a customer can have multiple upcoming trips, we’ll make sure the reminder is about the correct trip and sent at the correct time. Ingredients Basic knowledge of liquid Ability to send events to Customer.io Method This recipe will create two campaigns, one triggered by a trip_booked event and another by a hotel_booked event. You’ll store an attribute on people’s profiles, trips_with_hotel_booked, to track which trips already have a hotel booked. This way, you can ensure you only send the reminder to book a hotel for any trips not in that list. Campaign 1: Hotel is booked Since we want to send a reminder if a hotel has not been booked, we need to track which trips include hotels. This campaign will do nothing more than add the trip’s trip_id to a comma separated list of ids in an attribute on the person’s profile indicating that they’ve booked the hotel— trips_with_hotel_booked. This is the attribute that is checked prior to sending the reminder in the campaign we’ll build next. Send the event You’ll want to send an event into Customer.io for the user when they complete booking a hotel. In this example, we’re calling it hotel_booked. The event can contain any information about the hotel that you wish to include for use in other messages (maybe a confirmation email) that are triggered by the same event instance, but for the purposes of this capability, the only necessary attribute is the trip_id. { "trip_id": "123" } For more details on sending events into Customer.io see our API documentation. Create the campaign Now that the event is sending to Customer.io, you can use it to trigger a campaign. Go to Campaigns. Click Create Campaign. Click Choose trigger. Select Event and choose hotel_booked. The workflow The only item necessary in this workflow is an attribute update to add the trip_id to the comma separated list of ids named, trips_with_hotel_booked. Drag a Create or update person action into your workflow. Click it then choose Add details. The liquid for this update: {% if customer.trips_with_hotel_booked != blank %} {{customer.trips_with_hotel_booked}},{{event.trip_id}} {% else %} {{event.trip_id}} {% endif %} If the attribute doesn’t exist on the profile already, the Create or update person action will add it. This is the only workflow item required to make this use-case work, but adding a confirmation email to this workflow would be an excellent way to manage the entire hotel booking process in one campaign. Campaign 2: Trip is initially booked You’ll create a campaign triggered by the initial booking event. This will be the campaign that sends the reminder message a specified amount of time after the initial booking. Send the event Just like the first campaign, you’ll want to send an event into Customer.io for the user when they complete booking their trip. In this example, we’re calling it trip_booked. This event should contain any information about the trip that you care to include in the email reminding them to book their hotel. We’ve included destination, confirmation number, and trip_date. It also needs to include a unique identifier for the event. We’ve used trip_id in this example: { "trip_id": "123", "destination": "Melbourne, Australia", "confirmation": "N35RTX", "trip_date": "28-OCT-2020" } For more details on sending events into Customer.io see our API documentation. Create the campaign Now that the event is sending to Customer.io, you can use it to trigger a campaign. Go to Campaigns. Click Create Campaign. Click Choose trigger. Select Event. The workflow The first item your workflow needs to contain is a delay set to the amount of time you’d like to wait before sending the reminder. In this case, we’ll remind them after 10 days. Once someone waits 10 days and exits the delay, we want to check whether they’ve booked the hotel for this trip yet. If not, we’ll send them a reminder to do so. To create this logic, add a True/False Branch to the workflow. In this case, you want to check whether the trips_with_hotel_booked attribute for this person contains the trip_id for this event. To do this, use the ‘attribute condition’ in the True/False Branch. Enter the attribute trips_with_hotel_booked and then select the option to compare it to an event attribute. And finally, enter trip_id as the event attribute to check. If the hotel has been booked for this trip, we don’t need to send them a reminder, but we do want to clear the trip_id from the trips_with_hotel_booked to clean things up and make sure the attribute doesn’t hit length limits. To do this, add a Create or update person action to the True branch. This should update the trips_with_hotel_booked attribute by removing it from the list. Here’s the liquid for this update: {% assign trips = trips_with_hotel_booked | split: "," %} {% assign updated_trips = "" %} {% for trip in trips %} {% if trip != "" and trip != event.trip_id %} {% if updated_trips != blank %} {% assign updated_trips = updated_trips | append: "," %} {% endif %} {% assign updated_trips = updated_trips | append: trip %} {% endif %} {% endfor %} {{updated_trips}} The reminder email Now the fun part! With the data in the original trip_booked event, you can craft a personalized email reminding and encouraging them to book their hotel for the trip. The final workflow To recap, this campaign will hold the person in a delay for 10 days after booking the trip. After 10 days have passed, it will check whether they’ve booked a hotel for the trip. If so, it will simply clean up the attribute tracking that hotel booking, but if not, it will send a reminder to book a hotel for that particular trip. The workflow should look something like this: Wrap Up Using profile attributes to store status of multiple active trips on the customer account is a great way to customize your messaging for each of those trips. If this approach doesn’t work for you, though, or you need help getting it set up, send us a message! --- ## Introduction to broadcasts URL: https://docs.customer.io/journeys/broadcasts-in-customerio/ We offer two types of broadcasts: *Newsletter* and *Messages triggered via API*. If you’re new to Customer.io, checkout Campaigns, broadcasts, and transactional messages to make sure you’re creating the right kind of automation. How it works A broadcast is a message or workflow that you trigger (or schedule) for a group of people. It’s not “active” like a campaign: you’ll send, schedule, or trigger your broadcast for a group of people. Anybody outside that group won’t get the broadcast unless you send it again. When you create a new broadcast, you choose a Broadcast Type: Newsletter or Messages triggered via API. These two things are fundamentally different, but we can help you choose the right one. A Newsletter is a single message you want to send to a group of people. You can schedule a newsletter or send it immediately. Newsletters provide a simple way to broadcast a message to your audience without setting up a complex campaign. The type Messages triggered via API is what we call an API-triggered broadcast. This lets you set up a complete workflowA series of actions (messages, attribute updates, etc) that people progress through as a part of a campaign or broadcast.—like an event-triggered campaign, except that the trigger call that you send to our API supports multiple recipients. API-triggered broadcasts are highly customizable, and give you precise control over workflows from your backend integration, but are certainly more technical than newsletters or campaigns. What type of broadcast should I choose? You should send a newsletter if: You need to send or schedule one message to a group of people. The data to personalize your message already exists in Customer.io. You don’t have development resources. A newsletter is the easiest way to accomplish a simple broadcast. You might send a newsletter to alert your audience to changes in your terms of service, or to a group of people when you onboard people belonging to an account. You should set up an API-triggered broadcast if: You want to send more than one message. You want to send custom data with your message(s). You want to trigger a workflow for a group of people from your backend. API-triggered broadcasts provide a way for you to trigger workflows directly from an outside service and pass custom data to the workflow. It’s like an event-triggered campaign but the event can represent a group of people. For example, you might trigger a broadcast for people who are interested in an event when tickets go on sale for the event, or students who are interested in a particular online course when registration opens. Newsletters Newsletters provide a simple way to send a single message to a group of people. When you set up a newsletter you’ll: Determine your audience. Set up your message. Schedule or send it. Newsletters do offer a few powerful options that aren’t available for other campaigns or broadcasts: Send to Everyone: You can send a message to everyone in your workspace without setting up a segment or assigning attributes. This helps you send important messages, like changes to your Terms Of Service or company-wide alerts. Send rate: You can limit the send rate to deliver your message in batches. Scheduling: You can schedule your newsletter to send at a specific time! API-triggered broadcasts API-triggered broadcasts provide a way to send a one-to-many campaign on demand. It may help to think of API-triggered broadcasts like event-triggered campaigns, except the event comes from your backend integration and represents a group of people. For example, you might use an API-triggered broadcast to: Send news alerts to a group of people interested in a specific topic. Alert users interested in an event when tickets go on sale. Alert students when registration opens for a new online class. Unlike a newsletter, you can reference trigger data in your API-triggered broadcasts with 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}}.. Learn more about API-triggered broadcasts. Organize your broadcasts You can organize your broadcasts by assigning tags, a way of grouping related automations together. You can use tags to group not just broadcasts, but campaigns, transactional messages, and segments too. Create, assign, edit, and delete as many tags as you need in your workspace. --- ## Newsletters URL: https://docs.customer.io/journeys/newsletters/ A newsletter is a single message that you send to a group of people at once. Newsletters make it easy to broadcast a single message to a wide group of people at a predictable time without setting up a campaign workflow. How it works A Newsletter is a single message that you want to send to a group of people. You can schedule a newsletter or send it immediately. Newsletters provide a simple way to broadcast a message to your audience without setting up a complex campaign. You might send a newsletter to alert your audience to changes in your terms of service, or to a group of people when you onboard people belonging to an account. Send a newsletter The steps to set up a newsletter are fairly simple, but each step includes settings that we’ve explained in the sections below. Go to Broadcasts and click Create Broadcast. Pick Newsletter. Determine who gets the newsletter in the Recipients step. Set a Goal for your newsletter. Set up your message Content. Review your message and either send or Schedule it. Recipients You can send a newsletter to a group of People who match your conditions or to Everyone in your workspace. On the right, you’ll see how many people will receive your newsletter. Click View list to see the list of expected recipients. People matching conditions On step 1 of newsletter creation, you decide who will receive your newsletter. If you select, “People matching conditions,” then you can define who will receive the message based on one or more segment, attribute, newsletter, or message conditions. Segment condition People will receive your newsletter if they belong or don’t belong to specific segments. Filter for people IN or NOT IN segments and join conditions using AND or OR logic. Here’s a walkthrough of setting up a segment condition: Attribute condition People will receive your newsletter if their profileAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. attributes meet these conditions. You can filter for people whose attributes exist, don’t exist, are equal to, or are not equal to a value. Newsletter condition People will receive your newsletter if they previously received one or more newsletters.  Newsletter status indicates if it’s been sent If you select a newsletter as a condition, you may see a green or yellow dot on the newsletter icon. This is a “status” indicating whether the newsletter has been sent (green) or drafted (yellow). Message condition People will receive your newsletter if they meet conditions related to any message in your workspace, like: people have never been sent any in-app message people have ever been sent the email “Thank You email” in “Anniversary Campaign” Import recipients Some people might not have a shared condition that makes it easy to add them to a segment. In these cases, you can send a newsletter to your audience by uploading a CSV or importing a Google Sheet to produce a manual segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. Check out more information on manual segments here. Email: Skip duplicate addresses If your workspace is ID only, multiple people in your workspace can have the same email attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The Skip duplicate email addresses setting prevents you from sending your email newsletter multiple times to the same address. It is on by default. At your newsletter’s send time, we generate a deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. for each member of your audience. If this setting is on and a subsequent member of your audience has the same email attribute as someone who already generated a delivery, we will skip that person. This setting only prevents duplicate deliveries; we do not affect your newsletter audience in any other way. For example, we don’t modify the Is a recipient segment criteria for your newsletter—all of the people in the original audience of the newsletter, including people with duplicate email addresses who were skipped, will join the Is a recipient segment. This helps prevent you from accidentally sending repeat or follow-up emails to someone who already received your newsletter.  Recipient counts in your Newsletter set up do not account for duplicates Deduplication takes place at send time, so the audience counts you see when you set up your newsletter and A/B test might be inflated by people with duplicate emails in your audience. While this setting prevents you from sending duplicate emails, you can deduplicate people in your workspace by enabling the email or ID setting, which makes email a unique attribute. This ensures that multiple people cannot have the same email address attribute. SMS/WhatsApp: Skip phone number duplicates The phone attribute isn’t required to be unique in Customer.io. If more than one person in Customer.io has the same phone number, you’ll end up sending duplicate outbound messages. When you enable the Skip duplicate phone numbers setting, and we send a message to a phone number, we’ll ignore any subsequent messages to that phone number. Set a subscription preference If you’re using our Subscription Center feature, you’ll set your newsletter’s topicA category of message, set within your workspace’s subscription center, that people can subscribe to or unsubscribe from. Topics let your audience determine the kinds of messages they want to get from you. in the Subscription preference setting. If people are unsubscribed from the topic, they won’t get messages from the broadcast. If you don’t want to set a topic, you can use the All subscribed and unsubscribed setting. You should use this setting sparingly—for things like transactional-style messages and important notices. Sending messages to unsubscribed people can violate their trust—or even violate local laws and regulations (GDPR, CAN-SPAM, etc)! Send to unsubscribed users You can opt to send your message to unsubscribed users—which you might do to notify users of important information like a change to your terms of service or scheduled maintenance. Go to the Recipients tab under Sending options:  Be careful when sending to unsubscribed people. Continuing to message someone who has unsubscribed could breach anti-spam laws in your country, and we reserve the right to terminate your account if you do so. Goal: determine what makes your newsletter successful You track the success of your newsletter based on whether a person performs an event, enters a segment, or exits a segment after they receive your newsletter. If a person performs your goal criteria within the time frame you configure, the messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. is marked converted. You can set goals to determine the success of your messages, and follow conversion rates over time to improve your message strategies. See Goals for more information about conversions; newsletters have the same goal options as campaigns. We track conversions for the following message/delivery types: Conversions attributed Conversions not attributed Email Slack Message SMS Create or update person action Customer.io Push Notifications Customer.io In-app Messages Webhooks1 1You must enable webhook conversions manually. Slack and Create or update person actions are often internal or used for analytics purposes; they don’t always send messages to end-users. For that reason, we don’t attach conversions to them. You can enable webhook conversions on individual webhook actions. In the Goal section of the workflow, you can decide if you want to track opens and clicks (on by default). Content: write your message You can send a newsletter for any channel– email, in-app, push, SMS, or webhooks. Select your channel of choice to begin. While most channels are self explanatory, you can select Webhook to “broadcast” requests to your webhook server for each member of your audience. You might do this to trigger in-app messages or messages from a service outside Customer.io. When you elect to broadcast webhooks, you can personalize the URL or payload for each member of your audience using 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}}.. How you create your message is up to you. Choose the editor that works for you, use customer data, and even create variations to test!  You may need to set a send rate for webhooks If you haven’t set a Send rate, we issue requests to your webhook server as quickly as we can. Make sure that your webhook server can handle the spike in traffic from a broadcast or set a send rate to limit the number of requests we send to your webhook server at a time. Set up an A/B test You can add an A/B test to evaluate variations of your message and find the one that most resonates with your audience. You’ll set up a test and send it to a sample percentage of people. After you send your newsletter, you can track variation performance and send the most successful version to your remaining recipients. Click Add Test in Content step to set up an A/B test. You can send your tests to a sample percentage, and automate sending to the rest. Adding a variation will copy your first message, so tweaking one variable of the message should be easy! Set your Sample Percentage: this is the percentage of recipients in the test. After the test, you can send the best-performing variant to the rest of your audience. Let’s say you want 20% of recipients to receive one of two emails with different subject lines. A randomly selected 10% of people will receive each email, and you can choose to automatically send the one which performs better to the remaining 80% once the test is over. Start by setting the Sample percentage to 20% using the slider. Click Add variation to create the messages you want to test. While you can test anything, we recommend that you only change one variable per test to produce a reliable test. You can add up to 8 variations.  Use a statistically significant test audience Results are most reliable when you have at least 500 recipients per variation. With fewer recipients, your test may not produce Conclusive Results. (Optional) Enable Automatically send winner to remaining recipients to send the winning option to your audience as soon as we decide the winner. Select the metric you want to use to decide the winner and how long the test should run before sending it to the remaining recipients.  You must choose a winner within 7 days of activating your newsletter. If you’re manually choosing a winner, make sure you do so before the week is up. Otherwise, you won’t be able to select one and send to your remaining recipients. This limit helps reduce load times for newsletter metrics. Find the results of your test When you send your newsletter, you can start to monitor the results of your test on the Test tab of the Newsletter’s Overview page. From here, you’ll see the current metrics for each variation, with the metric chosen to track the winner in bold. The Details section of the page shows the timeline for the test: When you first sent the newsletter To what percentage of your recipients How much longer the test will run When the winning message will be sent to the remaining recipients. Modify your test If you want to cancel your test, edit the duration of the test, or edit the metric used to determine the winner, click Edit under Details. Cancel automatic send: If you don’t want to send the winning message automatically, click Edit in the bottom-most timeline of the Details section. When you turn off automatic sends, you can’t turn it back on. You’ll still have the chance to send to your remaining recipients manually. Send to remaining recipients manually: If you didn’t enable Automatically send winner to remaining recipients, you can manually determine the “winning” test and send it to your audience. When you feel that you’ve got conclusive test results, click Send to remaining. Then you’ll pick the variant you want to send and when. Review & schedule your newsletter You can send your newsletter immediately or schedule it to send for a future date/time. Click Send at specific time, and choose a date and time to schedule your message. You should always schedule your newsletter at least 5 minutes in advance, and note that the timezone defaults to your local time.  Did you just update a snippet? If you just updated a snippet used in your messages, make sure you wait a couple of minutes before activating your workflow to ensure all updates appear in your delivered messages. Send in recipient’s timezone You can also schedule messages to send at the specified time in each recipient’s timezone. Learn more about sending in users’ timezones. We calculate who receives your newsletter at send time, so the audience count you see under Recipients could differ from the final count we send to. Limit send rate Set a maximum rate limit to maintain deliverability. When you limit the send rate, we queue messages for delivery in batch sizes of the number you specify. On the Review tab, turn on Limit send rate. You can choose between two rate limit types: a Fixed rate or a Daily ramp. Fixed rate Choose Fixed rate if you want to send a consistent maximum number of messages per minute, hour, or day. Specify your Limit: the maximum number of messages and the time period. Choose your Send Mode: send evenly or send as fast as possible. Sending evenly across each period is the default to protect your downstream systems from getting overloaded. However, you might choose Send as fast as possible up to the limit in each period if your content is time-sensitive or you have campaign conditions dependent on this message. Note, you can’t set a daily fixed rate if you’ve enabled sending in a recipient’s timezone to ensure people receive your messages at the right time. Daily ramp Choose Daily ramp to exponentially increase your send volume over multiple days. This is ideal for domain warming; it automates the manual warm-up schedule so you don’t have to create multiple newsletters for each day. When you select Daily ramp, configure the following settings: Starting Daily Volume: The number of messages to send on the first day. For new domains, start with a low number like 100. For established domains, you can start higher—like 1,000. You can learn more about the recommended ramp up rates in Domain warming. Target Daily Volume: Set a target that reflects the maximum number of messages you’ll expect to send on a daily basis. This is the volume the newsletter will ramp up to sending by the end of the period. This is especially important if you’re using this newsletter to warm up your domain. You don’t need to warm it up past your max daily send rate. Number of Days: How many days the ramp lasts (up to 60). The daily volume increases each day until the ramp period ends or all recipients receive the message. We’ll send across the day, too, not all at once. If recipients remain after the last day of your ramp, we continue sending at the final day’s rate until all recipients receive the message. Preview the chart to see the projected daily send volume and make sure your adhering to best practices for ramping. We’ll warn you if your daily ramp might not be optimized too. After the ramp completes and all recipients have received the message, the newsletter’s status changes to Sent—just like any other newsletter. Rate limit constraints Newsletters must finish sending within 60 days. If your rate limit settings would take longer than 60 days to reach all recipients, you can’t activate the newsletter—adjust your settings accordingly. If you’re sending in a user’s timezone, you can set a fixed rate per hour or minute, but not day. You also can’t set a daily ramp schedule. If you pause and resume a newsletter during a ramp, it picks up where it left off based on how many messages have already been sent—it doesn’t skip days. Edit rate limit while sending You may want to change the rate limit while your newsletter is sending to improve your domain warming outcomes. For instance, if your emails are getting deferred or engagement rates are lower than expected, you may want to slow your send rate. This allows mailbox providers more time to evaluate the traffic. From your newsletter, click the Recipients tab. Click Edit rate limit. To edit the rate limit, your newsletter must follow these conditions: The newsletter state must be Sending. The newsletter must have a configured rate limit. You can’t add a rate limit if the newsletter is sending as fast as possible with no limit. Modify the settings as you see fit. You can change between Fixed rate, Daily ramp, or choose Unlimited to remove the limit. Click Save changes. The daily schedule will show the daily rate for the messages that have already been sent followed by the estimated schedule for the new rate limit. In the image above, notice that the Today marker (Day 7) shows a volume greater than the restart volume (Day 8). When this happens, your newsletter will restart sending the next day, not immediately. Specifically, it will restart in the next 24-hour period, which is based on when your newsletter first started sending. So if your newsletter started sending at 12 PM originally, your newsletter will restart sending after you save your changes and it reaches 12 PM again. Pause and resume a newsletter You may want to pause a newsletter when you notice a typo after sending or need to make some other change to your message. You can pause if the message hasn’t finished sending to all of your recipients. Any messages already sent cannot be recalled, though.  You can’t change recipients while your newsletter is paused When you resume a newsletter, it continues sending to your original recipient list. It cannot account for people who match, or stopped matching, your recipient list while it was paused. In Broadcasts, click a newsletter with a status of Sending. (If the status is “Sent,” then all recipients have received the newsletter and it cannot be paused.) In the top-right, click Actions then select Pause sending from the dropdown. This action immediately pauses any further messages from sending. Back on the Broadcasts landing page, the newsletter’s status will change to Paused. Go to the Content tab, click Edit content and make your changes. If editing a message with A/B variations, choose the variation then proceed to edit. You can modify any message content (email, in-app, etc) or field (subject, to, from, etc). You can’t modify your recipient conditions, tracking parameters, goals, or send options (rate limiting). After you make your changes, click Resume sending in the top-right of the newsletter then confirm your action. The newsletter will continue sending to people who have not received the newsletter yet; people who already received the newsletter will NOT receive another version. The newsletter’s status will update to “Sending” then “Sent” when all recipients have received it.  You cannot pause newsletters that send in a user’s time zone. You can pause newsletters with A/B variations and with a rate limit. But you cannot pause newsletters that are scheduled to send in a user’s time zone. Send to an individual person After you schedule and send your newsletter, you can send the content to an individual person who wasn’t in your original audience. You can only send your broadcast or newsletter to an individual this way. If you want to re-send your newsletter to a large number of new recipients, you should copy the newsletter and re-send it to a new segment of people. Go to Broadcasts and select the broadcast you want to re-send. The broadcast must have a sent status. Go to the Content tab. Select the Recipient you want to send your message to. The recipient must be a person already in your workspace; you cannot send a newsletter to a person you haven’t identified. Click Send newsletter to the recipient. Copy newsletters to another workspace If you use similar messages in different workspaces, you can copy a broadcast or a newsletter to another workspace. Go to Broadcasts, find the newsletter or broadcast that you want to copy, and click Copy to. Then select the workspace you want to copy your items to, and click Continue. Note that when you copy newsletters and broadcasts, we’ll reset settings that don’t exist in the destination workspace—like layout, audience, etc. You should also check your 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}}. syntax in your destination workspace. You may not have the same attributes or other variables in your destination, so you may need to update liquid statements to make sure that your messages render and send properly. --- ## API-triggered broadcasts URL: https://docs.customer.io/journeys/api-triggered-broadcasts/ An API-triggered broadcast is like an event-triggered campaign, but the event—the `trigger`—represents a group of recipients. How it works API-triggered broadcasts provide a way to send a one-to-many campaign on demand. It may help to think of API-triggered broadcasts like event-triggered campaigns, except the event comes from your backend integration and represents a group of people. For example, you might use an API-triggered broadcast to: Send news alerts to a group of people interested in a specific topic. Alert users interested in an event when tickets go on sale. Alert students when registration opens for a new online class. Unlike a newsletter, you can reference trigger data in your API-triggered broadcasts with 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}}..  Try our Postman collection! You can use our Postman collection and associated environment to get started with the Customer.io API. Our environment is based on our US endpoints; if you’re in our EU region, you’ll need to add -eu to track_api_url and app_api_url variables.  You cannot trigger a broadcast more than once every 10 seconds. This is because broadcasts are meant for a wide audience of people per trigger. They’re not intended for one-to-one interactions with individual people. You also can’t have more than 5 broadcasts queued for a single API-triggered broadcast. Create an API-triggered broadcast The steps to set up an API-triggered broadcast are fairly simple, but each step includes settings that we’ve explained in the sections below. Go to Broadcasts and click Create Broadcast. Enter a name so your team members can easily find the broadcast on the landing page. Click Messages triggered via API. (Optional) Click the name and add a description so your team members can tell what the purpose of the broadcast is. (Optional) Add one or more tags to help you organize and filter your broadcasts on the landing page. Click Set goal. You can choose “No goal” if you don’t want to set one up. Modify your settings and Messages to change what subscription center topic people must be subscribed to to receive messages. If you don’t have the subscription center enabled, we’ll send messages to people who are globally subscribed. Set a message limit if you want people to only receive a certain number of messages from you over a period of time. Click Build at the bottom of the workflow. Click and drag a message, flow control, or data block from the panel onto your canvas. After you add your first block, you can drop subsequent blocks over any plus sign. Decide your message sending behavior. By default, all messages are set to “Queue draft.” If you want messages to send automatically, click into each message and adjust the dropdown: After you’re finished building your workflow, click Start Broadcast in the top right to review and start your broadcast. If you haven’t fully set up your broadcast, click Review items and complete setup. Define your recipients and trigger your broadcast. Set a goal You can set up a goal to track the success of messages and journeys based on whether or not a person performs an event, enters a segment, or exits a segment after they receive a message from your broadcast. If a person performs your goal criteria within a configurable time frame after they receive a message in your workflow, we’ll mark the messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. and journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. converted. Goals help you determine the success of your broadcast, and follow conversion rates over time to try and improve your messaging strategies. See Goals for more information about conversions; api-triggered broadcasts have the same goal options as campaigns. We track conversions for the following message/delivery types: Conversions attributed Conversions not attributed Email Slack Message SMS Create or update person action Customer.io Push Notifications Customer.io In-app Messages Webhooks1 1You must enable webhook conversions manually. Slack and Create or update person actions are often internal or used for analytics purposes; they don’t always send messages to end-users. For that reason, we don’t attach conversions to them. You can enable webhook conversions on individual webhook actions. Set up your workflow The workflow defines the journey for people who receive the broadcast—the messages they receive and the actionsA block in a campaign workflow—like a message, delay, or attribute change. you perform. API-triggered broadcasts perform all actions in the workflow at the same time, so you can’t use delays or time windows in your workflow. You can, however, send multiple messages if you want, or add additional actions like updating people’s attributes, sending events, and so on.  Did you just update a snippet? If you just updated a snippet used in your messages, make sure you wait a couple of minutes before activating your workflow to ensure all updates appear in your delivered messages. Define recipients & trigger your broadcast You can manually define recipients or define them programmatically in your API call. If you define them manually, you can either trigger the broadcast through the UI too or through an API call. You might define your recipients in the UI if there’s a common group of people you send these broadcasts to. If you trigger the broadcast through an API call, note that the recipients defined there will override your UI conditions.  Recipients defined in your API call override recipients defined in the UI Make sure you don’t include recipients in your API call if you want to send to recipients defined in the UI. Define recipients via UI After you activate an API-triggered broadcast, you can define the recipients manually. To define your recipients in the UI, Click Workflow. Click Trigger > Edit Conditions. Define your audience based on their attributes, segment membership, and/or message conditions. Then you can trigger the broadcast through the UI or an API call. Define recipients via API In your API call, you can define recipients using a complex set of filters:  You can nest and and or filters Nest filters to create complex filters where you have different sets of and and or criteria that a person must meet to receive your broadcast. And And The “And” filter is an array of objects, where each object represents criteria for your audience. Each object must be true for a person to receive the broadcast. You can nest other compound filters—or or not—to set up complex audience conditions. { "recipients": { "and": [ { "attribute": { "field": "first_name", "operator": "exists" } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } and array of [ objects ] Match all conditions to return results. not Returns results if a condition is false. While and/or support an array of items, not supports a single filter object. and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. or array of [ objects ] Returns results matching any conditions. Or Or The “Or” filter is an array of objects where each object represents criteria for your audience. At least one object in the array must be true for a person to receive the broadcast. You can nest other compound filters—and or not—to set up complex audience conditions. { "recipients": { "or": [ { "segment": { "id": 4 } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } or array of [ objects ] Match any condition to return results. and array of [ objects ] Returns results matching all conditions. not Returns results if a condition is false. While and/or support an array of items, not supports a single filter object. and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. Not Not A condition that, if true, excludes people from your audience. The not filter is an object, but can take a complex filter, like and or or. { "recipients": { "not": { "and": [ { "segment": { "id": 3 } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } } and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. Segment Segment A ID of a segment that people must belong to. { "recipients": { "segment": { "id": 3 } } } id integer The ID of the segment you want to return people from. Audience Audience Filter your audience based on an attribute value. In this case, you’ll provide: field: the name of the attribute operator: One of exists (true if a person has a value) or eq (true if a person matches a value). value: Required if you use the eq operator. The value a person’s attribute must equal (eq) for the condition to be true. { "recipients": { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } } field string Required The name of the attribute you want to filter against. operator string Required Determine how to evaluate criteria against the field—exists returns results if a person in the audience has the attribute; eq returns results if the audience has the attribute and the attribute has the value you specify.Accepted values:eq,exists value string The value you want to match for this attribute. You must include a value if you use the eq operator. The people you set must already exist in your workspace. An API-triggered broadcast cannot create new people. Using an ID or an email that doesn’t belong to a person in your workspace will produce errors. Add custom data for recipients You can send custom data for each member of your broadcast using per_user_data or data_file_url. You can match data to people by id or email. For example, using the example below, you could use {{trigger.voucher_code}} to provide a custom coupon or voucher for your individual broadcast recipients. per_user_data per_user_data Go to our API docs and select User Maps under Request Body to learn more. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "per_user_data": [ {"id":"wiley","data": {"voucher_code": "FESwYm"}}, {"email":"road@runner.net","data": {"voucher_code": "cYm6XJ"}} ] } data_file_url data_file_url Go to our API docs and select Data file URL under Request Body to learn more. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "data_file_url": "https://myFile.example.com" } Trigger via UI To trigger a broadcast through the UI, Go to Triggering Details. (Optional) Under Manual, you can include JSON data. Make sure you preview this data in your messages before you send. Under Manual, click Trigger a Broadcast. After you confirm, you will immediately trigger the broadcast. Note, you can’t schedule an API-triggered broadcast through our UI like you can a newsletter. Trigger via API In your API call, enter your broadcast ID. You can find it in the Triggering Details tab of an active broadcast. Define liquid variables with JSON In the UI or API, you can define liquid variables with JSON data. In the UI, go to Triggering Details. Or in your API call, add info to the data key. Make sure you preview with sample data in your message(s) before triggering the broadcast. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": "January 24, 2018", "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" } } Track API-triggered Broadcasts After you trigger your broadcast, you can track metrics. Go to the Overview tab and you’ll see when you last triggered your broadcast and metrics across all sends. In the Broadcasts tab, you’ll see a history of all the times you triggered the broadcast, and how many recipients there were for each trigger. Click into an individual broadcast to check its performance. Duplicate broadcasts Duplicate an API-triggered broadcast to create your next broadcast faster or to run them in parallel for experimentation. You can copy entire broadcasts to preserve settings like recipient conditions, goals, and message settings in addition to their workflows. You must activate the duplicate broadcast before anyone can enter it.  You can only duplicate API-triggered broadcasts within a workspace If you need to duplicate a broadcast across workspaces, you can copy workflow items, but not broadcast settings. To duplicate a broadcast: Click broadcast settings inline with the broadcast you want to copy. Click Duplicate. The duplicate uses the original broadcast’s name prefixed with [COPY] and appears at the top of the API-triggered broadcast tab. You need to activate it before people can enter the broadcast. Frequently Asked Questions What does “queueing” mean? When you send an API call to trigger a broadcast, we put that broadcast in a queue. When it reaches the front, we start creating and sending messages or storing drafts depending on the sending behavior you’ve selected. When it finishes, it drops out of the queue! Per workspace, you can’t have more than 5 broadcasts queued for the same API-triggered broadcast. If you try to send more, you’ll get an error. You can retry the failed requests after the previous broadcasts finish sending. Why can’t I add delays and/or time windows? With API-triggered broadcasts, you’re meant to send one message to many people quickly or immediately in response to an API call or a recent change. For that reason, we haven’t added delays or time windows to the feature. That said, if you have a specific use case that necessitates time windows or delays in your broadcast, please let us know! How many people can I message with a single API-triggered broadcast call? There is no limit. You can pre-define a segment within Customer.io with the profiles you wish to message, or host a data file containing all profiles you wish to have messaged. --- ## Personalize messages with trigger data URL: https://docs.customer.io/journeys/previewing-broadcast-data/ You can use [Liquid](/journeys/liquid-tag-list/) to personalize your broadcasts. Any part of message or email layout can contain liquid, including the subject line and from address. You'll personalize broadcasts using `trigger` data that Customer.io doesn't know about until you trigger your broadcast. You can provide example data when you compose your broadcast to preview your broadcast with realistic data that you expect to send in a trigger. How it works When you trigger a broadcast, you can provide additional data in your trigger that you can use to personalize your broadcast with 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}}.. This is very similar to the way you’d personalize event-triggered campaigns. But, in this case, you’ll reference your data using {{trigger.<data.property>}}. When you compose a broadcast, you can provide representative data to test your liquid and make sure that your message behaves the way that you expect it to. If you’ve triggered the broadcast before, you can select previous broadcast data from the dropdown for preview purposes only. Send custom data for all recipients You can send custom data in the data object when you trigger a broadcast. You can preview your data in messages by pasting it in the JSON Sample box when you compose your message. For example, imagine that you send messages on behalf of ACME incorporated, and you want to send a message to all coyote users who want alerts about roadrunners in their area. In this case, you might trigger a broadcast with data that looks like this: { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" } } You’d paste this into the JSON Sample box. { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" } This data becomes available in messages and wherever you can use 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}}.. So you could use the headline or date in a message using {{trigger.headline}} and {{trigger.date}}. Providing custom data per user You can send custom data for each member of your broadcast using per_user_data or data_file_url. You can match data to people by id or email. For example, using the example below, you could use {{trigger.voucher_code}} to provide a custom coupon or voucher for your individual broadcast recipients. per_user_data per_user_data Go to our API docs and select User Maps under Request Body to learn more. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "per_user_data": [ {"id":"wiley","data": {"voucher_code": "FESwYm"}}, {"email":"road@runner.net","data": {"voucher_code": "cYm6XJ"}} ] } data_file_url data_file_url Go to our API docs and select Data file URL under Request Body to learn more. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "data_file_url": "https://myFile.example.com" } --- ## Format API-Triggered Broadcasts URL: https://docs.customer.io/journeys/api-triggered-data-format/ When you trigger a broadcast, you can send data to personalize your message or override your audience. This page helps you understand the format of that data and how to set up complex recipient groups. Providing custom data You’ll send custom data using our API or our libraries. You can also see sample data in our Composer preview. Here’s a basic example of what some custom data might look like in your request. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": "January 24, 2018", "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" } } Overriding Recipients You can set the audience for an API-triggered broadcast in the UI—which is useful when your audience always meets the same conditions. But you might want to define a custom audience when you trigger your broadcast. For example, you might send “Breaking News” notifications to different groups of users, but the same content layout. We support a complex set of audience filters, so you can broadcast to a specific group of people. We’ve provided a few basic examples below.  You can nest and and or filters Nest filters to create complex filters where you have different sets of and and or criteria that a person must meet to receive your broadcast. And And The “And” filter is an array of objects, where each object represents criteria for your audience. Each object must be true for a person to receive the broadcast. You can nest other compound filters—or or not—to set up complex audience conditions. { "recipients": { "and": [ { "attribute": { "field": "first_name", "operator": "exists" } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } and array of [ objects ] Match all conditions to return results. not Returns results if a condition is false. While and/or support an array of items, not supports a single filter object. and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. or array of [ objects ] Returns results matching any conditions. Or Or The “Or” filter is an array of objects where each object represents criteria for your audience. At least one object in the array must be true for a person to receive the broadcast. You can nest other compound filters—and or not—to set up complex audience conditions. { "recipients": { "or": [ { "segment": { "id": 4 } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } or array of [ objects ] Match any condition to return results. and array of [ objects ] Returns results matching all conditions. not Returns results if a condition is false. While and/or support an array of items, not supports a single filter object. and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. Not Not A condition that, if true, excludes people from your audience. The not filter is an object, but can take a complex filter, like and or or. { "recipients": { "not": { "and": [ { "segment": { "id": 3 } }, { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } ] } } } and array of [ objects ] Match all conditions to return results. or array of [ objects ] Match any condition to return results. Segment Segment A ID of a segment that people must belong to. { "recipients": { "segment": { "id": 3 } } } id integer The ID of the segment you want to return people from. Audience Audience Filter your audience based on an attribute value. In this case, you’ll provide: field: the name of the attribute operator: One of exists (true if a person has a value) or eq (true if a person matches a value). value: Required if you use the eq operator. The value a person’s attribute must equal (eq) for the condition to be true. { "recipients": { "attribute": { "field": "likes_baseball", "operator": "eq", "value": true } } } field string Required The name of the attribute you want to filter against. operator string Required Determine how to evaluate criteria against the field—exists returns results if a person in the audience has the attribute; eq returns results if the audience has the attribute and the attribute has the value you specify.Accepted values:eq,exists value string The value you want to match for this attribute. You must include a value if you use the eq operator. Specify recipients with segments Find your segment ID You’ll need the numerical ID for each segment you’d like to target. This can be found in the UI by hovering over the segment info icon in the Segment Overview: Or on the individual segment page: Then, you can use it to define or override recipients with JSON that looks like this: { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": "January 24, 2018", "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "recipients": { "segment": { "id": 3 } } } Using multiple segment IDs Here, you’ll need to format a boolean expression of segment IDs in JSON, like this: "recipients": { "or": [ { "segment": { "id": 3 } }, { "segment": { "id": 4 } } ] } This would target people who belong to either segment 7 or 8. Use and if you’d like to target people in both segments. Specify recipients with attributes In addition to segment IDs, you can also specify or override recipients with attribute conditions. These accept two operators: eq and exists. Note that you’ll need to separate the operator and value into separate entries! { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": "January 24, 2018", "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "recipients": { "attribute": { "field": "interest", "operator": "eq", "value": "roadrunners" } } } If you need an is not equal or does not exist condition, use the not operator on an eq/exists attribute condition.. Here is an example with a not operator: { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": "January 24, 2018", "text": "We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "recipients": { "not": { "attribute": { "field": "interest", "operator": "eq", "value": "roadrunners" } } } } Combining segments and attributes Alternately, you can use a combination of segment and attribute conditions, using the general syntax above. Here’s an example of how to do that, with some simple data to go with it: { "data":{ "headline":"Roadrunner spotted in Albuquerque!", "date":"January 24, 2018", "text":"We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "recipients": { "and": [ { "segment": { "id": 3 } }, { "or": [ { "attribute": { "field": "interest", "operator": "eq", "value": "roadrunners" } }, { "attribute": { "field": "state", "operator": "eq", "value": "NM" } }, { "not":{ "attribute": { "field": "species", "operator": "eq", "value": "roadrunners" } } } ] } ] } } } Recipient List In some situations it might be desirable to specify a list of recipients. These can be provided either through a list of profile ids or a list of email addresses. Profile IDs The list of profile ids is provided as a JSON array in the attribute ids. You can send up to 10,000 ids in one API call. Sending more than that will result in an error is returned to the caller. If any of the profile ids in this array do not correspond to an existing profile’s id attribute in Customer.io then the trigger API returns an error to the caller unless the boolean attribute id_ignore_missing is set to true, in which case the missing ids are ignored. Here’s an example of how to do that, with some simple data to go with it: { "data":{ "headline":"Roadrunner spotted in Albuquerque!", "date":"January 24, 2018", "text":"We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "ids": ["wiley", "roadrunner", "acme"], "id_ignore_missing": true } Email Addresses The list of emails addresses is provided as a JSON array in the attribute emails. You can send up to 10,000 emails in one API call. Sending more than that will result in an error is returned to the caller. If any of the email addresses in this array do not correspond to an existing profile’s email attribute in Customer.io then the trigger API returns an error to the caller unless the boolean attribute email_ignore_missing is set to true, in which case missing emails are ignored. If the data that you are sending contains customers that might not exist in Customer.io application then this is the attribute to use. If any of the email addresses in this array corresponds to multiple profile ids the trigger API returns an error to the caller unless the boolean attribute email_add_duplicates is set to true, in which case all matching customers are added as recipients. Warning: be careful as this means that the same email address will be mailed multiple times. Here’s an example: { "data":{ "headline":"Roadrunner spotted in Albuquerque!", "date":"January 24, 2018", "text":"We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "emails": ["wiley@coyote.com", "road@runner.net", "support@acme.com"], "email_ignore_missing": true, "email_add_duplicates": true } To see the implication of email_add_duplicates and email_add_duplicates, for the sake of an example, let’s assume that your Customer.io account has multiple profiles that have the same value for their email attribute but different values for their id attribute. If the email_ignore_missing is set to true then emails you included that are missing in your Customer.io account will be ignored. If email_add_duplicates is set to true then multiple emails will be sent. Let’s say you have the following data in your Customer.io account: id: 1 email: wiley@coyote.com id: 2 email: wiley@coyote.com id: 3 email: john@coyote.com And you send the following JSON data as part of your API call: { "data":{ "headline":"Roadrunner spotted in Albuquerque!", "date":"January 24, 2018", "text":"We've received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "emails": ["wiley@coyote.com", "john@coyote.com", "matt@coyote.com"], "email_ignore_missing": true, "email_add_duplicates": true } In that case a total of three emails will be sent. Two to wiley@coyote.com and one to john@coyote.com. Whereas matt@coyote.com will be ignored. Including custom data If you want to specify a list of recipients but also include custom data for them (for example, a voucher code), you can include a per_user_data attribute (for up to 10,000 recipients) or a data_file_url attribute. Custom data included in this way is available in the {{trigger.<attribute_name>}} space when building your message content, in exactly the same way as data provided in the global data block specified above. If an attribute is specified in both the global block and in the custom block, the value from the custom block will be used for each customer for which it is provided. If per_user_data or data_file_url is specified, the recipients, ids, and emails fields must not be present in the API request. per_user_datashould contain an array of JSON objects where each object contains an identifier (id or email depending on your workspace settings) and data values. Below is an example using per_user_data.  IMPORTANT: Remove linebreaks and collapse per_user_data You must collapse/uglify your per_user_data to prevent errors, as in the example below. { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "per_user_data":[{"id":"wiley","data":{"voucher_code":"FESwYm"}},{"email":"road@runner.net","data":{"voucher_code":"cYm6XJ"}}] } For data_file_url, the URL provided must be an http or https URL. We recommend using an https server for hosting any files containing sensitive customer information. If the file server requires user/password authentication, the values for those must be provided in the URL, like this: https://user:pass@myserver.com/myfile. The URL must point at a plain text file containing JSON Line formatted data. Each line must be a JSON object in the format {"id":xxxx,"data":xxxx} or {"email":xxxx,"data":xxxx}. We will download and process the file from the URL, and progress for that process will be visible in the status, with the following new fields: found_per_user_data - whether per_user_data was found in the data file per_user_data_position - the position to which we have processed (in bytes) in the data file per_user_data_error_count - the number of errors recorded so far while processing the data file Here is an example using data_file_url: { "data": { "headline": "Roadrunner spotted in Albuquerque!", "date": 1511315635, "text": "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" }, "data_file_url": "https://s3.amazonaws.com/awesometown/upsell_runners.json" } The file at the URL should contain JSON Line (newline-delimited JSON) formatted data. For example, the contents of upsell_runners.json might look like this: {"id": "customer_123", "data": {"product": "widget", "price": 29.99}} {"id": "customer_456", "data": {"product": "gadget", "price": 19.99}} {"email": "road@runner.net", "data": {"voucher_code": "FAST50"}} Here’s what you’d see when checking the status for the trigger: { "id": "5-37", "campaign_id": 5, "created_at": 1552523326, "recipients_filter": "{\"segment\":{\"id\":80}}", "data": "{\n \"headline\": \"Roadrunner spotted in Albuquerque!\",\n \"date\": 1511315635,\n \"text\": \"We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!\"\n }", "processed_at": 1552523331, "recipients_count": 3000, "workflow_action_ids": [], 'per_user_data_position': 16100, 'found_per_user_data': True, 'per_user_data_error_count': 0 } If errors are encountered while processing the data file or the values from the per_user_data block, the GET API endpoint provides a way to page through the list of errors using the start and limit request parameters. If a request returns a non-zero next attribute, specifying that value as the start value in the next request will return the next page of errors. Example: { "errors": [ "line 1: couldn't parse json data", "line 2: couldn't parse json data", "line 3: couldn't parse json data", "line 4: couldn't parse json data", "line 5: couldn't parse json data", "line 6: couldn't parse json data", "the following ids are missing: coyottee, crocodile" ], "next": 0 } If there are no errors found while processing the file, the broadcast will start automatically as soon as processing of the file has been completed. If the data file or the per_user_data block may contain ids that are not tracked in Customer.io or emails that don’t correspond to profiles that are tracked in Customer.io, you may specify the id_ignore_missing or email_ignore_missing fields in the request to allow the broadcast to ignore the errors and allow the broadcast to continue. The email_add_duplicates flag may also be used. Once again, be careful with this option, as it means that the same person may receive multiple copies of the broadcast. --- ## Edit live API-triggered broadcasts URL: https://docs.customer.io/journeys/apitb-changes/ Learn how changes to a live API-triggered broadcast impact customers. Change the recipient segment(s) in the UI Your updated segment(s) will be used the next time the broadcast is triggered. Keep in mind, if you set the recipients in the UI but end up triggering through an API call, any recipients defined in the API call will override the UI setting. Make changes while the broadcast is queued to send When a broadcast is triggered via API, it’s queued while sending to all recipients. Any edits you make to the message while it’s queued for sending will apply right away. You can tell whether or not a send is in progress by clicking the bell icon in the top navigation bar and viewing your Tasks history. --- ## Common API-Triggered Broadcast errors URL: https://docs.customer.io/journeys/api-triggered-errors/ This page lists some common errors you might encounter when you trigger a broadcast, and troubleshooting options to help you overcome these errors. Method Not Allowed You might have misspelled the request URI. The URI should look like the following (with the broadcast/trigger :id): US region: https://api.customer.io/v1/api/campaigns/:id/triggers EU region: https://api-eu.customer.io/v1/api/campaigns/:id/triggers “status”:“400” { "errors":[ { "detail":"bad request (reference 01C4MWXZPAR1SHBSZT4ZMZ6KZV)", "status":"400" } ] } Reason: Issues with the JSON formatting of the request. This could be mismatched opening or closing brackets, missing commas or quotes, etc. “status”:“401” You’ve provided an incorrect or invalid bearer token. Check that your token provides access to the workspace containing your broadcast and has sufficient permissions to trigger a broadcast. { "errors":[ { "detail":"unauthorized", "status":"401" } ] “status”:“403” The URL or the credentials in your data_file_url might be incorrect. { "errors": [ "failed to download data file, server responded with code 403 Forbidden" ], "next": 0 } “status”:“404” Your Broadcast’s trigger id doesn’t exist. You can double-check it by looking at your broadcast’s Triggering Details tab. { "errors":[ { "detail":"not found (reference 01C4MWKQA9XTFNYR68QB7JJ0XA)", "status":"404" } ] } “status”:“422” A 422 error means that your broadcast trigger failed a validation check. You can look use the trigger ID from the error detail to look up more information about the failure. Some common errors include: Mistyped keys or values One or more people in the payload don’t exist Improper or missing request headers when sending a cURL request (-H "Content-Type: application/json”) You used ids, emails or recipients alongside per_user_data or data_file_url. You can only use one of these in your request. If you used per_user_data or data_file_url, email addresses in your request might resolve to more than one person. { "errors":[ { "detail":"recipients filter is not valid", "source":{ "pointer":"/data/attributes/recipients" }, "status":"422" } ] } { "errors": [ { "detail": "exactly one of \"recipients\", \"ids\", \"emails\", \"per_user_data\", or \"data_file_url\" must be provided", "source": { "pointer": "/data/attributes/recipients" }, "status": "422" } ] } Overriding missing or duplicate identifiers While broadcasts fail by default when they encounter missing or duplicate IDs or emails, you can override this behavior by setting flags in your request payload: email_add_duplicates: Set to true to allow broadcasts to send to people with duplicate emails. email_ignore_missing: Set to true to skip over any people in your audience who don’t have emails, and continue sending to the rest of your audience. id_ignore_missing: Set to true to skip over any people in your audience who don’t have IDs, and continue sending to the rest of your audience. Need help? If you’ve encountered a different error, or need more help fixing one the errors listed on this page, let us know! --- ## Getting started: key concepts URL: https://docs.customer.io/journeys/transactional-api/ Transactional messages are emails, SMS, or push notifications that your audience implicitly opts-into, like a transaction receipt or a password reset request. You can send transactional messages programmatically through Customer.io. This page contains basic information about our transactional messaging service and how it differs from marketing campaigns and messages. How it works Transactional messages are messages that your audience implicitly opts into—like purchase receipts, password reset requests, shipping updates, two-factor authentication codes, etc. Your audience expects to receive these messages even if they’ve opted out of marketing messages. That makes transactional different from marketing messages, which require explicit opt-in. And this difference isn’t just limited to Customer.io; it’s governed by laws like the CAN-SPAM Act and GDPR! When someone does something that requires a transactional message, you’ll call our transactional API to send them a message—like when they request a password reset or need a receipt for a purchase. When you call the API, you can include data that you want to populate in your message, like a password reset URL or the purchase data. sequenceDiagram actor a as your user participant b as your website participant c as Customer.io a->>b: request password reset b->>c: send transactional API request c->>c: generate delivery c->>a: send password reset request  Moving from another provider and need help? Try our Agency Partners page. Go to our Agency Partners page, click Get Matched, and fill out the form; we’ll pair you with an agency that can help you create a transactional migration plan. Use cases Our transactional APIs help you separate transactional and marketing messages, while being able to see and manage both kinds of messages. We don’t charge extra for this service! Common use cases for transactional messages include: Purchase receipts Registration confirmations (invites and opt-in confirmations) Password resets Two-factor authentication (2FA) codes One-time passwords (OTP) Trial expiration reminders Comment notifications Event reminders Shipping updates Account alerts and security notifications Support and feedback requests Transactional emails have their own IP Pool By default, we add email domains to Customer.io’s shared IP address pool to send emails. We manage and monitor multiple IP pools and remove domains that perform poorly to maintain high deliverability. We also maintain a separate, transactional IP address pool. This pool has even higher standards—stricter bounce and spam thresholds—than our default, shared IP pool. This ensures that your transactional messages achieve the highest deliverability. Only domains used with the transactional service can send emails over this IP pool. To use our transactional IP pool, you’ll need a separate sending domain from your parent domain dedicated to transactional sending. Once that domain is set up, you can request to have it added to the transactional IP pool from your email settings by selecting Show Records and navigating to the Mail Servers tab.  When we add a domain to the transactional pool, you can no longer use it to send campaigns or broadcasts. You can also request dedicated IP addresses or set up a custom SMTP mail server from your email settings. Identify your recipients When you trigger a transactional message, you’ll specify an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for a person—their id, email, or cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (depending on the identifiers allowed by your workspace). If a person matching the id or email in your request doesn’t exist, we’ll create a new person. This means that you can send transactional messages even when someone hasn’t signed up for your marketing messages. Transactional message “templates” When you create a transactional message in our UI, you’ll set a Trigger Name and we’ll generate a transactional_message_id. When you call the transactional API, you’ll use either of these values to tell Customer.io which transactional message you want to populate and send to your audience. For emails and push notifications, you can omit the message template and populate your own message body, subject, and from values at send time. But we recommend that you create transactional message templates for every type of message you plan to send (“Receipt”, “Password Reset”, etc), even if you want to populate a custom message at send time, because it helps you effectively track metrics for your transactional messages. Where do I find my transactional message ID? You can find the transactional_message_id in a few places: In the code sample in the Send Message step when you set up your message. In the code sample in the Overview tab after you set up your message. In the URL when you select a transactional message in the user interface. The number immediately following /transactional/ is the transactional_message_id. You can return a list of your transactional messages, each containing the id (short for transactional_message_id), from the transactional API. Transactional templates and uncategorized messages To set up a transactional email or push notification, you can either: Create a transactional template in Customer.io—a message that you’ll populate with data from an API call. Send your entire message body through the API. For transactional SMS and WhatsApp messages, you must use a template. We recommend that you use templates for three reasons: It’s easier to design a message in Customer.io than send your entire HTML payload through the API. It reduces the size of the payloads you need to store in your integration. Most importantly, it makes it easy to track engagement with your transactional messages. We aggregate metrics by template ID. If you send your entire message through our API, you won’t have a transactional_message_id, and we’ll track all your transactional messages as “uncategorized” messages! We aggregate metrics for uncategorized messages at the bottom of the transactional list, which can make it difficult for you to see how people respond to different types of transactional messages. Trigger data and content variables When using a transactional message template, you can personalize it like any other message using 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}}.—like {{customer.first_name}} to use a person’s first_name attribute. But you can also set values that you want to populate when you call our API using {{trigger.<message_data.property>}}. When you send a message using the transactional API, you can pass message_data to populate those variables. Below is an example “password reset” message. First we have an example API call with a passwordResetUrl. That populates the {{trigger.passwordResetUrl}} 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}}. in our example message. message=' { "to": "sarah@example.io", "transactional_message_id": 3, "message_data": { "first_name": "Sarah", "passwordResetURL": "https://www.example.io/password?token=12345" }, "identifiers": { "id":"1234" } } ' echo $message | curl -v https://api.customer.io/v1/send/email \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer APP-API-TOKEN' \ -d @- Write fallbacks for liquid in transactional messages Because transactional messages are important and time-sensitive, we’ll send messages to your customers even if they have 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}}. errors. That means you’ll want to set fallbacks and make sure that your messages render properly even if a person doesn’t have the data you expect. For example, imagine you write a message that uses people’s first names with Hi {{customer.first_name}}. If someone in your workspace doesn’t have a first name attribute, we won’t send a message to them. To handle situations where people haven’t given you their first name, you can add a default value to the liquid statement to set a fallback value for people who don’t have a first_name attribute. For example, Hi {{customer.first_name | default: "Buddy"}} will send “Hi Buddy” if a person doesn’t have a first name. When you draft your message, we also won’t know who your transactional audience is. This means that we can’t validate the attributes and other properties you reference in 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}}.. This is another reason to set fallbacks in your liquid statements.  Did you open your account before Nov 28, 2023? If you opened your Customer.io account before Nov 28, 2023, you may be using our legacy version of liquid. For our legacy versions of liquid, fallback statements are a little different. Learn more and check which version of liquid you’re on. Test transactional messages If you want to test liquid as you’re building your content, you can send a test message to your inbox or mobile device. From the message editor, add a test payload under Sample data. Add attributes and values for your liquid to pull in. Specifically, add the information you’d include in the message_data object of the trigger. For instance, if your transactional message sends a purchase receipt, you might include product information: { "product": "socks", "price_per_unit": 10.99, "quantity": 2 } Check that your liquid references the attributes in your sample data. Before After Click Send test. You can also trigger a transactional message for testing purposes through a client like Postman or by sending a cURL request from your terminal. Check out our App API for more info. Scheduling transactional messages You can use the send_at parameter in your transactional API request to schedule your message for up to 90 days in the future. This helps if customers sign up for time-sensitive transactional messages, so you don’t have to manually schedule your transactional API requests in your backend system. You might use send_at to send booking reminders as a person’s vacation rental or event approaches. Or you might let customers who expressed interest in a product before it’s released know exactly when it becomes available. Message retention and link tracking By default, we retain the content of the messages you send and track links in transactional messages. But you might not want to do this for transactional emails with sensitive information—personal customer information, password reset tokens, etc. For sensitive messages, you can enable the Protect sensitive data setting. This setting prevents Customer.io from storing the body of transactional messages. When you enable the Protect sensitive data setting, you also necessarily disable link tracking in your message because we don’t retain the message. Localize transactional messages When you add content to your message, you can click Add language to select the languages you want to support. Then you can populate your languages, and we’ll automatically send your audience the language matching their language attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages..  You can override languages when you send your message If you don’t manage your audience’s language preferences in Customer.io, or you need to override your audience’s language preference for any reason, you can set a language property in your transactional message request. Learn more To take advantage of our localization feature, you must have set up an attribute to capture your audience’s language preference. See our localization section for more information about setting up multi-language messages. When you send your message, we’ll match your audience’s language attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.—their preferred language—with the languages in your transactional message. If a person’s language attribute matches a language in your transactional message, we’ll send them the appropriate language. If their language attribute does not match a language in your template, they’ll receive the default message. graph LR A[Person completes a transaction] --> B[Send transactional API call] B --> D{Does a person's language attribute match a message?} D -->|no| H[Person gets default message] D -->|yes, lang=es| E[Person gets Spanish message] D -->|yes, lang=fr| F[Person gets French message] D -->|yes, lang=de| G[Person gets German message] Overriding your audience’s language If you don’t manage your audience’s language preferences in Customer.io, or you need to override your audience’s language preference for any reason, you can set a language property in your transactional message request. This value represents the language variant that you want to send to the recipient. If the language doesn’t match one of your message’s languages, we’ll use the recipient’s language attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. If the language doesn’t match one of your message’s languages, and your audience doesn’t have a language attribute that matches one of your message’s languages, we’ll send the default message. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "transactional_message_id": 44, "to": "cool.person@example.com", "from": "override-templated-address@example.com", "subject": "Did you really login from a new location?", "language": "es", "identifiers": { "id": 12345 }, "message_data": { "password_reset_token": "abcde-12345-fghij-d888", "account_id": "123dj" }, "bcc": "bcc@example.com", "disable_message_retention": false, "send_to_unsubscribed": true, "tracked": true, "queue_draft": false, "disable_css_preprocessing": true } Organize your transactional messages You can organize your transactional messages by assigning tags, a way of grouping related automations together. You can use tags to group not just transactional messages, but campaigns, broadcasts, and segments too. Create, assign, edit, and delete as many tags as you need in your workspace. --- ## Set up a transactional email URL: https://docs.customer.io/journeys/transactional-email/ Transactional messages are email or push notifications that your audience implicitly opts-into, like a transaction receipt or a password reset request. You can send transactional messages programmatically through Customer.io.  New to transactional messaging? Check out our getting started section to learn more about transactional concepts and how our transactional API works. Before you begin Before you can send transactional emails, you need to: Confirm your registration email and verify your account. Authenticate your sending domain. If you configured a custom SMTP server, contact us to manually authenticate your sending domain. Get your app API key. This is the bearer token that you’ll use when calling the transactional API. app API keys are not the same as the Track API keys that you use to update profiles and trigger events. Customer.io does not store app API keys (only a hashed version) and you can restrict app API keys to specific IP addresses for extra security. We also recommend that you use a different, specialized domain or subdomain for transactional messages. A sending domain is the domain of the “From email address” when you send emails, and you can request that we add your transactional domain to our specialized, transactional IP pool. Email providers like Gmail monitor the sending domain for unusual behavior and spam complaints. Separating your transactional domain from your marketing domain—something like marketing.example.com and transactional.example.com—prevents your marketing messages from affecting your critical transactional messages. Learn more about domain reputation. Create a transactional email  Try our Postman collection! You can use our Postman collection and associated environment to get started with the Customer.io API. Our environment is based on our US endpoints; if you’re in our EU region, you’ll need to add -eu to track_api_url and app_api_url variables. Go to the Transactional page and click Send your first message or Create message—depending on whether there are already transactional messages in your workspace. Name your message and provide a description. These fields help your team members understand what kind of message this is (like “Password Reset Instructions”). You can also use the Name of your message instead of the transactional_message_id when you send your message. Click Add Content and set up your message. You’ll choose your editor or start from an existing email. When you build your email, you can personalize messages using attributes ({{customer.<attribute>}}) or API trigger data ({{trigger.<data-object-property>}}) to customize your message. Configure your message settings. Send to unsubscribed people? Unsubscribed people probably still want to receive your important transactional messages. The FTC provides guidelines about what qualifies as a transactional message, including what to do for messages that combine transactional and marketing content. Enable open and link tracking? Enable this setting if you need to know if people open or click links in your transactional messages. Protect sensitive data by disabling message retention? This setting prevents Customer.io from retaining your message content in delivery history and associated API calls. You might want to do this to conceal sensitive content, like password reset tokens. Queue messages as drafts? This setting generates a draft for every message you trigger, rather than sending them automatically. You can review these messages under the “Drafts” tab and decide whether to send or delete them. Set a Trigger Name: This is a friendly name for your message that you can use instead of the transactional_message_id when you send your message. It may help make your integration more human-readable if you use triggers that represent the kinds of messages you send—like password reset or order confirmation. To complete the setup, you need to call the API and trigger a message. If you’re not yet ready to send a message directly from your code, you can use an HTTP client like Postman or send a cURL request from your terminal to test your message and complete the setup process. Examples and API parameters Below are examples of transactional emails. We’ve provided a basic payload and examples for cURL, our Node.JS SDK, and our Python SDK. We’ve also provided a list of parameters for transactional message payloads. Your payload changes based on whether you reference a transactional_message_id (a template) or not. See our REST API documentation for more information. Basic Payload Basic Payload { "transactional_message_id": 44, "to": "cool.person@example.com", "subject": "Did you really login from a new location?", "identifiers": { "email": "cool.person@example.com" }, "message_data": { "password_reset_token": "abcde-12345-fghij-d888", "account_id": "123dj" }, "send_to_unsubscribed": true, "tracked": true, "disable_css_preprocessing": true } Node.js Node.js const { APIClient, SendPushRequest, RegionUS } = require("customerio-node"); const api = new APIClient('app-key', { region: RegionUS }); const request = new SendPushRequest({ to: "person@example.com", transactional_message_id: "44", message_data: { password_reset_token: "abcde-12345-fghij-d888", account_id: "123dj" }, identifiers: { id: "2", }, }); api.sendPush(request) .then(res => console.log(res)) .catch(err => console.log(err.statusCode, err.message)) Python Python from customerio import APIClient, Regions, SendPushRequest client = APIClient("your API key", region=Regions.US) request = SendPushRequest( transactional_message_id="3", identifiers={ "id": "2", } ) response = client.send_push(request) print(response) cURL cURL curl --request POST \ --url https://api.customer.io/v1/send/email \ --header 'content-type: application/json' \ --data '{ "transactional_message_id": 44, "to": "cool.person@example.com", "from": "override-templated-address@example.com", "subject": "Did you really login from a new location?", "identifiers": { "email": "cool.person@example.com" }, "message_data": { "password_reset_token": "abcde-12345-fghij-d888", "account_id": "123dj" }, "bcc": "bcc@example.com", "disable_message_retention": false, "send_to_unsubscribed": true, "tracked": true, "queue_draft": false, "disable_css_preprocessing": true }' transactional_message_id Required The transactional message template that you want to use for your message. You can call the template by its numerical ID or by the Trigger Name that you assigned the template (case insensitive). integer The ID of the transactional message you want to send. string The name of trigger for the transactional message you want to send; you set the trigger name in the Configure Settings step when setting up your message. This is case insensitive. body string The HTML body of your message. This overrides the body of the transactional template (referenced by transactional_message_id). If you send an AMP-enabled email (with body_amp), and the recipient’s email client doesn’t support AMP, this is the fallback email. body_plain string The plaintext body of your message. This overrides the body of your transactional template (referenced by transactional_message_id). from string The address that your email is from. This address must be verified by Customer.io. This overrides the from address set within the transactional template (referenced by transactional_message_id). You can include a display/friendly name in your from address, but we recommend that you use quotation marks around the friendly name to avoid potential issues with special characters, e.g. \"Person\" <person@example.com>. language string Overrides language preferences for the person you want to send your transactional message to. Use one of our supported two- or four-letter language codes. subject string The subject line for your message. This overrides the subject of the transactional template (referenced by transactional_message_id). body string Required The body of your message. from string Required The address that your email is from. This address must be verified by Customer.io. You can include a display/friendly name in your from address in the format Person <person@example.com>. subject string Required The subject line for your message. Update the content of your email You can update the contents of your message through our user interface or API. We’ve exposed an API endpoint so you can manage your message contents programmatically. This request takes a body, which represents the complete HTML content of your message. You’ll reference the message you want to update by transactional_id and content_id. You can find both in the URL when you look at the content of a message in the format https://fly.customer.io/journeys/env/last/composer/transactional/:transactional_message_id/templates/:content_id. For example, if I look at a transactional message with this URL: https://fly.customer.io/journeys/env/last/composer/transactional/3/templates/139, then the transactional_id is 3 and the content_id is 139. curl --request PUT \ --url https://api.customer.io/v1/transactional/{transactional_id}/content/{content_id} \ --header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \ --header 'content-type: application/json' \ --data '{"body":"string"}'  Did you just update a snippet? If you just updated a snippet used in your messages, make sure you wait a couple of minutes before activating your workflow to ensure all updates appear in your delivered messages. --- ## Set up a transactional push URL: https://docs.customer.io/journeys/transactional-push/ Transactional messages are email or push notifications that your audience implicitly opts-into, like a transaction receipt or a password reset request. You can send transactional messages programmatically through Customer.io. Before you begin Before you can send transactional push notifications you need to: Enable Push Notifications in your workspace for Android and/or iOS. Set up your app to receive push notifications from Customer.io. We strongly recommend that you use our SDKs. If this isn’t an option, you can follow the instructions here to configure push without our SDKs. Create a transactional push notification  Try our Postman collection! You can use our Postman collection and associated environment to get started with the Customer.io API. Our environment is based on our US endpoints; if you’re in our EU region, you’ll need to add -eu to track_api_url and app_api_url variables. Go to the Transactional page and click Send your first message or Create message—depending on whether there are already transactional messages in your workspace. Name your message and provide a description. These fields help your team members understand what kind of message this is (like “Password Reset Instructions”). You can also use the Name of your message instead of the transactional_message_id when you send your message. Click Next: Add Content to go to the next step. Select Push to create a transactional push notification. Click Add Content and compose your push notification. Add images, deeplinks and custom data through the editor, or provide these values in the API request. You can also use attributes (customer.<attribute>) or trigger data (trigger.<data-object-property>) to customize your message.  Need to track personalized links? If you need to track personalized links, use the cio_link liquid tag. This helps you group and gather metrics for links that are different for each person—like password reset links, customer dashboards, or product recommendations. Example: {% cio_link url:"https://mydomain.com?token=123abc" %}. If you don’t use this tag, we’ll track each URL independently, which might make it difficult to gather metrics for a links in your push notifications. Configure your message settings. Send to unsubscribed people? Unsubscribed people usually still want to receive your important transactional messages. Protect sensitive data by disabling message retention? This setting prevents Customer.io from retaining your message content in delivery history and associated API calls. You might want to do this to conceal sensitive content, like password reset tokens. Queue messages as drafts? This setting generates a draft for every message you trigger, rather than sending them automatically. You can review these messages under the “Drafts” tab and decide whether to send or delete them. Set a Trigger Name: This is a friendly name for your message that you can use instead of the transactional_message_id when you send your message. It may help make your integration more human-readable if you use triggers that represent the kinds of messages you send—like password reset or order confirmation. To complete the setup, you need to call the API and trigger a message. If you’re not yet ready to send a message directly from your code, you can use an HTTP client like Postman or send a cURL request from your terminal to test your message and complete the setup process.  Did you just update a snippet? If you just updated a snippet used in your messages, make sure you wait a couple of minutes before activating your workflow to ensure all updates appear in your delivered messages. Examples and API parameters Below are examples of transactional push notifications. We’ve provided a basic payload and examples for cURL, our Node.JS SDK, and our Python SDK. Below the examples, you’ll find a list of parameters for transactional push payloads. Your payload changes based on whether you reference a transactional_message_id (a template) or not. See our REST API documentation for more information. Basic Payload Basic Payload { "transactional_message_id": 44, "title": "Did you really login from a new location?", "identifiers": { "email": "person@example.com" }, "message_data": { "password_reset_token": "abcde-12345-fghij-d888", "account_id": "123dj" } } Node.js Node.js const { APIClient, SendPushRequest, RegionUS } = require("customerio-node"); const api = new APIClient('app-key', { region: RegionUS }); const request = new SendPushRequest({ to: "person@example.com", transactional_message_id: "44", message_data: { password_reset_token: "abcde-12345-fghij-d888", account_id: "123dj" }, identifiers: { id: "2", }, }); api.sendPush(request) .then(res => console.log(res)) .catch(err => console.log(err.statusCode, err.message)) Python Python from customerio import APIClient, Regions, SendPushRequest client = APIClient("your API key", region=Regions.US) request = SendPushRequest( transactional_message_id="3", identifiers={ "id": "2", } ) response = client.send_push(request) print(response) cURL cURL curl --request POST \ --url https://api.customer.io/v1/send/push \ --header 'content-type: application/json' \ --data '{"transactional_message_id":44,"title":"Did you really login from a new location?","identifiers":{"id":12345},"message_data":{"password_reset_token":"abcde-12345-fghij-d888","account_id":"123dj"}}' transactional_message_id Required The transactional message template that you want to use for your message. You can call the template by its numerical ID or by the Trigger Name that you assigned the template (case insensitive). integer The ID of the transactional message you want to send. string The name of trigger for the transactional message you want to send; you set the trigger name in the Configure Settings step when setting up your message. This is case insensitive. --- ## Set up a transactional SMS URL: https://docs.customer.io/journeys/transactional-sms/ Transactional SMS or WhatsApp messages are text messages that your audience implicitly opts-into, like order confirmations, password reset codes, or account verification messages. You can send transactional SMS messages programmatically through Customer.io.  New to transactional messaging? Check out our getting started section to learn more about transactional concepts and how our transactional API works. Before you begin Before you can send transactional SMS messages, you need to: Set up your Twilio account and enable SMS in your workspace. Get your app API key. This is the bearer token that you’ll use when you call the transactional API to trigger a message. We also recommend that you use different phone numbers for transactional and marketing SMS messages. This helps maintain deliverability and ensures your critical transactional messages aren’t affected by marketing message reputation. Create a transactional SMS Unlike transactional emails and push notifications, you cannot send a transactional SMS or WhatsApp message without a template. You must use a transactional_message_id when you send an SMS and you cannot override the body or image URL of your message at send time. Go to the Transactional page and click Send your first message or Create message—depending on whether there are already transactional messages in your workspace. Give your message a Name and a Description and then click Next: Add Content. The name and description help your team members understand what kind of message this is (like “Two-Factor Authentication” or “Password Reset Code”). Select SMS and click Add Content. Draft your message. If you see an error in the upper-right when you try to write your message, it’s likely that you need to select a person in your Sample data who has a phone attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. Selecting a representative sample person also helps you personalize your message. You can personalize messages using attributes ({{customer.<attribute>}}) or data from your transactional message’s message_data key ( in the format {{trigger.<data-object-property>}}). When you’re done writing your message, click Save Changes and then click Next: Configure Settings. Configure your message settings. In general, we recommend that you use our defaults and that you Set a trigger name so that it’s easier to send your message later. Send to unsubscribed people? Unsubscribed people probably still want to receive your important transactional messages like order confirmations and password resets. Enable link tracking? By default, we’ll track clicks on links in your transactional SMS messages. Disable this setting if you don’t use our link shortening feature or tracked links (which are longer than normal links) will likely exceed the 160 character limit for SMS messages. Protect sensitive data by disabling message retention? This setting prevents Customer.io from retaining your message content. You might want to do this to conceal sensitive messages, like password reset codes or verification tokens. Queue messages as drafts? This setting generates a draft for every message you trigger rather than sending them automatically. You can review these messages under the Drafts tab and decide whether to send or delete them. Set a Trigger Name: This is a friendly name for your message that you can use instead of the transactional_message_id when you send your message. It may help make your integration more human-readable if you use triggers that represent the kinds of messages you send—like password_reset or order_confirmation. Now you’re ready to send your message. You can adapt the code sample on the Send Message screen to your code—wherever you intend to trigger transactional messages—or use our API directly. (Optional) Even if you don’t want to wire up your integration completely, we recommend that you call our API and send a test message to make sure that your message works the way you expect. You can even use an HTTP client like Postman or send a cURL request from your terminal to test your message and complete the setup process.  Try our Postman collection! You can use our Postman collection and associated environment to get started with the Customer.io API. Our environment is based on our US endpoints; if you’re in our EU region, you’ll need to add -eu to track_api_url and app_api_url variables. The to parameter: sending your message When you send a transactional SMS or WhatsApp message, you must pass the to parameter in your request so we know who to send the message to. This value can be either a phone number in E.164 format (e.g., +15551234567) or a customer attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in 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}}. syntax, like {{customer.phone}}. Examples and API parameters Below are examples of transactional SMS messages. We’ve provided a basic payload and examples for cURL, our Node.JS SDK, and our Python SDK. We’ve also provided a list of parameters for transactional SMS payloads. Your payload changes based on whether you reference a transactional_message_id (a template) or not. See our REST API documentation for more information. Basic Payload Basic Payload { "transactional_message_id": "confirmation_code", "identifiers": { "id": "123456" }, "message_data": { "confirmation_code": "123456", "account_name": "Jane Doe" } } Node.js Node.js const { APIClient, SendSMSRequest } = require("customerio-node"); const api = new APIClient('app-key'); const request = new SendSMSRequest({ transactional_message_id: "confirmation_code", identifiers: { id: "123456" }, message_data: { confirmation_code: "123456", account_name: "Jane Doe" }, }); api.sendSMS(request) .then(res => console.log(res)) .catch(err => console.log(err.statusCode, err.message)) Python Python from customerio import APIClient, SendSMSRequest, CustomerIOException client = APIClient("your API key") request = SendSMSRequest( transactional_message_id="confirmation_code", identifiers={ "id": "123456" }, message_data={ "confirmation_code": "123456", "account_name": "Jane Doe" } ) try: api.send_sms(request) except CustomerIOException as e: print("error: ", e) Go Go import "github.com/customerio/go-customerio" client := customerio.NewAPIClient(""); request := customerio.SendSmsRequest{ TransactionalMessageID: "sms_test", Identifiers: map[string]string{ "id": "123456", }, } body, err := client.SendSms(context.Background(), &request) if err != nil { fmt.Println(err) } fmt.Println(body) Ruby Ruby require "customerio" client = Customerio::APIClient.new("") request = Customerio::SendSmsRequest.new( transactional_message_id: "sms_test", identifiers: { id: "123456", }, ) begin response = client.send_sms(request) puts response rescue Customerio::InvalidResponse => e puts e.message, e.code, e.response end cURL cURL curl --request POST \ --url https://api.customer.io/v1/send/sms \ --header 'Authorization: Bearer <YOUR-APP-API-KEY>' \ --header 'content-type: application/json' \ --data '{ "transactional_message_id": "confirmation_code", "identifiers": { "id": "123456" }, "message_data": { "confirmation_code": "123456", "account_name": "Jane Doe" } }' transactional_message_id Required The transactional message template that you want to use for your message. You can call the template by its numerical ID or by the Trigger Name that you assigned the template (case insensitive). integer The ID of the transactional message you want to send. string The name of trigger for the transactional message you want to send; you set the trigger name in the Configure Settings step when setting up your message. This is case insensitive. from string The phone number or sender ID that your SMS is from. This must be a verified phone number in your Twilio account. This overrides the from address set within the transactional template (referenced by transactional_message_id). Phone numbers must be in E.164 format (e.g., +15551234567). language string Overrides language preferences for the person you want to send your transactional message to. Use one of our supported two- or four-letter language codes. to string Required The phone number you want to send your SMS to. Phone numbers must be in E.164 format (e.g., +15551234567), but you can also use liquid syntax if you store users phone numbers as attributes; you don’t have to pass an E.164 formatted phone number in the call. --- ## Transactional examples URL: https://docs.customer.io/journeys/transactional-api-examples/ This page contains some example requests to help get you started with our transactional API. Send an email using a transactional message template To send a message using a transactional message template, you need the following: transactional_message_id: This is found in the code examples when setting up a transactional message. It’s also the id listed in the URL of a transactional message. message_data: A list of data that you want to pass into your message using 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}}.. Your API Key: This is how you’ll authenticate with Customer.io’s API. You can find or create your App API key from your Account Settings. Recipient email - The email address for the to field, and identifiers.id, identifiers.email, or identifiers.cio_id: a unique identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for the recipient, so your message is associated with the correct person. Using the data above, you can trigger a message with a request like the one below. This example is for a transactional message that includes two variables: first_name and passwordResetURL. message=' { "to": "Sarah <sarah@example.io>", "transactional_message_id": 3, "message_data": { "first_name": "Sarah", "passwordResetURL": "https://www.example.io/password?token=12345" }, "identifiers": { "id":"sf3sd" } } ' echo $message | curl -v https://api.customer.io/v1/send/email \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer APP-API-TOKEN' \ -d @- Send an email without a transactional template To send a simple request that doesn’t use a transactional template, you must provide all of your message information—to, from, subject, body, and one of identifiers.id, identifiers.email, or identifiers.cio_id—in the request. message=' { "to": "Sarah <sarah@example.io>", "from":"win@customer.io", "subject":"Reset your password", "body":"Hello Sarah, you or someone in your team account has requested a password reset on your behalf. Reset your password by clicking <a href='https://www.example.io/password?token=12345'>here</a>.</p>", "identifiers": { "id":"d34sh" } } ' echo $message | curl -v https://api.customer.io/v1/send/email \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer APP-API-TOKEN' \ -d @-  Messages sent without a transactional_message_id are ‘uncategorized’ Any messages you send without a transactional_message_id are grouped together in the All Uncategorized Messages bucket in your workspace. We strongly recommend creating transactional message templates, so that it’s easier to manage your messages and to separate reporting metrics for your different types of transactional messages. Including an attachment  Include PDF attachments from the asset library While you can encode attachment and send them directly to people, you can also host PDFs in our asset library and link to them in your messages. This can save you the trouble of encoding and sending files directly. You can include base64-encoded attachments in your request, like calendar invites, in the format: {"filename": "base64-encoded-file-body"}. Total attachments size, across all attachments for the message, must be under 2MB. For example, here’s the text body of a simple calendar invite (example_invite.ics): BEGIN:VCALENDAR VERSION:2.0 PRODID:http://www.icalmaker.com BEGIN:VEVENT UID:http://www.icalmaker.com/event/69f018d0-a2b1-44c0-ac66-a28e8c493a6b DTSTAMP:20201113T220919Z DTSTART:20230504T113000Z DTEND:20230504T123000Z SUMMARY:Customer.io Calendar Example LOCATION:921 SW Washington St., Suite 820, Portland, OR 97205 DESCRIPTION:Learn more about sending calendar invites as attachments at https://customer.io/transactional-api. END:VEVENT END:VCALENDAR You can base64-encode the invite and include it in your request like this. message=' { "to": "Sandra <sandra@example.io>", "from":"win@customer.io", "subject":"Registration Confirmation", "body":"Attached is an invite to add to your calendar.", "identifiers": {"id":"e5zs8"}, "attachments":{"example_invite.ics": "QkVHSU46VkNBTEVOREFSClZFUlNJT046Mi4wClBST0RJRDpodHRwOi8vd3d3LmljYWxtYWtlci5jb20KQkVHSU46VkVWRU5UClVJRDpodHRwOi8vd3d3LmljYWxtYWtlci5jb20vZXZlbnQvNjlmMDE4ZDAtYTJiMS00NGMwLWFjNjYtYTI4ZThjNDkzYTZiCkRUU1RBTVA6MjAyMDExMTNUMjIwOTE5WgpEVFNUQVJUOjIwMjMwNTA0VDExMzAwMFoKRFRFTkQ6MjAyMzA1MDRUMTIzMDAwWgpTVU1NQVJZOkN1c3RvbWVyLmlvIENhbGVuZGFyIEV4YW1wbGUKTE9DQVRJT046OTIxIFNXIFdhc2hpbmd0b24gU3QuLCBTdWl0ZSA4MjAsIFBvcnRsYW5kLCBPUiA5NzIwNQpERVNDUklQVElPTjpMZWFybiBtb3JlIGFib3V0IHNlbmRpbmcgY2FsZW5kYXIgaW52aXRlcyBhcyBhdHRhY2htZW50cyBhdCBodHRwczovL2N1c3RvbWVyLmlvL3RyYW5zYWN0aW9uYWwtYXBpLiAKRU5EOlZFVkVOVApFTkQ6VkNBTEVOREFS"} } ' echo $message | curl -v https://api.customer.io/v1/send/email \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer APP-API-TOKEN' \ -d @- The request results in a delivery with a calendar invite, which looks like this in Gmail: Send a push notification using a transactional message template To send a transactional push notification, you need the following: transactional_message_id: This is found in the code examples when setting up a transactional message. It’s also the id listed in the URL of a transactional message. message_data: A list of data that you want to pass into your message using 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}}.. Your API Key: This is how you’ll authenticate with Customer.io’s API. You can find or create your App API key from your Account Settings. identifiers.id, identifiers.email, or identifiers.cio_id: a unique identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for the recipient, so your message is associated with the correct person. Using the data above, you can trigger a message with a request like the one below. This example is for a transactional message that sends a push notification to the recipient’s last used device and includes two variables: first_name and passwordResetToken. message=' { "to": "last_used", "transactional_message_id": 3, "message_data": { "first_name": "Sarah", "passwordResetToken": "token=12345" }, "identifiers": { "id":"sf3sd" } } ' echo $message | curl -v https://api.customer.io/v1/send/push \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer APP-API-TOKEN' \ -d @- Need help? If you have another use case in mind, or need help applying one of these examples, let us know! --- ## Common transactional API errors URL: https://docs.customer.io/journeys/transactional-api-common-api-errors/ Successful transactional requests return *200 OK*. If you get another response, something probably went wrong! This page can help you better understand and troubleshoot errors you receive from the transactional API. Successful requests A successful transactional API request returns a 200 OK and looks like this: status: 200 OK { "delivery_id": "A1C2E3jdieDks7zXdyz8XCFn1888", "queued_at": 1604977406 } status: 405 Method Not Allowed Possible misspelling of the request URI. The URI is https://api.customer.io/v1/send/email or https://api.customer.io/v1/send/push. status: 403 Forbidden You probably need to take some additional steps to ensure that you’re authorized to send transactional messages. Error Message Resolution Account owner’s email address is not confirmed. Confirm your email address (or the email of the person who signed up for the Customer.io account) to send messages. Look for the confirmation email in the inbox that you used when signin-up, or log in to trigger another email confirmation. Account is not yet approved for sending. Verify your account to send messages. Learn more about why we verify accounts. Workspace is set to “Do Not Send.” Update your Workspace Settings to send messages normally. status: 401 Unauthorized Invalid App API key. Check that your API key is correct and assigned to the correct workspace by visiting your Account API Credentials page. You may also have restricted your APP API keys to specific IP addresses. status: 400 Bad Request An invalid request is typically caused by missing, malformed, or invalid data. See below for common errors and fixes. Error Message Resolution Request entity must be JSON encoded and not exceed 2048 KB. Check that your request is less than 2MB and is formatted correctly. Extra spaces, missing commas, and extra/missing quotation marks may all cause the request to fail. “from” address domain has not been verified. Verify your domain to send a message using the specified “from” address. “transactional_message_id” not found. Check that you’re using a valid transactional_message_id in your workspace. Missing required field “to” and “identifiers” are required fields. “from”, “subject”, and “body” are not required when using a transactional_message_id in your request and the message template has these values. Invalid email address in field Email addresses in “to”, “from”, “bcc”, and “reply-to” must be properly formatted. Use angled brackets and quote the “name” portion of an email address like "\"Name\" <test@example.com>" to add a display name to an email address and avoid problems that might arise from special characters. Use commas to separate multiple email addresses. Limit of 15 recipients exceeded. A single request can have up to 15 recipients across both “to” and “bcc” fields. Invalid format in field “message_data” Check that the “message_data” is formatted correctly. Extra spaces, missing commas, and extra/missing quotation marks may cause the request to fail. “Header” is a reserved header. Reserved headers cannot be overwritten. See the full list of denylisted headers. Custom headers must be strings and not contain any non-ASCII characters. Headers must be comma-separated and consist of only ASCII characters. Missing filename or content for attachment. Every attachment must have a filename and corresponding base64-encoded content. Forbidden file type We support a limited set of file types as attachments. See full list of supported file types. Attachment must be base64-encoded Attachments must be base64-encoded. See examples here. Total attachment size exceeds 2048KB limit. Total attachments cannot exceed 2MB in size. Invalid custom_payload or custom_data format. The value in the iOS and/or Android custom_payload and custom_data fields must be a string. View in Browser links do not work “View in browser” links do not work with Transactional messages that have Protect sensitive data enabled. We don’t retain protected messages, and cannot retrieve them. Need help? If you’ve encountered a different error, or need more help fixing one of these, please let us know! --- ## Frequently Asked Questions URL: https://docs.customer.io/journeys/transactional-faq/ This page contains answers to frequently asked questions about our transactional API.  Is your question not answered here? If you have any questions or feedback not covered on this page or in our transactional API documentation, let us know at product@customer.io! Does the transactional API cost extra? No! You can start sending transactional messages immediately alongside your other messages. When should I use the ‘disable message retention’ setting? You probably want to disable message retention to concealing sensitive content, like password reset tokens. This setting prevents us from retaining and showing the contents of messages when you view sent deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. or when you retrieve messages via API. Instead, you’ll see this: When should I use the transactional API vs event-triggered campaigns? A transactional message is typically a single message that a person has implicitly requests—even if they’ve unsubscribed from your marketing messages. These are things like: Receipts Password reset requests Account alerts For emails that require multiple messages (like a double-opt in) or branching (e.g. multiple channels or A/B testing), you should using event-triggered campaigns. When should I use the transactional API vs API-triggered Broadcasts? API triggered broadcasts are optimized for sends that go to many people at a time (ie marketing blasts). These broadcasts can be configured to send to an entire segment(s) at a time, and are thus limited to a rate of 1 request/10 seconds, with dedicated messaging queues to manage the high volume. The transactional API is optimized to send transactional messages to one recipient at a time (ie order confirmations), and should not be used to send marketing blasts. They have a limit of 15 total recipients across To and BCC fields, a higher rate limit of 100 requests/second, and also have dedicated sending lanes to ensure a quick delivery. How do I send a transactional message supporting multiple languages? All you have to do is set your language attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and then you can add languages to your transactional message template. When you send a message, we’ll match your audience’s attribute to the languages in your template. If a person’s attribute matches one of the languages, they’ll get the appropriate localization; if they don’t, they’ll get the default message. Learn more about localized transactional messages. How many people can I message within a single transactional API call? A single API request can contain a maximum of 15 recipients. This includes all recipients across both the To and BCC fields. How do I use the transactional API with a custom SMTP server? If you have configured a custom SMTP server and wish to use an unauthenticated sending domain, you’ll need to contact us at win@customer.io. We’ll manually authenticate the record after confirming that you own the sending domain. Can I send transactional messages across multiple channels? We currently support transactional messages for email, SMS, and push notification channels. If you want to send “transactional” messages to another channel, you can use an event-triggered campaign, potentially with branches. Why can’t I CC someone on an email? With Customer.io’s profile-based messaging approach, we don’t include Carbon Copy (CC) options across the platform because of potential complications in open and link tracking. If you truly need to CC someone on a transactional message, add additional addresses to the TO or BCC fields. Can I A/B test transactional messages? No. If you need to run an A/B test, try sending an event-triggered campaign. Can I use the transactional API to send SMS messages? Yes, you can send transactional SMS messages. Transactional SMS helps you send two-factor authentication codes, one-time passwords, account alerts, and other critical messages that your customers expect to receive even if they’ve unsubscribed from marketing messages. Can I use the transactional API to send bulk marketing sends? No. The transactional API is not optimized for bulk marketing sends, nor should you use our transactional IP pool or your transactional domain to send marketing messages. Use API-triggered broadcasts instead. What attachment file types are forbidden? bat, bin, chm, com, cpl, crt, exe, hlp, hta, inf, ins, isp, jse, lnk, mdb, msc, msi, msp, mst, pcd, pif, reg, scr, sct, shs, vbe, vba, vbs, wsf, wsh, wsl What headers are reserved and cannot be overwritten in the API request? Authentication-Results, Auto-Submitted, Content-Alias, Content-Base, Content-Disposition, Content-ID, Content-Identifier, Content-Length, Content-Transfer-Encoding, Content-Type, Encoding, Lines, Mail-System-Version, Mailer, Mime-Version, Originating-Client, Received, Received-SPF, Return-Path, VBR-Info, X-Mailer, X-Report-Abuse-To Can I copy transactional messages across workspaces? You can copy a transactional email to another workspace to save yourself time creating similar messages. Go to Transactional in the left hand navigation to get started. Filter or scroll to find the transactional message you want to copy. Then select the three dots to the right of the message and click Copy to. On the modal, select the workspace you want to copy your items to then click Continue.  When you copy transactional messages, we’ll reset settings that don’t exist in the destination workspace—like layout, audience, etc. You should also check your 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}}. syntax in your destination workspace. You may not have the same attributes or other variables in your destination, so you may need to update liquid statements to make sure that your messages render and send properly. --- ## Campaigns with transactional messages URL: https://docs.customer.io/journeys/transactional-campaign/ You can use a campaign to send a transactional message. This can be helpful if you want to send transactional messages that aren't email or push notifications, or you want to perform multiple actions in response to a transactional event. How it works A transactional message is a message that your audience expects to receive even if they’ve opted out of marketing messages—like purchase receipts, password reset requests, shipping updates, etc. In most cases, we suggest that you use our transactional API to send transactional messages. But, if you need to send a transactional message over a channel other than email, SMS or push notifications, or you want to perform additional actions—like attribute changes, etc—you may need to set up a transactional campaign. In this case, you’ll send an event to trigger your campaign—and set up your workflow to send a transactional message and perform other downstream actions. For example, here’s a password reset request example, in which we update a user’s attributes and send two messages—one providing instructions or a magic password link, and another confirming the password change. sequenceDiagram actor a as your user participant b as your website participant c as Customer.io a->>b: request password reset b->>c: send transactional event a->>c: person enters campaign rect rgb(181, 255, 239) note over a,c: The Campaign Workflow c->>a: send password reset request email c->>c: set new attributes on person c->>c: wait until user changes password i.e. password-changed event a->>b: person sets a new password b->>c: send "password-changed" event c->>a: send password reset confirmation end c->>a: person exits campaign  Make sure you follow transactional message regulations Transactional messages are typically intended for people regardless of their subscription preference—like shipping updates or purchase receipts. So, when you set up a “transactional” campaign, you may need to send messages to unsubscribed users, but make sure that you abide by the appropriate regulations (CAN-SPAM, GDPR, CASL) and don’t violate your audience’s subscription preferences. How is this different from using the transactional API? The transactional API has guard rails in place to make sure that you stay within FCC, CAN-SPAM, and other strict messaging regulations. It’s limited to a single message per API call. Transactional messages don’t go through a campaign/journey workflow, so they take less processing power and tend to send faster than a campaign-based transactional message. A campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. lets you perform a complete workflow in response to an event—including multiple messages and other actions, like attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. updates. You can also send SMS or in-app messages. This makes a campaign more flexible than the transactional API, but it lacks the safety of the transactional API. If you send transactional messages using a campaign, you must make sure that you abide by appropriate regulations. Set up your transactional campaign A transactional campaign is basically an event-triggered campaign, where the event represents your audience’s action and your campaign performs the expected response—messages, attribute updates, etc. 1. Send an event Before you create a campaign, you should send an event representing your transaction. That way you’ll have all your event information ready when you create your campaign. You can send events using our JavaScript snippet, the API or one of our libraries. For this recipe, we’ll send a purchase event into our workspace. curl -i https://track.customer.io/api/v1/customers/:id/events \ -X POST \ -u YOUR-SITE-ID-HERE:YOUR-SECRET-API-KEY-HERE \ -d name=purchase \ -d data[price]=13.45 \ -d data[product]=socks Confirm that this event appears in your workspace by going to the Activity Log and filtering for events. A purchase event should appear for the customer corresponding to the id in the request. 2. Create an event-triggered campaign After you verify that your event appears in your workspace, go to Campaigns > Create Campaign. Give your campaign a name that makes its purpose clear so you can track it later (in this case, we used Order Confirmation). Select Event as the trigger type. Under Conditions, find the event you sent in earlier steps. A person will enter your campaign when they perform the purchase event—i.e. when someone makes a purchases. If your campaign is truly transactional (it does not contain marketing content and your audience implicitly expects to receive the message), you should set your messages to send to all subscribed and unsubscribed. Click the title of your campaign, then choose Manage under Messages to adjust your audience. 3. Build your workflow Click Save to continue to the next step. In the Workflow Builder, you have full control over the content and schedule of your messages. A campaign can be simple and include one message—a receipt for the purchase. For our example, we’re going to send a message and add some customer attributes: An attribute update to keep an internal order count for the customer A transactional email receipt, framed as a thank you message A T/F branch to determine if this purchase is the customer’s first purchase Another attribute update; if the purchase is the customer’s first, we’ll set an attribute to trigger a downstream campaign that offers the customer a coupon for a follow-up purchase. These options can turn simple transactional messages into powerful campaigns that build customer loyalty through automated, yet personalized, messages. 4. Set a Goal and Exit Conditions (optional) Because transactional messages are intended to be purely informational, and expected by your audience, you may not need to set a goal or exit conditions. But, if you want to make sure that your transactional messages resonate with your audience, you might set a goal—like a follow up purchase, or someone clicking related items in your message. Learn more about goals and exit conditions. 5. Review and Activate your campaign Before you activate your campaign, you should check for errors or missing workflow actions. Make sure that you’re comfortable with your campaign’s timing, message names, and sending behaviors. When you’re satisfied, click Start, and that’s it! Go to the campaign’s overview page after the emails start sending to see how your campaign performs. Wrap Up Transactional messages are an essential part of any business and are often underutilized by product and marketing teams. Good transactional emails are timely, personalized, and provide value to your audience (and company). We hope this guide provides both instruction and inspiration for your transactional messages! If you have any questions about the process or how to apply it to your business, please send us a message! --- ## Workflow builder URL: https://docs.customer.io/journeys/workflow-builder/ Our visual workflow builder helps you construct message and action sequences on an open canvas. After you create a campaign, you'll land on the workflow builder. Campaign settings: edit the name, goal, exit conditions, and message settings. Build menu: drag and drop message and action blocks onto the canvas to create your workflow. Canvas: drag blocks here to add them to your workflow. The canvas flows top to bottom. Builder features: open/close the build menu, zoom in or out, add a sticky note for your teammates, export an image of your workflow, or review keyboard shortcuts. After you click a block, this menu expands to show Copy, Copy to, Move and Delete actions, too. How to use the builder To add a block to your workflow, drag an item from the sidebar onto the canvas. The canvas shows you where you can drop a workflow block with the indicator. Moving the canvas If your workflow stretches beyond the margins of your browser, you can scroll around the canvas. You can hold spacebar and move your mouse to scroll around the canvas as well. Select items on the canvas Click a workflow item to configure it. Hold shift and click workflow blocks to select them for Move, Delete or Copy operations. You can also click and drag to select multiple workflow blocks. Selecting a branch this way also selects all of the items in the branch. Moving items To move a block in your workflow, simply drag it anywhere you see a indicator. Click and drag to select multiple items. You can either drag items to a new spot in the workflow or click Move and select where you want to drop them. If you want to move items to a spot in your workflow that doesn’t fit on your screen, just drag the item to the edge of the canvas, and the workflow will scroll. Keyboard shortcuts The workflow builder supports a few keyboard shortcuts to help you complete some repetitive actions quickly and easily. You can review them within your campaign by clicking the lightbulb icon on the bottom menu. Function Key Description Build B Opens of closes the Build menu Select multiple shift + click To select multiple blocks Copy option / alt + drag Hold while selecting items to automatically copy them Delete del / backspace Delete workflow items Pan spacebar + drag; right-click + drag To move the canvas Zoom in/out command + + / - Zoom in or out Zoom to fit command + 0 Zoom to fit your workflow to the canvas Show/hide notes shift + N Show or hide sticky notes Sticky notes As workflows become increasingly complex, you may want to leave notes for yourself and other team members to help explain workflow logic, actions, delays, etc. To leave notes, drag sticky notes from the bottom menu onto your workflow. Click a note to add or edit the text. You can drag notes around the workflow to place them exactly where you want them. Hover a note and drag the icon to a relevant workflow action to link the sticky note to the action. Then when you move the action, your note moves with it. You can click to delete a note or click the icon to change its color. You might color coordinate notes written by different members of your team or differentiate between informational notes and warnings. Configuring workflow items After you drag a block into the workflow, click it to configure it—set a delay timer or add content to a message. Messages and data blocks that you haven’t configured are marked: 'You still have work to do'. Messages For each message block—Email, Push, Slack, SMS—you can: Edit the item’s name: this is not a subject line, just a shorthand name for the workflow item that will appear in the workflow builder. Click to add content. Turn it into an A/B test. Edit sending behaviour: whether it queues as a draft, sends automatically, or doesn’t send at all. Edit unsubscribe behaviour: by default, these will not send to unsubscribed. Please be careful with this setting! Add action conditions, in case you’d like the item to be skipped. Track links. (Emails only) Choose a specific platform. (Push notifications only) If you’ve added content to a message, you’ll be able to get a preview of it. Here’s a screenshot: Data blocks With data blocks, you can update people, send events, query collections, and create webhooks. Editing a Send and receive data block (a webhook) is similar to editing a message block. For each data block, you can: Set the block name Add details or add a request (Send and receive data blocks only) manage sending behaviour (queue drafts, send automatically, don’t send) Set action conditions. Delays There are three types of delays you can add to your campaign workflows: Time Delays, Time Windows, and Wait Until…. After you add a delay to the workflow, you can click it to edit the following: Time Delay: edit how long the delay is and add an action condition. Time Window: configure the window’s day and specific time. Wait Until…: add the condition that needs to be met and/or a maximum wait time. You can add an action condition to any delay. Flow control Branches let you send people down different paths in your workflow depending on various conditions. You can split people into a true/false branch, multi-split branch, or random cohort branch, depending on the types of conditions you want to apply. After you add a branch to your workflow, click it to define conditions for the branch. True/false branch: edit the name of the branch and the conditions determining whether people go down the true or false path. Multi-split branch: edit the name of the branch, choose the data type you want to split paths on, and then pick the values of that data type for each path. Random cohort branch: you won’t add conditions. Rather, specify the number of paths and what percentage of people should move down each. Exit: Reconnect the branch’s exit block to another path in the branch. Export your workflow as an image Click the image icon at the bottom to export your workflow as an image. This action downloads a PNG to your computer - you won’t receive an email notification - which you can share with teammates or other stakeholders that don’t have access to Customer.io. Copying workflow items You can copy items to/from campaigns in any of your workspaces when you want to re-use specific sequences of actions, their conditions, and content. You can also duplicate campaigns to re-use not only an entire workflow, but also campaign settings like trigger conditions and goal and exit criteria. You cannot, however, copy campaigns across accounts. --- ## LLM actions: Generate data & decisions with AI URL: https://docs.customer.io/journeys/llm-actions/ An **LLM action** lets you prompt a Large Language Model (LLM) to generate and store data for use throughout a campaign. It's how you use generative AI to enhance your workflows!  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. How it works LLM actions let you prompt an AI model as a part of a campaign and store the output as attributes so you can use them later in the campaign. You can personalize messages, enrich data, and create conditions to help you reach the right audience. flowchart LR A[Person enterscampaign] --> B[LLM actionruns] B --> C[Response stored asattribute] C --> D[Use attribute inmessages and conditions] By default, LLM actions store data as journey attributesAn attribute stored on a journey during a campaign. Journey attributes expire when people exit your campaign., which expire when people exit your campaign. If you want to use the LLM’s response outside of the campaign, you can change them to customer attributesData stored on your customers’ profiles, like a person’s name. You can include this data in messages or conditions across your workflows. instead. Billing: LLM actions use AI credits Unlike other workflow blocks, LLM actions have their own currency: AI credits. Each time an LLM action calls a model, it uses AI credits. This includes when a person reaches the action in a campaign and when you use Preview response to test it. The number of credits consumed depends on the model you select, the size of the prompt, and the amount of context sent with the request. See AI credits for details on pricing and what happens when credits run out. Ways to use LLM actions You can use LLM actions to generate data for use across your workflows. Here are a few use cases you could consider: Personalized product recommendations: Pass purchase history and browsing data to suggest relevant products for each person. Follow-up on purchase based on customer sentiment: Create message content based on a customer’s experience from purchase to delivery. If sentiment is positive, request review. If sentiment is negative, send a follow-up asking what you could do better. Classify accounts: Classify customers based on their companies’ data. Update data from the response of an LLM action You can use LLM actions to analyze a customer’s behavior and generate insights that you store on attributes for use later on in your campaign. To set or update data based on an LLM’s insights, you would follow these steps: Prompt the LLM to analyze specific customer attributes, trigger data, or data provided in the prompt. Store the output as a journey or customer attribute, depending on if you want to use the data outside of the campaign. Create subsequent conditions that target the updated attribute or reference the data in messages using liquid. Send a message using content from an LLM action  Don’t communicate sensitive information or updates with LLM actions If you’re looking to automate personalized messaging at scale, you can use LLM actions to create email content unique to each person moving through your workflow. However, you’ll be sending content that hasn’t been reviewed by your team. Remember that LLMs can make mistakes, like not quite matching your tone or incorrectly categorizing your data. Don’t communicate sensitive matters with unreviewed, LLM-generated content. Consider using our Agent to generate a template instead. To send a message using content from an LLM action, you would follow these steps: Prompt the LLM action to create copy based on your customer’s data and your content guidelines. Store the output as a journey attribute, like body. Reference the journey attribute in a subsequent message block. If the attribute value doesn’t contain liquid syntax, you can reference it as: {{journey.body}}. If the LLM-generated content contains liquid syntax—like {{customer.first_name}}—use {% render_liquid journey.body %} so the liquid within the value renders dynamically. If you use {{journey.body}} instead, any liquid in the value displays as static text. Set up an LLM action LLM actions are available for campaigns. In the workflow builder, scroll down to Data, then drag the Run LLM onto your campaign’s canvas. Click the block to open its configuration menu, and select Edit Content to get started. (Optional) If you only want certain people who trigger the campaign to run the LLM action, you can add Conditions here to filter your audience. Add a Prompt to instruct the LLM on what to do and how. The more specific you are, the better the results will be. Learn more about creating prompts below. Consider the type of task it should perform then choose your Model. Learn more about model types, credit usage, and costs below. Generate Output Fields—the journey attributesAn attribute stored on a journey during a campaign. Journey attributes expire when people exit your campaign. you want to create to store data from the LLM response. Learn more about setting and storing responses below. Click the Response tab to set fallback values for each attribute created by your output fields. If you want this data available outside the campaign, this is also where you can change a journey attribute to a customer attribute. Click Preview Response to test the LLM action and see an example of how the chosen LLM would interpret your prompt. This counts towards your AI credit usage. Learn more about billing. Prompt: Tell the LLM what to do and how When you prompt an LLM action, you should include the following so the LLM has full context on your use case: Define your goal. If you don’t know exactly what you want, the LLM won’t either. Be direct, concise, and specific. Provide any context that’s necessary to achieve your goal, like how and why to evaluate data. Include any attributes you want the LLM to use in its response. See What data can an LLM action use for more info. Define the structure of your output. Your AI settings (compliance prompt, business context, etc.) influence the output of LLM actions too. If you want your responses to differ from these defaults, consider updating your settings or explicitly define the tone, audience, etc in the prompt of the LLM action. You can learn more about best practices for prompts from the LLM providers. If you choose a Google model to process your prompt, learn more in the Google’s Gemini documentation. If you choose an Anthropic model to process your prompt, learn more in the Anthropic’s Claude documentation. Prompt example Below is an example of how to improve a prompt. Bottom line, you should preview responses to your prompt to gauge whether the output is what you want. But if you’re looking to improve your output quality and make it more consistent, here’s an example that highlights best practices. Prompt Quality Why Account upsell: Compare customer seat utilization to their current plan. Low The goal is not clear; there’s only an idea around upselling. The data to use is barely defined and the desired output is absent. Analyze this account’s expansion readiness. Compare their seat utilization {{customer.seats_used}} to their current plan {{customer.plan_name}}. An account may expand if seat utilization is greater than 80% and they’re not on the highest plan. Medium The goal is stated. Some data is identified along with some criteria for evaluation. But the desired output is still absent. High The goal is defined and criteria for being expansion ready is defined. The prompt includes the data to use and desired output format. Sample prompts Need inspiration? These example prompts show you how to structure instructions for common use cases. They aren’t meant to be copy-pasted; you should adapt them to your business, your data, and your tone. Each prompt demonstrates key best practices: defining a persona, referencing customer or event data, setting clear guidelines, and specifying the output format. Expand a use case below to see the full prompt and learn more about how to use it. Personalized welcome email Generate a welcome email tailored to how someone found you. This prompt uses the customer’s signup source to adjust messaging—with relevant content for blog readers, product categories for product page visitors, or a referral program for referred customers. You are an email copywriter for an outdoor gear company. Write a welcome email for a new customer. Customer details: * Name: {{customer.first_name}} * Signup source: {{customer.signup_source}} * Interests: {{customer.interests}} Guidelines: * If from blog: mention our guides section * If from product page: highlight that product category * If from referral: thank them and mention referral program * Tone: warm and enthusiastic, like a friend welcoming them * Mention: free shipping over $75, easy 60-day returns Output: * Subject line (max 50 characters) * Email body (approximately 200 words) with CTA to browse bestsellers Avoid generic phrases like "Dear valued customer." Keep it human. Lead qualification and sales routing Score and classify leads based on your data. This prompt returns structured JSON that you can store as attributes and use in downstream workflow conditions—routing hot leads to sales demos and cold leads into nurture sequences. You are a lead qualification specialist. Analyze this lead and determine sales readiness. Lead information: * Name: {{customer.first_name}} {{customer.last_name}} * Title: {{customer.job_title}} * Company size: {{customer.employee_count}} employees * Pricing page views: {{customer.pricing_page_views}} * Trial actions: {{customer.trial_actions_count}} * Content downloaded: {{customer.downloads}} Signals: * 100+ employees = higher priority * Multiple pricing views = buying intent * Title with Director/VP/Manager = decision maker * 10+ trial actions = engaged Output JSON: { "score": [1-100], "qualification": ["hot", "warm", "cold"], "recommended_action": ["schedule_demo", "send_case_study", "nurture_sequence"], "reasoning": "[One sentence]" } Be conservative with "hot"—only 80+ if multiple strong signals. Win-back campaign for churned customers Write a re-engagement email that acknowledges why someone left and highlights what’s changed. The prompt uses the customer’s cancellation reason and feature usage to personalize the message rather than sending a generic “come back” email. You are a retention specialist writing a win-back email for a cancelled customer. Customer details: * Name: {{customer.first_name}} * Previous plan: {{customer.previous_plan}} * Tenure: {{customer.tenure_months}} months * Cancellation reason: {{customer.cancellation_reason}} * Top features used: {{customer.top_features}} * Days since churn: {{customer.days_since_churn}} What's new: * New mobile app with offline access * AI-powered reporting dashboard * 3x faster sync times * Slack and Notion integrations Guidelines: * Don't be desperate or pushy * If they gave a reason, acknowledge it and mention improvements * Reference their specific usage to remind them of value * Offer: 30% off for 3 months if they return this week Output: * Subject line (max 50 characters) * Email body (approximately 200 words) Never use guilt-tripping language like "We miss you." Personalized product recommendations Recommend products based on a customer’s preferences and purchase history. This prompt matches customers to specific products and explains why each is a good fit—useful for subscription services, e-commerce, or any business with a product catalog. You are a personal shopping assistant for a coffee subscription company. Write a product recommendation email. Customer profile: * Name: {{customer.first_name}} * Preferred roast: {{customer.roast_preference}} * Favorite flavors: {{customer.favorite_flavors}} * Brewing method: {{customer.brewing_method}} * Past purchases: {{customer.purchase_history}} This month's products: * Ethiopian Yirgacheffe (light, floral/citrus) * Colombian Supremo (medium, chocolate/nutty) * Sumatra Mandheling (dark, earthy/bold) * Costa Rica Honey Process (medium, sweet/fruity) * Espresso Blend (dark, caramel/cocoa) Task: * Pick TWO products that match their taste * Explain why each suits them * Match to their brewing method * Tone: like a barista friend who knows their taste * Include code: JUSTFORYOU for 15% off Output: * Subject line (max 60 characters, include their name) * Email body (approximately 200 words) Write like a coffee enthusiast, not a marketer. Post-purchase review request with sentiment awareness Request a review—or don’t—based on the customer’s likely satisfaction. This prompt evaluates support tickets and delivery status to determine the customer’s sentiment and generates the appropriate response: a confident review ask, a careful check-in, or a service recovery email. You are a customer success writer crafting a review request. Adjust approach based on likely satisfaction. Customer details: * Name: {{customer.first_name}} * Product: {{event.product_name}} * Purchase date: {{event.order_date}} * Support tickets since purchase: {{customer.tickets_since_order}} * Ticket status: {{customer.ticket_status}} * Delivery status: {{event.delivery_status}} Sentiment rules: * Unresolved support tickets = frustrated, do NOT ask for review * Late delivery = acknowledge the delay * Repeat customer, no issues = satisfied, confident ask * First-time buyer = check in first, subtle review mention Write ONE of these: 1. SATISFIED: Friendly review request with direct link 2. POTENTIAL ISSUE: Check-in email, subtle review mention only if happy 3. FRUSTRATED: Apology-first, focus on resolution, NO review request Output JSON: { "sentiment_assessment": ["satisfied", "neutral", "frustrated"], "email_type": ["review_request", "check_in", "service_recovery"], "subject_line": "[max 50 characters]", "email_body": "[approximately 175 words]" } Never ask a frustrated customer for a review. Review your AI settings In your account and workspace settings, you can add context about your company and audience to improve how AI generates responses across your workflows. These settings influence how the agent communicates with you, how AI features like segment generation and email content analysis work, and the data generated by LLM actions. You can manage context given to LLM actions on these pages: Workspace settings > Business context Account settings > Privacy, Data, & AI Gemini Safety Settings—Within Run LLM actions, these settings only apply if you’re using one of Google’s models, as indicated in the model dropdown menu. They don’t apply to Anthropic models. Compliance Prompt If you want a single LLM action to differ from these defaults, make sure you include that in the prompt you give the LLM. What data can an LLM action use? An LLM action bases responses on the text in the LLM action prompt, the context from account and workspace settings, and the workspace data it has access to. Data available Data unavailable Text and data provided in the LLM action prompt Any media files like images and videos Context from account and workspace settings Websites, articles, or other online content; it can’t crawl any sites Customer attributes Events (unless they’re part of the trigger data) Journey attributes set earlier in the campaign Object or relationship attributes (unless they’re part of the trigger data) Data that triggered the campaign Any trigger data available through liquid is accessible to LLM actions; the LLM action can use events, objects, webhooks, etc that trigger campaigns to generate responses. However, LLM actions cannot access event and object relationships that did not trigger campaigns. For instance, this means you could ask an LLM action to generate a message based on event data from the trigger, but you shouldn’t prompt the LLM action to analyze all event data for a person and save its findings to the customer’s profile. That wouldn’t be inclusive of the breadth of a person’s activity across your platform. Model: Choose the right model for the task When you configure an LLM action, you choose which model processes your data. Different models have different strengths—and different costs. Reasoning models produce higher-quality results for complex tasks but use more credits per run. Quick models are faster and more cost-efficient, using fewer credits per run. Consider the complexity of your task when choosing a model. If you’re doing simple categorization or translation, a quick model may work well. For nuanced analysis or creative content, a reasoning model may produce better results. When you choose a model, you’ll see a multiplier beside the model name. This represents the credit burn rate compared to the base model. In this example, the Anthropic model uses 10x more than our base model—Google’s Gemini 2.5 Flash Lite. Learn more about credit burn rates. Output: Store the response as attributes After you add your prompt, you’ll generate the output—how the LLM will store its response. By default, the LLM action stores data as journey attributesAn attribute stored on a journey during a campaign. Journey attributes expire when people exit your campaign., which you can use throughout a person’s journey in the campaign, but not once they exit. If you want to use this data outside the campaign, change them to customer attributes in the Response tab. You can use these attributes in a variety of ways in subsequent actions: Personalize messages with liquid Create branches in your workflow based on the attribute output from the model Build conditions to filter people out of certain actions or messages Use them as inputs for other LLM actions downstream Create outputs manually On the Content tab, click Add field under Output Fields. Add a Name. This becomes the key used to reference the output through liquid syntax. Select a Type of value you want to store. Enter a Description so you know how to use the output. This is especially helpful if you’re setting customer attributes. This description will appear in your Data Index and help you audit your data in the future. Select whether the LLM action is required to generate the output. Click Save. By default, output fields are journey attributes, which expire once a person exits the campaign. If you want to use these attributes outside the campaign, you can change them to customer attributes in the Response tab. Generate outputs from your prompt On the Content tab, click Generate from prompt under Output Fields. Click Replace to view the latest output fields. Review the output: click to view the returned name, value type, and descriptions. Modify them as you see fit. Name: The key used to reference the output through liquid syntax. Type: The type of value you want to store. Description: A description of the output. This is especially helpful if you’re setting customer attributes. This description will appear in your Data Index and help you audit your data in the future. Save your changes. You can also add fields manually alongside generated outputs or delete items you don’t want to store. By default, output fields are journey attributes, which expire once a person exits the campaign. If you want to use these attributes outside the campaign, you can change them to customer attributes in the Response tab. Types of values Each output field has a type of value that defines what the LLM action should store in your attributes. Type Description Example Text A text string value “Mark your calendars: the summer solstice is coming!” Number A number that can include decimals 3.14 Integer A whole number (no decimals) 42 Boolean A true/false value true Date A date string (ISO 8601 format) “2026-03-31” Date and Time A timestamp string (ISO 8601 format) “2026-03-31T14:30:00Z” Time A time string “14:30:00” List An array of generated text values ["Subject line 1", "Subject line 2", "Subject line 3"] Single Select One value picked from predefined options “positive” (from options like ["positive", "negative", "neutral"]) Multi Select Multiple values picked from predefined options ["positive", "neutral"] (from options like ["positive", "negative", "neutral"]) Delete output fields To remove output fields stored from an LLM action response, go to the Content tab and click beside the field you want to delete. The Response tab will update to reflect the changes. Change from journey to customer attributes By default, the output fields generated in the Content tab are journey attributes, but you can change that in the Response tab. If you want to take action on the data outside the campaign, then you’ll want to change them to customer attributes. Click beside an attribute to switch types. You can’t set or modify events, objects, or relationships with LLM actions. However, you can use a Send event action to store events based on customer or journey attributes set by an LLM action. Respond to failed LLM actions An LLM action can fail for reasons including: Your account runs out of AI credits The model returns an error The action times out If an LLM action fails, your campaign will retry the action twice. If the action fails after three attempts, the journey will continue without the attribute updates, which could impact subsequent workflow actions that rely on them. You can set fallback values so any condition or content that references the attributes continues to be evaluated in a way that’s best for your customers. By default, output attributes do not have fallback values, but you can set them in the Response tab. Consider what’s best for your use case. How should people move through your campaign if the Run LLM action fails? If the LLM action generates email copy, it might make sense to store fallback content so your customers still get the core of your message in a subsequent action, just with less personalization. Otherwise, the email would fail to send altogether, and they’d move onto the next action. If the LLM action is meant to determine whether your customer is likely to upgrade their plan, you might leave the fallback blank so you know it didn’t update and send them down a different path in the workflow when the attribute does not exist. If a customer or journey attribute is already set and the LLM action should update them, the attributes will only update if the LLM action succeeds or has fallback values. If the LLM action fails and has no fallbacks set, the attributes remain unchanged; they won’t be cleared or unset. Preview your LLM action response You’ll see two preview options in an LLM action: Preview Response—This shows you an example of how the LLM model you selected will interpret your prompt. This uses up AI credits. Processed Prompt—This renders any liquid in your prompt according to the sample data selected in the panel. Use this to make sure any liquid logic in your prompt works as expected. On smaller screens, click the Preview tab to see the processed prompt. To use either preview, any liquid in your prompt must render. This means the keys must exist in the sample data selected and/or have fallbacks. If the prompt preview doesn’t work, click Review Errors to find and fix liquid that’s causing an issue. Before you activate a campaign with an LLM action, test it to make sure it returns the results you expect. Search for and select a person from the Sample Data panel that would cause the LLM action to run. Click Preview Response. Remember, each run uses AI credits. Review the model’s output to verify it meets your expectations. Check your credit usage; does your account have enough credits to run the action considering the anticipated size of your audience? If a value is cutoff, hover your cursor over it to view the full output. Adjust your prompt or model selection if needed and preview the response again.  Test LLM actions with multiple people Try testing with several people to make sure your prompts handle a variety of inputs. Check edge cases like missing attributes or unusual values to make sure the LLM returns something useful. --- ## Send event URL: https://docs.customer.io/journeys/event-action/ You can create an event within a campaign through the *Send event* block. This makes it easy to trigger other campaigns or add people to segments. For webhook-triggered campaigns, this makes it easy to reshape and associate data from an outside source with people in your workspace, all without having to talk to a developer or write your own integration. An event consists of a name and a data object. The name is typically how you’ll select an event, and you can further filter people based on data in the event. You can use properties in the data object to personalize campaigns using 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}}. in the format {{event.<property>}}. { "name": "my_event", "data": { "a_value": "that-I-use-in-liquid" } } A Send event action has a size limit of 100 KB. How it works in most campaigns As long as your campaign isn’t triggered by a webhook—i.e. it begins when a person performs an event, matches attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. conditions, etc—you can send an event for the person currently in campaign, or another person entirely. You’ll send your new event from values in the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. that triggers your campaign or customer attributes. If you want to send an event for another person, you’ll need a value representing that other person either in the event that triggered the current campaign, or stored as an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. on the person currently in the campaign. If the person with this value doesn’t exist, your event will create them! flowchart LR a[person]-->|Performs event or attribute change|1 subgraph 1 [Campaign 1] direction TB b[message]-->c[send event] end 1-->|Event triggers new campaign|2 subgraph 2 [Campaign 2] direction TB d[message]-->e[message]-->f[conversion] end Send an event When you use the Send Event action, you can pick from different types of values for your Event Name (the name key) and Event Attributes (contained by the data object): Static value: a value that is the same in every event Customer attribute: a value associated with the person in the campaign 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}}.: use liquid to modify customer or event attributes to fit your event—like reformatting a date value or appending a string to your event’s name. JavaScript: use JS to modify customer or event attributes to fit your event. Event attribute: a value from the data object of the event that triggered the campaign (if applicable) Trigger object attribute: a value from the object that triggered the campaign (if applicable) - To see a preview of the payload, make sure you select a person from the sample data on the left who has a relationship to the object. Trigger relationship attribute: a value from the relationship that triggered the campaign (if applicable) - To see a preview of the payload, make sure you select a person from the sample data on the left who has a relationship to the object. To send an event in a campaign: Drag Send Event into your workflow and click it. Set the Event Name, and then click Edit event to set up your event. The event name helps you identify this action in your workflow; it’s not the name value for the event. Select the person you want to send an event for. By default, this is the person in the current campaign. If you select Someone else, you can pick the Workspace your other person belongs to, and you’ll need to select the value representing their identifier. Set the Event Name. This is the name value in the event, and the way you’ll reference this event in campaign triggers, segments, etc. Add Event Attributes. These are the values that appear in the data object of your event. You can use these properties to filter people into different campaigns, or reference them in messages using 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}}.. Click Done. How it works in a webhook-triggered campaign A Webhook-triggered campaign is a campaign that begins when you receive an incoming webhook. This incoming webhook is not directly associated with a person in your workspace. That’s often why you’ll create a new event: to re-shape and associate your incoming webhook data with a person in your workspace to trigger messages, add a person to segments, etc. Your incoming webhook should contain a value that you use to identify people. When you create an event, you’ll associate a value in your incoming webhook with an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people in one of your workspaces. If a person with this identifier doesn’t already exist, this action will add them to your workspace. sequenceDiagram participant A as Person participant B as External System participant C as Customer.io A->>B: Does something B->>C: Incoming webhook C->>C: Create event Note over A,C: Your event can add people to segments or trigger campaigns C-->>C: Add person to segment C-->>C: Trigger campaign C-->>A: Response message (Optional "Thanks for your response!") Send an event from a webhook-triggered campaign When you create an event, you must specify an Event Name (the name key) and Event Attributes (contained by the data object). Each of these fields can be one of four types: Static value: A value that is the same in every event. Trigger attribute: A property from your Trigger data. You can start typing to find a key in the incoming data or click the value in the Trigger data. 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}}.: Use liquid to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. JavaScript: Use JavaScript to modify trigger attributes to fit your event—like reformatting a date value or appending a string to your event’s name. Use JavaScript over liquid if you want to modify incoming JSON. To send an event from your webhook-triggered campaign: Drag Send Event into your workflow and click it. Set the Event Name. The event name helps you identify this action in your workflow; it’s not the name value for the event. Click Add event to set up your event. Under Who do you want to update?, select the Workspace containing the people you want to attach events to. Select the type of Identifier and set the identifier’s value for people in that workspace. You can identify someone from a trigger attribute, liquid, or JavaScript. Set a static value to always send events for the same person. Set the Event Name. This is the name value for the event that you’ll reference outside of this campaign in other campaign triggers, segments, etc. Add Event Attributes. These are values that appear in the data object of your event. You can use these properties to filter people into different campaigns or reference them in messages using 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}}.. Save your changes and click Done. Now you can finish building your webhook-triggered campaign. Use liquid to modify event properties The Liquid value type (or the JSON editor) lets you manipulate data to fit your event using 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}}.. When using liquid, you access properties using JSON dot notation, beginning with the type of data you’re working with: Customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. begin with customer. If you wanted to access a person’s ID, you would use {{customer.id}}. Events begin with event. If you wanted to access a property in an event’s data object called id, you would use {{event.id}}. Objects begin with objects. If you wanted to access a property for an object, you would use {{objects.<object_type>[#].<attribute>}}. Objects trigger syntax would be {{trigger.<object_type_singular>.<attribute>}}. Relationships begin with objects. If you wanted to access a property in a relationship, you would use {{objects.<object_type>[#].relationship.<attribute>}}. Relationship trigger syntax would be {{trigger.relationship.<attribute>}}. Webhook trigger data begins with trigger. For example, you would access the id attribute from the trigger data below using {{trigger.identifiers.id}}.  When you reference an array or JSON object, use to_json By default, liquid maps objects and arrays to strings (or an integer, where applicable). Use | to_json when you reference an object or array in your outgoing webhook to avoid errors and maintain the original shape of your trigger data! { "name": "purchase", "identifiers": { "id": "abcd-1234", "email": "person@example.com" }, "purchased_at": "Fri, 04 Feb 2022 23:49:39 GMT", "total": 123.45, "items": 2, "tax": 10.45 } Liquid with arrays and objects By default, liquid maps arrays and objects to strings (or, if an array contains only integers, liquid maps it to a single, concatenated number). You need to use to_json to maintain the shape of arrays or objects in your event. Otherwise, you won’t be able to access properties inside the array or object in campaigns or segments that use your event. With to_json Without to_json For example, if you want to send an event with the purchase array from the trigger data below, you would format your liquid like this: {{trigger.purchase | to_json}}. The to_json tag also maintains the original data type of items inside the object—strings, numbers, etc. { "name": "purchase", "identifiers": { "id": "abcd-1234", "email": "person@example.com" }, "purchased_at": "Fri, 04 Feb 2022 23:49:39 GMT", "total": 123.45, "items": 2, "tax": 10.45, "purchase": [ { "product_name": "shoes", "sku": 1234, "qty": 1, "price": 73.00 }, { "product_name": "socks", "sku": 5678, "qty": 4, "price": 40 } ] } Examples When you add trigger data to your event with liquid, you can modify or even transform values to better support the things you want to do in Customer.io See the liquid tag list to see all the ways you can modify these values. But, using the event above, here are a few quick examples of what you can do: Reformat the purchase_at ISO date-time to a Unix timestamp using date %s. "created_at": "{{ trigger.purchased_at | date: %s }}" outputs 1644018579 Prepend or append the event name with a static value with append or prepend. "name": "{{trigger.name | prepend "online_" }}" outputs: online_purchase Map the product names from the purchase array to a single key using map. "products_purchased": "{{trigger.purchase | map: 'product_name'}}" outputs: shoes, socks Use JavaScript to modify event properties You can manipulate values that you set in your event with JavaScript. When you use the JavaScript option, you’ll either produce a return statement for each data attribute in your event, or switch to the JSON editor and return an object containing all your event data properties. You can set properties from any source available in your campaign—trigger data, attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., event properties, snippetsA common value that you can reuse with Liquid in messages and other workflow actions—like your company address. You can store Liquid inside a snippet, making it easy to save and reuse advanced values and statements across your messages., etc. For example, if you wanted to access a person’s id from the object we used in our Liquid example above, you would use return triggers.identifiers.id;. See our JavaScript quick reference guide for more examples to help you take advantage of JavaScript in your workflow.  You can’t use Liquid inside JavaScript When you use the JavaScript option, you must manipulate values with JavaScript. If you try to return a snippet value that contains Liquid, you’ll receive an error. --- ## Batch update URL: https://docs.customer.io/journeys/batch-update/ A batch update lets you apply data to a group of up to 1,000 people matching your criteria. For each person your batch update matches, you can update attributes or send events (to trigger a message campaign, for example). How it works You can perform a batch update in any campaign. A batch update lets you associate this data with one or more people—either as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages.. For example, you could create a campaign that messages people when they’ve been assigned to teach an upcoming class. You could use a batch update to send events to people who have this person listed as their favorite teacher. Then you could trigger another campaign based on this event and encourage these students to sign up for the new class. Set up a batch update Drag Batch Update into your workflow. Add a Name, then click Add Details. Select whether you want to Update profile attributes or Send an event. Set criteria for the people you want to update.  Your criteria must match fewer than 1,000 people If your batch criteria ever matches more than 1,000 people, the batch update will not run. Set your attributes or event data. You can use 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 set attributes or event properties. Click Save. Matching people in your batch update When you perform a batch update, you set criteria matching the group of people you want to update. It’s like setting up a segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., but you can only match on attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. conditions. You can set multiple conditions for people you want to match, but a person must match all of your conditions to be included in the update. A batch update is limited to 1,000 people. Anytime your batch update criteria matches more than 1,000 profiles, we won’t run the update. Instead, we’ll skip it and report it as a Failed Action in your activity log. When you set up a batch update, we’ll warn you if your criteria matches more than 1,000 profiles, but you should monitor your campaign to make sure that your batches don’t exceed the limit. This example shows matching people based on both a profile attribute and relationship attribute for an account. With object and relationship-triggered campaigns, you can match people based on the trigger object or any object within an object type you choose. For instance, you could match people related to any course that the current person in the journey has a relationship to: Goal: When a teacher is assigned to a course, I want to recommend the new course to students in the teacher’s other courses. Method: Set the recommended new course as an attribute on students in one campaign. Then segment users based on this attribute and trigger another campaign to notify them. Campaign for batch update: Trigger: course - relationship updated where role is equal to teacher Audience: the person that was added to the course Batch update: match people who are related to any course that the current person is related to where the people in the batch update have the role of student With relationship-triggered campaigns, you can also match on the person triggering the campaign, instead of the current person in the journey. This is helpful when the trigger person is not included in the audience of your campaign, so the trigger person could never be “the current person.” Update attributes When you use the Update profile attributes option, your batch update sets or updates attributes for every person matching your conditions. You can add people to segments based on these attributes which trigger messaging campaigns. In an object or relationship-triggered campaign, you can update attributes based on the attributes of the trigger object or relationship. In a webhook-triggered campaign, you can set attributes to data from your incoming webhook—the trigger- using 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}}.. Send events When you use the Send an event option, your batch update sends an event to every person matching your conditions. In an object or relationship-triggered campaign, you can add event attributes based on the attributes of the trigger object or relationship. But if you’re trying to trigger a campaign off of these events, consider triggering the campaign based on the object or relationship instead. In a webhook-triggered campaign, you can set event attributes to data from your incoming webhook—the trigger- using 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}}.. --- ## Conditions URL: https://docs.customer.io/journeys/action-conditions/ You can control which customers enter your campaigns with triggers, filters, and frequency settings. You can also add **conditions** to messages, data blocks, and delays to filter your audience within your campaign. Click an action to edit conditions: Hover over the condition on the action to see the details: How they work Conditions are optional checks you can add to any message, data block, or delay in your campaign. We check the conditions before initiating the block’s action (sending a message, starting a delay timer, or sending a webhook, for instance). If people don’t meet the conditions, they skip the action and continue through the campaign. If they should leave the campaign entirely, then consider adding these conditions to the campaign’s trigger criteria. You can build conditions from people’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. data, message data, and segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. membership. You can also use journey attributesAn attribute stored on a journey during a campaign. Journey attributes expire when people exit your campaign. in conditions. When building conditions based on email opens or clicks, you can also specify whether to target human only or human and machine engagement.  You can use JSON dot notation in condition logic If you store attributes or event data in JSON objects or arrays, you can use JSON dot notation in your branch conditions to evaluate these properties. Use array[] to represent any item in an array or array[0] to represent the first item in the array. See Storing and using JSON for more information about dot notation in Customer.io. When to use action conditions Use conditions to personalize journeys even further within a single campaign. Here are a few ideas: Channel targeting: send an SMS message, but only if the customer has a phone number Tailored onboarding: skip tutorial messages if your users have already completed those tasks to keep your emails relevant Messaging by segment: send custom content to users based on segment membership Edit conditions Drag and drop a block onto the canvas or click one to edit it. Click Conditions to expand it. Then define your criteria. Specify which conditions a person should meet to receive the message, enter the delay, or to cue the data update. If people do not meet the conditions, they will skip this item and move on in the workflow. Event has been performed conditions The Event has been performed and Event has not been performed conditions check whether a person has ever performed an event. It isn’t limited to the current campaign. If you use the Event has been performed condition, and a person performed the event before they entered the campaign—even if they performed the event well before they started a campaign journey—they’ll meet the condition. Use RegEx in conditions Event names and other data you may want to create conditions with can include special characters. In most cases, we’ll treat special characters as literal characters: & is &. However, the following special characters perform Regular Expression (RegEx) functions that look for patterns of characters. To treat them as characters, not search functions, you can escape them with \ (for example, \+1). Otherwise, this is how they work: Special character RegEx function * This is a wild card: it represents any character. For example, s*n matches event names “sink”, “sun”, and “lesson”. If you don’t place any characters after *, it’ll match the string up to *. For example, si* matches “reside” and “sink”. + This represents “and”: it matches both values in a statement. You can use it with attribute conditions and event conditions, but not event names. For example, if you wanted to find a person whose email address contains person and gmail, you could write email contains person+gmail. A few emails this would match on include person@gmail.com, person+test@gmail.com, or person2@gmail.com because these addresses contain both person and gmail. | This represents “or”: it lets a person who has performed (or not performed) either event to enter the segment. For example, a segment based on hide|seek events lets a person who has performed either the hide or seek events to enter the segment.  Use the conditions with contain to use RegEx To use regular expressions, you need to use the “contains” or “does not contain” conditions. Using “equals” or “does not equal” will yield no results. --- ## Holdout tests URL: https://docs.customer.io/journeys/holdout-test/ A *holdout* is a type of A/B test for emails. But, rather than testing how your audience responds to variations of a message, a holdout test helps you test how your audience responds when they receive a message and when they don't. In a holdout test, some percentage of your audience *won't* receive a message at all. How it works A holdout is a type of A/B or random cohort test for email messages. But, rather than testing how your audience responds to variations of a message, some members of your audience will receive a message, and some won’t—they’re purposely “held out” of the test. This helps you determine whether a message is useful or not. When you setup a holdout test in your campaign, you’ll determine the percentage of your audience you want to prevent from receiving a message. We’ll still generate deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. for your holdout group, but we send them to an internal message trap rather than sending them to your audience. This lets us calculate conversions, so you can compare conversionsA campaign goal that you want your audience to achieve. You measure this goal as an attribute change or an event that occurs after a person receives a message within a campaign workflow. for people who receive a message to conversions for people who don’t, and determine the true utility of your message. flowchart LR a[people enter a/b test] --> b{email or hold out?} b-->c(people receive messages) b--->|Holdout group doesn't get a message|e(calculate success rate) c-->f(calculate success rate) e-->g{is there a clear winner?} f-->g g-->|yes|h(choose the winner) g-.->|no|i(wait for more data or end the test) Create a holdout test It tends to be easier to set up an A/B test where one variant is the holdout. But, if you want to test multiple variants of a message, you can also perform holdout tests with a random cohort branch. Either variation in your A/B test—email A or B—can be the holdout. But, for this example, we’re treating variation B as the holdout. In your campaign’s Workflow, select the email you want to convert to a holdout test. Click Turn into A/B test. Set the amount of traffic that you want to send to your message (variation A) and holdout group (variation B). Click Edit Variation B, and select Make this message a holdout test. This converts the “message” to a holdout, ensuring that it’s sent internally to a message trap; it is not sent or visible to your audience.  Check your sending behavior The Sending Behavior has to be set to Queue Draft or Send Automatically for your holdout test to work because the holdout test “sends messages” to the message trap. If you use the Don’t Send setting, your holdout test won’t work. Now, when you start your campaign, some people will receive Message A, and some people won’t get a message at all. Random cohort holdout testing If you’ve set up your own holdout test by creating a “black hole” address, or you want to test more than the two variants supported by an A/B test, you can use a random cohort branch to perform a holdout test. Drag a Random Cohort Branch into your campaign or broadcast workflow. Click the Random Cohort Branch action, set the percentage of people who will flow down each path, and click Save. Click Add Path if you want more than two branches in your random cohort. Add messages to each branch in the test, and set up your message content for the “live” message. You don’t need to write content for the “holdout” variant, because it won’t be sent to your audience. Click the message that you want to represent your holdout, and select Make this message a holdout test. This converts the “message” to a holdout, ensuring that it’s sent internally to a message trap; it is not sent or visible to your audience. Now, when you start your campaign, a percentage of people will flow through your random cohort branch to the holdout variant. These people won’t receive a message. Check holdout test performance To check your results, go to your campaign and click the A/B Test tab. Here, you can select the “winner” of the test. The winner remains in your campaign, and the other message is removed. If your winner is the holdout, the message action is removed from your workflow entirely. You can determine the winner using the Chance To Beat Original (CTBO) metric, in three different statistics: open rate, tracked link clicks, and conversion rate. You can learn more about how we calculate CTBO here. When you use holdout tests, you should set conversion criteria and use the conversion metric to determine the true utility of your message. We don’t send a “holdout” message, so it’ll never be opened or clicked; the message variant that you send will always win those metrics. But in holdout tests, people in the holdout group register conversions when they perform the goal action within the conversion time window—even though they didn’t receive any message. This helps you measure the true impact of your messages by comparing conversion rates between those who received messages and those who didn’t. If there isn’t a clear winner in your test result categories, we’ll show a “Not significant, need more data” message. You might need to let your test run longer to gather more results. If your test has already run for a while, and you’re confident that enough people have gone through the test, there might not be a statistically significant difference between your message and the holdout. Holdout message status and metrics Messages that are held out are “sent” and “delivered” to a message trap in Customer.io. We do this purely to help you visualize the performance differences between your real messages and your “held out” messages. But these messages never leave Customer.io. Because holdout messages never leave Customer.io, they don’t affect your email domain’s deliverability ratings. When you check the Sent tab from your campaign, holdout messages show their status as Holdout, so you can differentiate between your real messages from your holdout tests. --- ## A/B tests URL: https://docs.customer.io/journeys/a-b-test-campaigns/ A/B testing lets you test different versions of email, push notifications, and SMS messages in your campaign, so you can measure which version performs better. For email, this means you can test subject lines against each other, different versions of your call to action, or try out new designs. For SMS and push notifications, you can test for content, imagery, or even deep links. You can test multiple messages in every triggered campaign, though we do recommend sticking to making just one change in your variation. This allows you to get an accurate picture of how that change measures up against the original without being confused by other data points. How do I set up an A/B test? For either email, Push, or SMS messages, this process is the same. First things first, you’ll want to head to the campaign where you want to run your test, and create the type of message you want to test—email or push notification. Then, on the message you’d like to add a test to, you’ll see a Turn into A/B Test button.  Want to run a hold-out test instead? Use a Random Cohort Branch to run a test in which some members of your audience receive a message and the rest don’t. Learn how to run a hold out test. By default, 100% of the traffic goes to the original and 0% to the variation. Make your changes to the variation. You can change most anything about the message. In the case of email, this could be your from address, subject, body, even the sending mode (“Queue Draft” vs. “Send Automatically”). For push, feel free to test your push title, body content, or get creative with custom payloads. Any delays and time windows are shared across both versions, though. We start routing traffic as soon as your campaign is live. If you have a low volume of messages being sent or are just starting with A/B testing, try testing a small subject line or content change, and measuring opens. Check in on your changes Once you have an A/B test running in an active campaign, you should see an A/B Test tab appearing in your campaign overview. That’s where you can go to see the results of the campaign and pick a winner. When you have statistically significant results, or if you want to end the test before that, you can pick one of the options as the winner by clicking the “Select winner” button. We’ll remove the other option and remove that particular A/B test from the screen.  After you end the test, you will no longer be able to see your non-winning content or your A/B test results. Understanding your test results Want more information about what statistical significance means or how we’re calculating different things? Read about it in more detail here. --- ## Copy workflow items URL: https://docs.customer.io/journeys/copying-workflow-items/ Do you have a successful campaign workflow you want to replicate or a fine-tuned set of messages in one campaign that you want to re-use in another? No problem! You can copy workflow items from one campaign to another, even if your campaigns are in different workspaces!  To copy a workflow AND campaign settings, copy the campaign instead! You can duplicate a campaign from the campaigns page or an individual campaign. Copy items from another workflow When you copy items from another workflow, not all of the copied items’ settings carry over to the destination workflow. For instance, if you copy a Create or Update Person action that references trigger data from another campaign, the same logic might not work in the current campaign depending on its trigger. This is especially true when you’re copying items from a different workspace. Make sure that copied workflow item settings apply to your current workspace and campaign before you save changes in your workflow. Click Copy from workflow at the top of the Build menu. Select the Workspace and Campaign (or broadcast) that you want to copy workflow blocks from. Click and drag to select the workflow items you want to copy. Or check Select entire workflow to copy everything. Click Continue to place items. Click to place your copied items in your current workflow. Copy items to another workflow When you copy items to another workflow, not all of the copied items’ settings carry over to the destination workflow. For instance, if you copy a Create or Update Person action that references trigger data to another campaign, the same logic might not work in the other campaign depending on its trigger. This is especially true when you’re copying items to a different workspace. Make sure that copied workflow item settings apply to your current workspace and campaign before you save changes in your workflow. To copy items from your current workflow to another one: Select the items you want to copy to another workflow. Click Copy to…. Select the Workspace you want to copy items to. Select the Campaign or API-triggered broadcast that you want to copy items to. Click to place your copied items into the workflow. Save your changes: Click Save & close to return to the workflow you copied items from. Or click Save & go to Workflow to go to the campaign you just copied items to. Sending behavior when copying The sending behavior of a copied message depends on the campaign’s status: If you copy messages into an active campaign, copied messages default to Queue Draft, regardless of their original states. This prevents you from accidentally sending messages before you have a chance to edit the copied messages to fit your current workflow. If you copy messages into an inactive campaign, the Sending Behavior doesn’t change. For example, let’s say you copy an SMS that is set to Send Automatically. If you copy into: An active campaign: the duplicate SMS is set to Queue Draft. An inactive campaign: the duplicate SMS remains set to Send Automatically. To change a message’s sending behavior, click the message block and adjust the dropdown: Copy between workspaces You can copy workflow blocks across workspaces. However, we won’t copy settings that typically don’t transfer across workspaces like: segments email layouts reply-to and from addresses some action conditions SMS, Slack or push notifications if these modules aren’t enabled in the destination’s workspace settings Action conditions If you copy workflow blocks that have action conditions, some settings may not copy over to the workspace. This is because your new workspace may not contain the same options as the original workspace—like segments and subscription preferences. Action Condition Copied? Why? Segment conditions ❌ Segments are workspace-specific, so you must re-define segment conditions on workflow items that you copy across workspaces. Attribute conditions ✅ While we copy attribute conditions over to the new workspace, you’ll need to review them to make sure they’re correct. Event data conditions ✅ While we copy event data conditions in event-triggered campaigns, you’ll need to review them to make sure they’re correct. Subscription preferences Sometimes Subscription preferences are copied if they match the campaign settings or global options. Subscription preferences are not copied if you’ve overridden the subscription preference to a specific topic. In this case, the preference resets to Use campaign settings because your new workspace may not contain the same topics. For example, consider an email with the following action conditions: If you copy this email to another workspace, the “Canada” location attribute copies normally, but the Profile > 50% segment condition does not. Instead, you would see the following warnings on your copied email: If you want the copied email to have the same conditions as the original, you must: Re-create the segment and re-add it as an action condition. Make sure the location attribute exists in the workspace you’re copying to. Email layouts, reply-to, and from address When you copy messages from one workspace to another, the Layout, Reply-to and From addresses reset to the default in your destination workspace! Your copied message may display warnings reading “Email layout was reset to default” or “Headers were reset to default.” To address these warnings, go to the editor and check that the Layout, Reply-To, and From Address settings are correct for your copied message and campaign. Non-email messages If you’re copying SMS, Slack, or push notifications between workspaces, make sure you’ve enabled those action types in your destination workspace! If they aren’t, you won’t be able to copy these workflow blocks over. With in-app messages, you can copy them even if they’re not officially enabled in workspace settings. --- ## Create or update person URL: https://docs.customer.io/journeys/create-update-person/ This action lets you update a person. You can update the person currently in your campaign workflow, or you can update a related person. If this related person doesn't exist, this action creates a new person! How it works You can use the Create or update person action in any campaign workflow. When you update the person currently in your campaign, you can set attributes from static values, the event that triggered your campaign, other profileAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. attributes—or any other value you could otherwise set with 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}}. or JavaScript. If you want to update another person—or you use the action in a webhook-triggered campaignA campaign triggered by an incoming webhook, in which the data is the subject of the campaign instead of a person. Webhook-triggered campaigns help you integrate with external APIs without writing your own code or using a middleware product like Zapier. From a webhook-triggered campaign, you can manipulate your incoming data to create people, update people, and trigger events.—you’ll select a value representing the other person’s identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. If the person exists, we’ll update their attributes accordingly. If the person does not exist, we’ll create them and set their attributes accordingly. You might use this action to: Update a person’s subscription date for “account anniversary” campaigns. Calculate a customer’s lifetime value (with a total_purchase_value attribute, for example). Create a person based on an incoming webhook. Set a customer’s next appointment time.  You don’t need a delay after this action Your campaign workflow will automatically wait for the attribute update to finish before moving to the next step. You don’t need to add a delay after a Create or update person action—the workflow won’t proceed until the update finishes. flowchart LR a{Is the update for the current person}-->|Yes|b[Update current person] a-->|No: someone else or webhook-triggered campaign|c{Does this person exist?} c-->|Yes|d[Update other person] c-->|No|e[Create new person] Create or update a person In the Workflow step of your campaign: Drag the Create or update person block into your workflow. Give the action a Name and click Add Details. You can also set Action Conditions if you don’t want everybody in your workflow to trigger this action. Select whether you want to update The person in the workflow or Someone else. If you select Someone else, you’ll need a value to use as an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for that person. Click Add attribute and set attribute values for the person. Attribute represents the name of the attribute you want to set. Value is the type of value you want to populate your attribute with. For example, if you want to use an event property as the person’s attribute value, select Event attribute. Then in the field to the right, choose the event attribute.  You can set nested attributes with JSON dot notation Use JSON dot notation to set attributes inside other attributes or create an array of values. See Storing and using JSON for more information about dot notation in Customer.io. Click Save Changes when you’re done.  Use the Remove attribute option if you want to unset a person’s attribute Setting an attribute value to an empty string or null is the same as removing that attribute from a person.  You don’t need a delay after this action Your workflow automatically waits for the attribute update to complete before moving to the next action. You don’t need to add a delay after a Create or update person action—the workflow won’t proceed until the update finishes. Webhook-triggered campaigns or the Someone Else option When you use Create or update person in a webhook-triggered campaignA campaign triggered by an incoming webhook, in which the data is the subject of the campaign instead of a person. Webhook-triggered campaigns help you integrate with external APIs without writing your own code or using a middleware product like Zapier. From a webhook-triggered campaign, you can manipulate your incoming data to create people, update people, and trigger events. or select the Someone else option, you’ll select an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. that we’ll use to find or create a person. Customer.io looks for the person represented by this value. If that person exists, we’ll update them normally. If we can’t find a person matching the identifier you provide, we’ll create one. In general, this means that either the incoming data or the current person in your workflow must have a value representing another person. Set attributes In most campaigns, you can set an attribute to one of these data types: Profile attribute Journey attribute Static value Liquid Javascript In campaigns triggered by events, form submissions, objects, or relationships, you can also set an attribute equal to trigger data. For event or form-triggered campaigns, you’ll choose “Event attribute” from the dropdown then click beside an event attribute from the left-hand panel of sample data. For object or relationship-triggered campaigns, you’ll choose “Trigger object attribute” or “Trigger relationship attribute” from the dropdown. Then choose an attribute from the list of available values. To see a preview of the payload, make sure you select a person from the sample data on the left who has a relationship to the object. Using liquid in attribute updates You might use liquid to manipulate the values you set as attributes—if you need to convert timestamps, append values, or access a specific set of values from an array, etc. See the liquid tag list for a list of ways you can transform data. An attribute can contain JSON. However, when you reference an array or object, you’ll need use | to_json. By default, liquid maps objects and arrays to strings (or integers, where applicable). Use this tag, like {{event.object | to_json}} when you reference an object or array to avoid errors and maintain the original shape of your data! Use JavaScript in attribute updates You can manipulate attribute values with JavaScript. When using Javascript, you’ll write a return statement for each attribute value you want to set. You can set properties from any source available in your campaign—trigger data, attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., event properties, snippetsA common value that you can reuse with Liquid in messages and other workflow actions—like your company address. You can store Liquid inside a snippet, making it easy to save and reuse advanced values and statements across your messages., etc. For example, if you wanted to access a person’s id from within the identifers object in your campaign’s trigger, you would write return triggers.identifiers.id;. See our JavaScript quick reference guide for more examples to help you take advantage of JavaScript in your workflow.  You can’t use Liquid inside JavaScript When you use the JavaScript option, you must manipulate values with JavaScript. If you try to return a snippet value that contains Liquid, you’ll receive an error. --- ## Set journey attributes URL: https://docs.customer.io/journeys/set-journey-attributes/ Journey attributes help you temporarily store data that you want to use in your campaign—like properties from a webhook or data from a collection query. Unlike customer attributes, journey attributes don’t persist on a person’s profile—they automatically expire when the person exits the campaign. This keeps your workspace data clean while still letting you use data for personalization, conditions, and branching within a workflow. How it works You can set journey attributes in three ways: Method Use case Set journey attributes action Set values directly based on profile attributes, trigger data, liquid, or JavaScript. For instance, you might do this to temporarily store a discount code while a person is actively in a campaign. Webhook response Store data returned from an external API. Learn more on setting journey attributes in a webhook action. Collection query Store results from a collection query. Learn more on setting journey attributes in a collection query action. After you set a journey attribute, you can use it to: Personalize messages with this liquid syntax: {{journey.<attribute_name>}} Create branch conditions to send people down different paths Set action conditions on messages, delays, or other blocks flowchart LR a[Set journey attribute from webhook, collection, or action block] a-->b[Use in messages with liquid] a-->c[Create branch conditions] a-->d[Set action conditions] b-->e[Journey ends] c-->e d-->e e-->f[Journey attributes automatically deleted] style f fill:#FFC4CF,stroke:#69002C Set journey attributes action To set journey attributes in a campaign, follow these steps: Drag the Set journey attributes action into your workflow, and then configure the attributes you want to set. Click the action Name to add your own descriptor. You can also set action conditions if you don’t want everybody in your workflow to trigger this action. Click Add details. Click Add attribute to set your journey attribute. Attribute is the name of the journey attribute. Reference it later in liquid as {{journey.<attribute_name>}}. In the example above, you could use {{journey.discount_code}} in a subsequent message. Value is the type of data you want to set for your attribute—a profile attribute, trigger attribute, a static value, liquid, or JavaScript. Learn more about setting attributes. Click Save Changes when you’re done.  You don’t need a delay after this action Your campaign workflow will automatically wait for the attribute update to finish before moving to the next step. You don’t need to add a delay after a Set journey attributes action—the workflow won’t proceed until the update finishes. Limits on journey attributes Overall limit: 100 journey attributes per journey. You can create or change up to 100 journey attributes during a single journey. Updates beyond this limit fail, and the person moves forward without the update. Attribute name limit: 128 bytes Attribute value limit: 100 KiB Use journey attributes in your workflow You can reference journey attributes in messages and conditions for actions like branches and emails. If the value of your journey attribute contains liquid syntax, make sure you add {% render_liquid %} around your liquid object so it pulls in data dynamically. Otherwise, it will render as static text. Reference journey attributes in messages Use the journey key in liquid to include journey attribute data in messages. If the attribute value is an object (not an array), access properties directly: {{ journey.promo_course.name }} {{ journey.promo_course.description }} If the data is an array, you can loop through the data: {% for item in journey.course_recommendations %} {{ item.name }} {% endfor %} You can also use liquid filters like first, last, and map to access specific items in the array: {% assign first_course = journey.course_recommendations | first %} {{ first_course.name }} How to reference journey attributes with nested liquid You might store attributes containing liquid syntax, like when you store personalized email content generated from an LLM action or webhook. For example, imagine that you have a journey attribute named body with the following value: "body": "Hello {{customer.first_name}}!" If you reference this journey attribute in a message with {{journey.body}}, the body field won’t be evaluated! Instead it’s as static text! So you’ll see “Hello {{customer.first_name}}!” instead of “Hello Alex!” To render liquid syntax dynamically, you need to wrap the liquid object in {% render_liquid %}, like {% render_liquid journey.body %}. Then, when the message sends, we’ll evaluate {{customer.first_name}} and render “Hello Alex!” as you’d expect. Preview journey attributes in messages To preview journey attributes, you need to provide sample data. Otherwise, we won’t render anything in the preview: Open the Sample data panel in the message editor. Select the Journey tab. Enter test values under your attribute names. In the message preview, you’ll see the sample value render. Like with any other liquid variables, if we can’t retrieve a value for the journey attribute when we attempt to send the message, the message will fail to send. Reference journey attributes in conditions After you set a journey attribute, you can use it in any condition in your workflow—including branch conditions and action conditions. For example, you could send people down different paths in a multi-split branch based on a journey attribute value. You could also only send an email to people when their journey attribute contains a certain value. --- ## Fix typos in attributes URL: https://docs.customer.io/journeys/how-to-use-attribute-updates-to-fix-typos-in-your-data/ Have you ever added data to Customer.io and notice typos after the fact? You can create a campaign and use the *Create or Update Person* action to fix typos and make sure that future updates are clean and typo-free. In this example, let’s say you have an attribute called frist_name that should be first_name instead. 1. Create a segment to capture the misspelled attribute Create a new segment that captures people with the misspelled attribute. In this case, we’re looking for people with a frist_name attribute. 2. Create a campaign using your segment Use that segment as the trigger condition in a new campaign. If misspelled data is still coming into your workspace, make sure that your campaign is re-enterable, so that you fix the typo whenever it reoccurs. Since this campaign won’t send any messages, you’ll want it to “send” to unsubscribed people as well. 3. Use Create or Update Person to fix the attribute Drag the Create or Update Person action into your workflow. You’ll use this to set a correctly-spelled attribute and remove the old, misspelled attribute. Set a new attribute with the correct name using the value from the misspelled attribute. In this case, we’ll create a first_name attribute using the value from the old frist_name attribute. Remove the old, misspelled attribute. 4. Start your campaign. On the review page, make sure you select Current people and future additions. This makes sure that your campaign corrects the typo for all your current people and will correct typos that come up in the future. --- ## Reformat timestamp attributes URL: https://docs.customer.io/journeys/how-to-use-attribute-updates-to-reformat-timestamps/ In Customer.io Journeys, you need to format timestamps as Unix epochs to full take advantage of them. If you've already uploaded data with timestamps in a different format, you can use the *Create or Update Person* action to reformat your timestamps. 1. Create a segment with the attribute you want to reformat When you create your segment, you can use the is not a timestamp condition. This makes sure that you only update attributes that aren’t formatted correctly. 2. Create a campaign using your new segment When you create your campaign make the campaign re-enterable, so you can reformat the timestamp whenever it appears. This campaign won’t send any messages, so you’ll want it to “send” to unsubscribed people as well. 3. Use the Create or Update Person action to fix the typo The only action in your workflow should be a Create or Update Person block. You’ll replace the current attribute value with a properly formatted timestamp using Liquid filters or JavaScript. In this example, we’re assuming that our initial value is an ISO date or date-time. If you use Liquid, your code changes depending on whether your initial value is a complete ISO-8601 timestamp or a date-only value in YYYY-MM-DD format. For more about converting dates using Liquid, see our Liquid documentation. JavaScript JavaScript return new Date(customer.date_of_birth).getTime() / 1000; Liquid ISO date-time Liquid ISO date-time // if date_of_birth has a value like: "1983-03-15T12:00:00Z" {{customer.date_of_birth | date: "%s"}} Liquid ISO date-only Liquid ISO date-only // if date_of_birth has a value like: "1983-03-15" {{customer.date_of_birth | append: " 12:00:00" | date: "%Y-%m-%d %H:%M:%S" | date: "%s"}} 4. Start your campaign On the review page, select Current people and future additions, so that you correct the timestamp on all current and future people. --- ## Types of branches URL: https://docs.customer.io/journeys/branches/ Branches help you personalize campaigns or test different messages in a campaign. When people reach a flow-control block in your workflow, we'll evaluate criteria to determine which path to send them down. Flow controls and branches Branches help you personalize campaigns or test different messages in a campaign. When people reach a flow-control block in your workflow, we’ll evaluate criteria to determine which path to send them down. We offer three types of branching flow controls that you can place anywhere in your workflows: True/False Branches: Send people go down one path if they match a condition, or down the other path if they don’t. Multi-Split Branches: Send people down different paths where each path has its own conditions. Random Cohort Branches: Send people down different paths based on a random distribution. This is a great way to test different flows in a campaign, like a more robust version of our A/B testing feature for individual messages.  You can use JSON dot notation in condition logic If you store attributes or event data in JSON objects or arrays, you can use JSON dot notation in your branch conditions to evaluate these properties. Use array[] to represent any item in an array or array[0] to represent the first item in the array. See Storing and using JSON for more information about dot notation in Customer.io. Walkthrough of different branch types Check out this walkthrough to understand how you could use each branch type: Moving branches You can move branches like any other items in the workflow. When you click and drag a branch, you’ll carry everything along its paths. That way, wherever you drop the branch, everything stays together. Moving delays in active campaigns If a branch contains a delay that currently has people waiting in it, and you move the branch, the people in your campaign will move with the delay. Deleting branches You can’t delete a branch until you’ve removed all items from it. If you need to delete a branch, first delete or move all items out the branch’s path. Then, select the branch and click Delete. Copying branches You can copy branches to/from other workflows by clicking Copy from workflow at the top of the Build menu or selecting a branch then Copy to at the bottom. --- ## Multi-Split Branches URL: https://docs.customer.io/journeys/multi-split/ A multi-split branch sends people down different paths based on one or more conditions. People go down the first path they match. How it works A multi-split branch sends people down different paths based on one or more conditions: Profile attributes Segment membership Events objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. attributes relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. attributes In this video, we show a campaign to convert people from free to paid plans, and we’ll add a multi-split branch to nudge people to download the company’s mobile app. What if a person could match multiple paths? A person flows down the first path they match moving from left to right (or, looking at your conditions, from top to bottom). If they do not match any path, they will flow down the default “All others” path on the right. Set up a multi-split branch By default, the multi-split branch has two, empty paths and a third “All others” path. These paths have no conditions, so they default to false and customers will skip them until you add conditions. Drag a Multi-Split Branch into your workflow and select it to edit its conditions. Give your branch a Name. Choose the Data Type you want to use to split your audience. The options change depending on the trigger for your campaign. For example, you can only use event attributes for event-triggered campaigns, and you can only use object trigger attributes in campaigns triggered by objects or relationships. Define your paths. Add more paths if you need. Click Save and you’ll see the paths on your canvas update to match. Split based on people’s attributes To split based on the value of a single attribute, click Attribute in the Data type dropdown. Select the attribute you’d like to split on in the Attribute name dropdown that just appeared. And finally, enter the values of the attribute that you’d like to split on in each of the Path text fields. To add another path, click Add. Click is equal to if you’d like to split based on a range of values.  Splits are evaluated from left to right, so make sure your conditional logic works in that order. Add up to 20 paths, and click Save to save the paths and collapse the sidebar. Split based on event attributes  You can only split based on event attributes when a campaign is triggered by an event. To split based on the value of an event attribute, click Event attribute in the Data type dropdown. The event that triggered the campaign will appear in the group box along with a text field to specify which event attribute you would like to split on. Enter the event attribute name in the text field. Finally, enter the values of the event attribute that you’d like to split on in each of the Path text fields. To add another path, click Add. Click is equal to if you’d like to split based on a range of values.  Splits are evaluated from left to right, so make sure your conditional logic works in that order. Add up to 20 paths, and click Save to save the paths and collapse the sidebar. Split based on object or relationship attributes To split based on the value of a relationship attribute, click Relationship from the Data Type dropdown. Select the object type and attribute type. Then enter the value for each pathway. To create another path, click Add.  Splits are evaluated from left to right, so make sure your conditional logic works in that order. For object and relationship-triggered campaigns, you can also split based on object trigger attributes. Click Object-type-name (Trigger) from the Data Type dropdown. You can add up to 20 paths. Click Save when you’re done. Split based on segment membership To split based on a segment match, click Segment in the Data type dropdown. Select the segment that you’d like to split on in each Path. To add another path, click Add. Add up to 20 paths, and click Save to save the paths and collapse the sidebar. --- ## Random Cohorts URL: https://docs.customer.io/journeys/random-cohort/ A Random Cohort action randomly distributes people across up to 20 different paths in your workflow. It provides a way to test different messages or actions in a campaign. How it works A Random Cohort action randomly distributes people across up to 20 different paths in your workflow. It provides a way to test different messages or actions in a campaign. If you use custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in your workspace, pay attention to the Cohort by setting. By default, we add people to random cohorts. But if you use custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in your workspace, you can group people into cohorts by a related object so people in the same company, account, class, or other object “group” travel down the same path. See Random cohorts with custom objects for more details. In this video, we show a campaign to convert people from free to paid plans, and we use a random cohort branch to randomly distribute people down different paths to test the best time to send an offer. Set up a random cohort branch When you drag a Random Cohort Branch into your workflow: Pick the Cohort by setting. If you use custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in your workspace, pick Objects Otherwise, leave the setting set to Profile. Profile: people are added to random cohorts as individuals. Objects, like Companies, group people into cohorts by a related object. This ensures that people related to the same object—in the same company, account, class, or other object—all travel down the same path. If someone belongs to multiple objects, they’ll exit the campaign immediately. See Random cohorts with custom objects for more details. Add up to 20 paths and set the percentage of cohorts you want to send to each path. The total distribution must add up to 100% before you can save your changes.  Small sample sizes may not reflect your exact distribution settings Because random cohort distribution is truly random, you may not see people distributed exactly as you expect until your campaign experiences a statistically significant sample size. The larger the sample size, the easier it is to reach an exact distribution. Random cohorts in active campaigns When you drag a Random Cohort into an active campaign, one path is set to 100% and the other to 0%. While you can add more paths, you should leave the distribution at 100% for the first path and 0% for all other paths. Then you can edit your new paths without people flowing through them right away. This helps you prevent unintended consequences as you add paths to your campaign. When you’re done editing your campaign to account for the new paths, return to your random cohort branch and update the distribution to reflect your new paths. Cohort by: random cohorts with custom objects Random cohort blocks typically send people down different paths in your workflow. But imagine a scenario where you want to use a random cohort but you want to make sure that people related to the same account all travel down the same path—you don’t want people in the same account to have different experiences! That’s what the Cohort by setting in a random cohort branch does: it treats custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. as cohorts rather than profiles, so people related to the same object all travel down the same path. When someone reaches a random cohort block, we check their cohort. If the cohort has already been assigned to a branch, they’ll go down that branch. If the cohort has not been assigned to a branch, they’ll go down a random branch—and future members of the same cohort will follow the same path. If someone belongs to multiple objects of the same type, we won’t know which cohort to assign them to, and they’ll exit the campaign immediately! If people could have relationships to multiple objects of the same type, you should use the Cohort by: Profile option instead. flowchart LR a(Person reaches a random cohort block) a-->b{Has the person's related object been assigned to a branch?} b-->|No|c(Person goes down random branch) b-->|Yes|d(Person goes down the same branch as other people related to the same object) --- ## True/False Branches URL: https://docs.customer.io/journeys/true-false/ A true/false branch sends your audience down different paths based on conditions. Customers matching the conditions go down one path; customers who don't match the conditions go down the other. How it works A true/false branch sends your audience down different paths based on conditions. Customers matching the conditions go down one path; customers who don’t match the conditions go down the other. You can create a true/false branch based on: profile attributes segment membership events messages (like if a certain email has ever been clicked) objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. attributes relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. attributes Set up a true/false branch Drag a True/False Branch anywhere in your workflow. The branch doesn’t have any conditions by default; in this case, it’ll send people down the False path. Select the branch to define your true/false conditions. Name your branch in a way that makes sense to you and your teammates. Click Add condition and choose to add an attribute, event, segment, message, or relationship condition. If you’re creating an object or relationship-triggered campaign, you can also add an object trigger condition. Set the details of the condition and click Add. Repeat this process to add other conditions. When you’re done, click Save. When you hover over your branch, you’ll see the conditions you’ve set.  True/False branches do not support OR conditions You cannot combine multiple conditions with “OR” logic within a single True/False branch. For more complex conditional routing, consider using a Multi-Split branch. --- ## Exit Blocks URL: https://docs.customer.io/journeys/exit-blocks/ Exit blocks allow you to force a set of users who are following a given path to exit the campaign. How it works Normally, people leave a campaign when they reach the end. But if you set up branches and flow controls, you may want to allow people to exit the campaign at different points along different paths. That’s what exit blocks are for! You can place exit blocks along different branches in your workflow, letting people gracefully exit the campaign along different paths in your workflow. For example, imagine that you have a multi-split branch configured to send messages to anyone whose favorite color is red or yellow, but you want to make sure that everyone else does not receive messages from this branch and leaves the campaign. You can place an exit block in the ‘All others’ branch so anyone whose favorite color isn’t red or yellow exits the campaign immediately. Add an exit to a branch Drag an Exit Block into your workflow. You can only place exit blocks at the ends of branches. When you drag an exit block, you’ll see a ‘+’ icon appear in all the places you’re allowed to place it. Delete an exit block To delete an exit and reconnect the path to the workflow, click the Exit block and click Delete. --- ## Time Window URL: https://docs.customer.io/journeys/delivery-window/ To make sure that a message gets delivered within a very specific time range, you can set a time window. For example, you can set a message to only send during your business hours, during specific hours in your user's time zone, or prevent messages from sending on the weekends. Whatever works best for you! This video shows each of our delay options. Click to fast forward to the Time Window section. How to set up a time window Within your campaign workflow, the Time Window option will be available to drag and drop into your workflow builder from the sidebar. If there are already other items in your workflow you’ll need to choose where you want to place your Time Window. Then, you can customize when you’d like the user to advance to the next step in the workflow. Choose specific days, specific times, or even your user’s time zone!  Set your audience’s timezone attribute If you select to match the user’s time zone (at: specific times…in: the user’s time zone), you need to send us the timezone attribute. You can also set a fallback time zone. Example with multiple messages When a message has a delivery window, later messages are pushed back to accommodate it. For example, in a workflow like this: A two-day delay A time window action item set for 9AM - 5PM, Monday to Friday, in the user’s time zone. An email Another two-day delay A push notification Let’s say a person enters the campaign on Monday at 6PM. They: Wait for two days, until Wednesday at 6PM. This is outside the time window, so– They’ll wait again, until Thursday morning at 9AM. The first email will be sent. They will wait for another two days (until Saturday morning at 9AM) The push notification is sent. Note that a time window only applies to the message that follows it. If you want to deliver the push notification in the above example during a specific time window as well, you need to add one before that message. Changing a person’s timezone attribute while they wait in a time window Assume you’ve created a time window like so: The key here is you’ve selected a time range in the user’s time zone. There may come a time when you update a person’s timezone attribute while they are being held in a time window dependent on the user’s time zone. When this happens, we re-evaluate the user’s timezone attribute once they meet the time window’s conditions based on their previous timezone attribute. Let’s break that down with an example: A person enters a campaign with a time window based on the user’s time zone (like above). Their current timezone attribute is US/Eastern. They reach the time window and do not meet the conditions of the time window. In this case, this means it is not a weekday between the hours of 9 and 5pm ET. They are held in the time window. You update the person’s timezone attribute to US/Pacific. The person continues to wait in the time window until they meet the conditions based on their previous timezone attribute US/Eastern: a weekday between the hours of 9 and 5pm ET. It reaches 9am ET on Monday. We check the user’s timezone attribute and see it’s been updated to US/Pacific. 9am ET = 6am PT, so the person does not meet the time window conditions and continues to wait another 3 hours before exiting the time window. Also consider the case where the user’s timezone attribute is updated to a time zone that is ahead of their previous timezone attribute, not behind. Let’s say, in the above scenario, the person’s timezone attribute was US/Eastern, and you updated it to Japan (9 hours ahead) while they’re held in the time window. At 9am ET, we re-evaluate the user’s timezone attribute and see it is 6pm in their time zone. They do not meet the time window conditions, so we hold them until 9am in Japan, the next day. This is important if you have time-sensitive emails. Ultimately, the time window will respect the user’s up-to-date timezone attribute, but we do not re-evaluate it immediately. --- ## Wait Until... URL: https://docs.customer.io/journeys/wait-until/ Use the *Wait Until* item to hold a person in a campaign until they meet a condition or enter a segment. How it works You can set three different types of waits. Each wait type adds a new path in your workflow. A person progresses down the first path they meet conditions for. Conditions: Determine whether a person needs to achieve certain attributes, perform an event, join a segment, and so on before they can progress through the campaign. You can set multiple conditions. Event time (event-triggered campaigns only): Base your wait on a timestamp in your event data. Max. time: Set the maximum time a person can wait. If a person reaches the maximum wait time without achieving any other conditions, they’ll progress through the workflow. For example, if you set attribute conditions and a max time, a person will progress in your campaign if they either meet your attribute conditions or they reach the maximum wait time. flowchart LR y(person reaches 'Wait Until...' block)-->z z{does the person already meet conditions?} z-->|no|a z-.->|yes, person skips wait|c a[person enters wait]-->b{has the person met a condition?} b-.->|no|d{is there a max time?} b-->|yes|c[person moves to the next action] d-->|yes|e{has person waited for the max time?} e-.->|no, continue waiting|b e-->|yes|c d-.->|no, person waits indefinitely until they meet conditions|b  People who already meet your conditions will skip the wait If you set up a wait condition, and a person already meets the condition when they reach the wait, they’ll move on immediately. See Why did someone skip the wait? for more information. Each type of delay starts a new path You can set up different types of waits in the same actions, creating multiple paths for people in the journey. A person will progress down the first path they meet conditions for. You can either take advantage of different paths to send different messages based on the conditions the person met or, if the result of a conditional wait or a maximum wait is the same, you can place actions after the paths converge. Set up a Wait Until This video shows each of our delay options. Click to fast forward to the Wait Until section. Drag Wait Until… into your workflow. Click it and determine the conditions a person must meet before they continue the campaign. You can choose one or more types of conditions. However, a person will proceed through the campaign when they meet any of the following: Conditions: Attribute, event, segment, or message conditions that a person must meet (or not meet) to move on. Multiple conditions in the same path are joined with And—a person must meet all conditions to satisfy the wait; you can also set up multiple conditional paths (joined by Or). Event time: A timestamp specified in an event that triggers your campaign. For example, if you trigger your campaign with an event called event_reminder you could Wait until the remind_me_at timestamp in your event to send a reminder message. Your timestamp should be in Unix seconds since epoch. Max. time: The maximum time that a person can wait before moving on in the workflow. Conditions When you set your Wait until delay, you can use attribute, event, segment, or message conditions. Within a path, can set multiple conditions joined by AND—meaning that a person must meet all conditions to progress down the path. You can also set multiple conditional paths. See the section below for more information about setting up multiple condition-based paths. Attribute: Wait until a person matches a profile attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. condition—whether a person has an attribute, doesn’t have an attribute, matches an attribute value, etc. Event: Wait based on whether a person has, or has not, performed an event. Unlike Event time delays, this does not have to be an event that triggered your campaign. You can also specify event properties to narrow down the condition. Segment: Wait based on whether or not a person belongs to a segment. See the Not in segment condition for more information about matching folks who aren’t in a segment. Message: Set a condition to check if someone has been sent or is sent a message. You can also base message conditions on other statuses like delivered, opened, clicked, and more. Choose messages from any campaign, broadcast, or transactional message in your workspace. For example, if your campaign follows up on a person’s order, you might wait until after they receive a transactional message containing their receipt. The following conditions are only available in object- or relationship-triggered campaigns: Object (Trigger): Wait until a condition on the object that triggered a campaign is true. For example, you could set up a wait until Order.status is equal to delivered or Subscription.active is equal to true. Relationship: Wait until a condition on a relationship to an object is true. For example, if your objects are online courses, you could set up a wait until a person’s relationship attribute for enrolled is equal to true. This can be a relationship to any object, not just the triggering object! So you could trigger a campaign when someone signs up for an account (i.e. an account object is created). Then you could have them wait until they’ve activated a subscription, where subscriptions are separate objects.  Wait Until event conditions aren’t available in liquid While you can reference an a different event than the one that triggered your campaign as a wait condition, you can’t use properties from this event later in your campaign with 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}}.. The liquid event context is always the event that triggered your campaign. Event property conditions When your Wait Until condition is an event, you can click Add event data filter to evaluate the wait condition based on fields in an incoming event. You can even evaluate properties in your condition against a person’s attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or the trigger event’s properties. This helps you fine-tune the paths people might take in journeys. For example, imagine that a person enters a campaign when they set up an appointment—they generate a new_appointment event with an appointment_id. You want to wait until the day before the event to send an email reminding a person about their appointment—but you don’t want to send a reminder if they already cancelled the appointment. In this case, you can set up a Wait Until… condition to look for an cancelled_appointment event where the appointment_id matches the trigger event’s event.appointment_id. If the two values match, you don’t send the appointment reminder. Learn more about editing wait until blocks in live campaigns. Evaluating multiple conditions When you set conditions, you’ll see AND; these conditions are a part of a single path; a person must meet all of the conditions to progress down a path. But, unlike Event time and Max. time, you can set up multiple Condition-based paths. Each set of conditions creates a branch in your workflow. If a person meets the conditions in any path, they’ll progress down the associated path. Paths for your different wait conditions are numbered, starting at 1. In the workflow, they’ll read left-to-right. If a person meets the conditions in multiple paths simultaneously, they’ll go down the path closest to 1 (farthest to the left) in your workflow. Message conditions You can set conditions based on whether a person has ever been sent or is sent a message. You can also base message conditions on other statuses like delivered, opened, clicked, and more. Choose messages from any campaign, broadcast, or transactional message in your workspace. In a message condition, you can check if a person has ever been sent a message, or wait until a message is sent after a person enters the wait. The has ever condition is based on whether or not a person has ever been sent (or another message status like delivered) a particular message. If you specify “has ever been sent,” and the person was sent a message before they entered the wait until, they meet the condition and will move on in the workflow. The is condition is only satisfied if a person is sent (or another message status like delivered) a message after they enter the wait. This prevents people from inadvertently meeting wait until conditions based on messages sent long before a person entered your campaign or wait. If you specify “is sent,” and the person was sent a message before they entered the delay, they do NOT meet the condition. If they were sent a message after entering the wait until, they do meet the condition and will move on in the workflow. Object and relationship conditions In Wait Untils, you can set conditions based on objects or relationships when your campaign is triggered by an object or relationship. Other campaign types won’t have this option. You can set conditions based on any object or relationship in your workspacel it’s not limited to trigger data. If your campaign is triggered by an object or relationship, you’ll also see the option to have them wait based on trigger data. Here are a few examples: Object trigger condition: You want someone to wait until a date associated with your triggering object, like an appointment time, before they receive a message. Relationship to trigger object: A campaign triggers when people express interest in a webinar (a relationship is created with a webinar object). You want people to wait until a week before the webinar before you send them a reminder. Relationship to object that’s not the trigger: A campaign triggers when someone signs up for your product (i.e. an account object is created). You want people to wait until they’ve activated a subscription, where subscriptions are separate objects, before sending them further messages. In this example, you’ll see a couple conditions based on the trigger object: Event time If your campaign is triggered by an event, you can make people wait until a timestamp value in the triggering event before they move on to the next step in the workflow. For example, if you trigger your campaign with an event called new_appointment, you could make people wait until the day before the appointment_time in the triggering event and then send a reminder message. The timestamp in your event can be either: Unix timestamp (in seconds) RFC 8601-formatted time (for example, 2022-06-04T10:24:34-0400). Unlike event conditions, your timestamp must be a part of the event that triggers your campaign. You can’t base this time on a different event.  Make sure your triggering event includes a timestamp and that it’s correctly formatted If you send an event that either doesn’t contain the timestamp specified in your wait, or the timestamp is incorrectly formatted, your event timestamp condition won’t do anything. Your audience will wait until they meet another condition. If your wait doesn’t have any other conditions, a person could wait forever! Note, people will wait until the current time is equal to or after the time specified by the wait condition. For instance, if you set an event time path of “2 days before the time specified in the appointment-time” of the trigger event, then people will meet the condition and move forward when it reaches not just before 2 days, but also within 2 days of the appointment_time. It’s possible then that some people move down this wait until condition if it’s 1 day before the appointment time. To demonstrate that last statement, let’s continue with the example of a campaign triggered by scheduling an appointment: It is June 13, and an appointment is just now scheduled for June 14 which triggers the campaign. The first step in the campaign sends a confirmation email immediately. The second step is the wait until set for two days before the appointment, which means before or after 2 days. Because it’s 1 day before the appointment, the person passes the condition immediately and moves down that path. The campaign immediately sends an email reminder for the appointment. Since the current time is already after the wait until condition of “2 days before appointment_time”, the person is not held in the wait until and we consider that they’ve successfully met the condition. In this example, the person would receive the confirmation email and reminder email close together. To avoid this behavior, you can add a True/False branch to your workflow. For this example, you could set an event attribute condition to “appointment_time is a timestamp before 2 days from now”. When this is true, people move forward but don’t receive a reminder email. If this is false, people would enter a wait until and eventually receive a reminder email only 2 days before the appointment time. The max time fallback By default, people will wait until they meet your conditions. If they never meet your conditions, they could get stuck waiting in your campaign forever! Use the Max time option to let people exit the campaign or move on to the next action. In this example, if a person isn’t in the specified segment after one week, they’ll move on to the next action in the workflow. Segment “Not in…” conditions If you base your wait condition on whether people are not in a segment, people who were never in the segment will skip the wait. If people are already in the segment, they will be held in the wait until they leave the segment. Why did someone skip the wait? If a person meets your wait conditions before they enter the wait, they’ll skip it entirely. We evaluate the conditions as soon as someone reaches the Wait until block; someone doesn’t have to meet the conditions again to progress past it. flowchart LR y(person reaches 'Wait Until...' block)-->z z{does the person already meet conditions?} z-->|no|a(person begins waiting) z-.->|yes, person skips wait|c c(person moves to the next action) For example, imagine that someone performs an event—they signed up for a class at a local school. You want to send them a reminder message a week before the class starts. If someone signed up late for the class, and the class starts in less than a week, they’ll skip the wait and move on to the reminder message, which you can see on the person’s journey. Modify Wait Until blocks in live campaigns Change event conditions You can edit the conditions of your Wait Until if the campaign is live; just be aware that doing so might cause people to quit waiting. For example, if you reduce an Event time condition from seven days to two days: People who have already waited more than two days will move on (or exit the campaign) People who have not yet waited two days will continue waiting Delete a Wait Until If you delete a Wait until, you can choose what to do with anybody currently waiting. Continue to the next action in the campaign Exit the campaign immediately It’s up to you! If people have been waiting a long time, you may not want to continue sending messages in the campaign. Copy a Wait Until to other campaigns It’s easy to copy the Wait Until between campaigns within the same workspace, because your segment conditions remain the same. If copying between workspaces, you’ll need to re-add your conditions, and we’ll show you a warning note both in the workflow and when editing the item. Combining delay blocks Meeting wait until conditions doesn’t ensure that someone receives your message at the right time; and it being the right time doesn’t mean that someone’s met your conditions yet. You can use conditions together so people meet your conditions and receive messages in the right timeframe! For instance, you could wait for people to match specific conditions using a Wait Until block, but you’d need to use a Time Window if you also wanted people to only receive a subsequent message during a specific timeframe. Combine Wait Until and Time Delay You might add multiple flow control blocks to a campaign to ensure people don’t receive a message too soon. For instance, maybe you want people to minimally wait 7 days after receiving their first message, and then they should receive their next message only if they’re a member of a specific segment. Time Delay: Wait 7 days Wait Until: Person enters the segment Email Make sure you check the order of your blocks so you don’t make people wait unnecessarily in your workflow. You might switch the delay with the Wait Until if you want to make sure that people wait exactly seven days after satisfying the wait condition. Wait Until: Person joins the segment Time Delay: Wait 7 days Email For Wait Untils, consider adding a second condition, like a max wait, so people aren’t held forever in the flow. This is also important if the subsequent message is time sensitive. Combine Wait Until and Time Window With both Time Windows and Wait Until blocks, you can add conditions to determine which people wait before continuing through your campaign or just specify a static time or timeframe to wait. You’ll only need to combine them if you need to set a specific timeframe to wait. For instance, if you want people to belong to a segment before receiving a message in a certain timeframe, you’d start with a Wait Until block, followed by a Time Window: Wait Until: Person joins a segment Time window: Monday between 9AM and 5PM Email In this case, people wait until they’re in a segment, and then wait for 9am on a Monday (assuming it isn’t already that time). If the person stops matching the segment condition while they’re waiting for the time window, they would still get the email on Monday at 9AM. If you want them to skip the message if they stop matching, add the segment condition to the email too.  Don’t use a Time Window if you need to send a message at a specific, relative time. For instance, let’s say you want a campaign to send a reminder email 1 day before an appointment. Your campaign could flow like this: Trigger: When a person books an appointment Wait Until: 1 day before the appointment Email You might think to add a Time Window before the message so they receive it at a reasonable time. However, the Time Window could prevent them from receiving the message exactly 1 day before, especially if they booked their appointment less than a day ago. If receiving the message exactly 1 day before is the most important thing, don’t add a time window. --- ## Randomized delay URL: https://docs.customer.io/journeys/random-delay/ Use the *Randomized Delay* action to control the volume of requests (webhooks or messages) sent to downstream services. This helps you workaround limited developer resources or services that don't let you throttle incoming requests. You can hold people in randomized time delays in campaigns. When journeys reach a Random Delay block, we hold them for a random amount of time between the minimum and maximum time you set using a flat distribution model. This can help you avoid overloading rate-limited API endpoints with webhook requests. Add a condition Like with other workflow blocks, you can add one or more conditions so only certain people wait in a random delay. If they don’t meet the condition, they move forward immediately. You can set a profile attribute, event, message, or segment condition to make sure people move through your campaign at the right pace. Consider whether the condition might be better suited to a forthcoming webhook or message action instead. For instance, if you’re concerned about overloading an API endpoint, you could reduce webhook requests if you only send them when people are a member of a certain segment. You can also add conditions to multiple blocks if that better suits your needs! Edit a random delay in a live campaign You can adjust the minimum or maximum delay while the campaign is live. After the change, journeys that reach the delay will randomly wait based on the new timeframe. Any journeys that were currently waiting and now fall outside the maximum time to wait will move forward. --- ## Send and receive data with webhook actions URL: https://docs.customer.io/journeys/webhooks-action/ The *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 action panel, like whether you want to track conversions. To modify your webhook, click Add Request. You can create a new webhook or reuse a saved webhook from your workspace settings. 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 RegionEU 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 136.111.237.157 34.10.127.179 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: x-www-form-urlencoded—for forms that include form data in the request body text/plain text/xml 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: 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-Timestamp header 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 always v0. 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-Signature value 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 You can choose to set attributes on people or temporarily store data within the workflow based on the response data. This allows you to retrieve data from an endpoint to use in conditions or content later on. If you want to use this data outside of the campaign, set customer attributes. This stores data on people’s profiles. If you don’t want to use this data outside the campaign, set journey attributes. This stores data on the journey, not the customer’s profile. From the Response tab, click Add attribute to get started.  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. Set journey attributes You can set journey attributes as a part of a webhook response in a campaign. They provide a way to use webhook data throughout a person’s journey. This lets you include data essential for someone’s journey that doesn’t need to be stored for use outside of the campaign. To set your attribute based on the webhook payload, use the response key with JSON dot notation. For example, if your webhook returned "weather": "rainy", the value of your journey attribute could be response.weather. Note that you can set this dynamic value with or without liquid brackets. If your response is an array, you’ll reference the index with bracket notation, where [0] is the first element or object in the array. You can also use liquid to conditionally render values. If you set a value that isn’t in the webhook response, each journey will have the same value. Then in subsequent actions, you can set conditions, send people down different paths, and update content based on a journey attribute. Learn how to add and preview journey attributes in messages. After the journey ends, we automatically delete the journey attribute data. Limits on journey attributes Overall limit: 100 journey attributes per journey. You can create or change up to 100 journey attributes during a single journey. Updates beyond this limit fail, and the person moves forward without the update. Attribute name limit: 128 bytes Attribute value limit: 100 KiB Set customer attributes To store data on a person’s profile, set Customer attributes. Select an existing attribute or add a new one under Name. Then set a static value, dynamic value, or 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}}. statement. A dynamic value uses JSON dot notation with or without liquid brackets. You access data in the webhook response using the response object. For instance, after creating a lead in your CRM, you could sync the lead_id returned in the response with your Customer.io lead: Or you could increment the lifetime_value of a person based on their purchases through a webhook response: 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. Max response size The response size is limited to 100 KB. If the response is larger than 100 KB, the webhook will fail. 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}}" ] } --- ## Configure reusable webhooks URL: https://docs.customer.io/journeys/webhook-manager/ In Workspace Settings, create webhook configurations to reuse across campaigns and broadcasts and secure your credentials. Overview In Workspace Settings, you can configure webhooks to reuse them across your campaigns and API-triggered broadcasts. Use them to reduce friction across your workflows: Less repetitive work & fewer errors. Define your webhook URL, headers, and auth details once, and reference them in any campaign. Updates at scale. When an API endpoint or auth header changes, update the configuration in one place and all referencing campaigns pick up the change automatically. Tighter security. Team members can select pre-configured webhooks without ever seeing sensitive data like API keys, auth tokens, or headers. Account admins and Workspace managers have full access to creating and choosing webhooks in your campaigns, while Authors can only select which webhooks to use. Currently, people with custom roles cannot create or configure reusable webhooks. Create webhooks Go to Workspace Settings > Webhook configuration. Click Create webhook. Add a Name for your webhook. (Optional) Add a Description to help you distinguish your webhooks. Specify the right Method and Endpoint URL. Add any other Headers you need. (Optional) Send a Test Webhook to ensure your webhook is functional. Now you can add the webhook to your campaigns and API-triggered broadcasts, which is where you’ll specify the Body or store data on attributes for use later on. For more information on setting up a webhook, check out our webhook action article. Edit webhooks To edit a webhook, click Edit from the Webhook configuration page. Make sure you test the webhook before you save; changes take effect immediately across your campaigns and broadcasts. Delete webhooks To delete a webhook, click Edit and choose Delete from the webhook’s page. You can’t delete a webhook in use; you must remove it from all campaigns and broadcasts first. Use saved webhooks You can use saved webhooks in campaigns or API-triggered broadcasts. To assign a configured webhook to a campaign or broadcast, follow these steps: Drag a Send and receive data action into your workflow. Click the action and then choose Edit Request. Click the dropdown at the top and select a pre-configured webhook. You’ll see the name and description set in the configuration manager. Then you can finish setting up the webhook: Add a request body. Set journey attributes to temporarily store and use data in an active journey. Set customer attributes to use the data outside of the campaign. Preview the webhook with sample customer data. Send a test. --- ## Send in-app messages using webhooks URL: https://docs.customer.io/journeys/in-app-message-webhooks/ You can use webhooks to send in-app messages through other platforms. Webhooks provide a way to access public APIs and services outside of Customer.io. We treat webhooks like messages: when someone reaches a webhook in your campaign, we'll execute the webhook, accessing an external API. In this case, we'll show you how to use webhooks to send in-app messages. An in-app message is a message that targets a person currently using your app or website. In-app messages reach people after they open your app, helping you communicate with engaged members of your audience without bombarding them with notifications outside your app.  We have our own in-app solution! Check out our in-app documentation to learn more about sending in-app messages from Customer.io without a third party service. If you already use another service, you might use webhooks as a transitional solution while you migrate to Customer.io. Use a webhook to launch Appcues Flows Appcues Flows are product tours or another series of in-app messages. If you set up your Appcues Flow to target an audience of Specific Users—a group of users bearing one or more properties—you can use a Customer.io webhook in your campaign to apply properties to people in Appcues, making them eligible to experience your Appcues Flow when they return to your site or app. Before you get started, you’ll need your Appcues API credentials. People in your Customer.io workspace must have an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or IdentifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. (id or email) that maps to a person in your Appcues account so that you can associate people in Customer.io with the right Appcues users. To set up a webhook that triggers an Appcues Flow in your campaign: Add a Send and Receive Data block to your workflow, and click Add Request. Set the URL to https://api.appcues.com/v1/accounts/{account id}/users/{{customer.<attribute>}}/activity where: {account id} is your account ID in Appcues. <attribute> is the attribute that maps to your users’ identifiers in Appcues. Make sure the method is set to POST. Click Add Header and enter Authorization for the name and bearer <your_appcues_api_key> for the value. Click here to find your Appcues API key Enter your payload. The property_name and property_value in the example payload below represent the name of the attribute and the value of the attribute that trigger your flow. { "profile_update": { "property_name": "property_value" } } (Optional) Test your webhook request! Under Results, select the person you want to send a test to, and then click Send test…. If your webhook is configured correctly, your request will expose your Appcues Flow to your test person. Click Save Changes. When you activate your campaign, your webhook will expose tours to people who progress to the webhook in your campaign. Use a webhook to start Chameleon tours Chameleon tours work like in-app messages, but, unlike other in-app messages, you don’t send a tour or experience: you trigger it. Your tour should be set up to trigger based on a the presence of one or more attributes. Our webhook will set an attribute on a person in a campaign, causing the tour to appear for that person. To set an attribute on a person in Chameleon, you need to know their uid or id. You can store this value on an attribute in a person in Customer.io; or, if people in Customer.io have an id value that matches the id or uid values in Chameleon, you can use {{customer.id}} in your payload. Before you begin, you must have: mapped people from Customer.io to users in Chameleon. a tour or experience that is triggered by a custom user property. your Chameleon API token. In the workflow for your campaign: Add a Webhook to your workflow and click Add Request. Set the URL to https://api.trychameleon.com/v3/observe/hooks/profiles. Make sure the method is set to POST. Click Add Header and enter X-Account-Secret for the name and enter your Chameleon API token for the value. Click here to find or generate your Chameleon API token. Enter your payload. In this case, because you’re setting a custom property on a user to trigger a tour or experience, the payload is relatively simple. { "uid": "{{customer.chameleon-uid}}", // chameleon-uid is an example // set the attribute that maps to // your Chameleon UID "experience-triggering-property": "experience-triggering-value" } (Optional) Test your request! Under Results, select the person you want to send a test to, and then click Send test…. If your webhook is configured correctly, your request will expose your tour or experience to your test person. Click Save Changes. When you activate your campaign, your webhook will trigger tours and experiences for people who reach the webhook in your campaign. Send in-app messages with Airship To take advantage of Airship’s in-app messaging solution in Customer.io, you must: integrate the Airship SDK in your app. have your Airship App Key and Master Secret. You can find both in the Airship dashboard. use the named_user feature in Airship. create an attribute in Customer.io called named_user that matches the Airship named user value. When you set up your webhook, you’ll use {{customer.named_user}} to make sure that your in-app message reaches the right person. In the Workflow for your campaign: Add a Webhook to your workflow and click Add Request. Set the URL to https://go.airship.com/api/push. Click Add header. Enter Authorization for the Name and basic <base64-encoded credentials> for the Value. You can go here and enter your credentials in the format appKey:masterSecret to get your Base64-encoded authorization credentials. Click Add Header again and enter Content-Type for the Name and application/vnd.urbanairship+json; version=3; for the Value. Provide the payload for your in-app message. The example below has the basic information required by Airship’s API, but the in-app object can be much more complex. See Airship’s documentation to learn more. In our example, we have a customer attribute mapped directly to a named_user in Airship. { "audience": { // push to the specific user you want to message "named_user": "{{ customer.named_user }}" }, // set the device types your audience might have "device_types": ["ios", "android"], "in-app": { "alert": "Your in-app message goes here", "display_type": "banner" } } (Optional) Test your request! Under Results, select the person you want to send a test to, and then click Send test…. If this person has a named_user attribute and your webhook is configured correctly, your request should send them a message. Click Save Changes. When you activate your campaign, your webhook will send in-app messages through Airship. --- ## Web push with webhooks URL: https://docs.customer.io/journeys/web-push-webhooks/ You can use webhooks to trigger web push messages from other platforms as a part of your campaigns. Webhooks provide a way to access public APIs and services outside of Customer.io. We treat webhooks like messages: when someone reaches a webhook in your campaign, we’ll execute the webhook, sending a request to an external API. In this case, we’ll show you how to use webhooks to send web push messages.  Try our in-app message solution! Check to see if our in-app messages suit your needs before moving forward with web push with webhooks. It may take less time to set-up and allow you to keep more of your messaging within your workspace! A web push notification targets people on your website. If a person is not on your website, they won’t see the notification until they return (and are identified). Because you can’t guarantee when a person will return to your website, you may want to explore “time-to-live” settings for notifications in your outside service, so that your notification expires if a person does not return to your site (and receive the notification) within a reasonable period of time. In general, people need to opt in to receive push notifications, and behaviors for push notifications may vary by browser and the platform you use to send web push notifications. Whether you manage push preferences in an external platform or in Customer.io, you should make sure that you don’t send messages to people who have opted out (or have not opted in). Send web push with Airship To send web push notifications with Airship, you must: integrate the Airship SDK in your website. have your Airship App Key and Master Secret. You can find both in the Airship dashboard. use the named_user feature in Airship. create an attribute in Customer.io called named_user that matches the Airship named user value or have a way to map that value to Airship. When you set up your webhook, you’ll use {{customer.named_user}} (or something similar) to make sure that your web push reaches the right person. In the Workflow for your campaign: Add a Send and receive data block to your workflow, and click Add Request. Set the URL to https://go.airship.com/api/push. Click Add header. Enter Authorization for the Name and basic <base64-encoded credentials> for the Value. You can go here and enter your credentials in the format appKey:masterSecret to get your Base64-encoded authorization credentials. Click Add Header again and enter Content-Type for the Name and application/vnd.urbanairship+json; version=3; for the Value. Provide the payload for your web push message. The example below has the basic information required by Airship’s API, but the notification.web object can be much more complex. See Airship’s documentation to learn more. { "audience": { // push to the specific user you want to message "named_user": {{ customer.named_user }} }, "device_types": ["web"], "notification": { "web": { "alert": "Your web push goes here" } } } (Optional) Test your request! Under Results, select the person you want to send a test to, and then click Send test…. If this person has a named_user attribute and your webhook is configured correctly, your request should send them a message. Click Save Changes. When you activate your campaign, your webhook will send web push notifications through Airship. Send web push using OneSignal Before you can send a notification through OneSignal, you must have a value that maps people across platforms. We suggest that you map people’s external user IDs in OneSignal to people in your Customer.io workspace—either directly to id or another attribute on a person’s profile. For this example, we assume that customer.id matches the external user ID in OneSignal. To send a notification, you need both your OneSignal API key and your App ID. In the Workflow for your campaign: Add a Webhook to your workflow and click Add Request. Set the URL to https://onesignal.com/api/v1/notifications. Click Add header. Enter Authorization for the Name and basic <your API key> for the Value. Enter the payload for your web push message. The example below contains required fields based on Web push to a person with any type of web browser identified by an external user ID, but your payload may vary based how you identify people, your audience’s language, and the type(s) of browser you want to send notifications to. See OneSignal’s documentation for more information. { "app_id": "your-app-id", "include_aliases":{ "external_id": ["{{customer.id}}"] }, // select the platform "target_channel": "push", "isAnyWeb": true, "contents": { // English-based message, though you can test for your audience's language with liquid "en": "Your web push goes here" }, "headings": { // English-based heading, though you can test for your audience's language with liquid "en": "Web push title" } } (Optional) Test your request! Under Results, select the person you want to send a test to, and then click Send test…. If the person in OneSignal is appropriately mapped to Customer.io and your webhook is configured correctly, your request should send them a message. Click Save Changes. When you activate your campaign, your webhook will send web push notifications through OneSignal. --- ## Send direct mail with Lob URL: https://docs.customer.io/journeys/lob-webhook-integration/ Using webhooks, you can trigger personalized direct mail right from your campaigns, so you can reach people physically in addition to other message channels. How it works Lob specializes in intelligent direct mail, so you can send personalized postcards, letters, and more to your customers. Using webhooks, you can send personalization data and trigger messages from your campaigns. This lets you send personalized physical mail to customers in response to the things they do on your website or in your app—like if a customer requests more information, or if you want to follow up with a physical coupon to retain customers. You can even trigger campaigns in Customer.io based on events from Lob, helping you keep the conversation going with your customer through both digital and physical channels. Prerequisites Before you can trigger Lob messages from Customer.io, you’ll need to set up a few things in Lob: Your Lob API keys: you provide these keys when you set up webhooks in your campaigns or broadcasts. You can find your API credentials in Lob. Set up your Lob creative file(s): when you send a webhook to trigger messages in Lob, you’ll specify the creative file, or “template”, that you want to send to your audience, so you’ll want to set up your templates before you begin. You can create templates in the Lob dashboard.  Use a test API key first If you’re just getting started, you may want to use a test API key to make sure that your webhooks in Customer.io trigger the right messages in Lob. When you’re sure everything’s set up correctly, then you can switch to production credentials. Send a Lob message Set up a campaign or broadcast in Customer.io. When you get to the Workflow step, you can add a webhook that triggers a message from Lob. Drag Send and Receive Data into your workflow, then click it and set the Webhook Name. This is the name you’ll see in the workflow and can help you understand what the webhook does. Click Add Request to begin setting up your webhook. Enter the URL for the request next to POST. Your URL contains your API key and the type of mail you want to send—postcard, letter, etc in the format: https://<your_api_key>@api.lob.com/v1/<resource>. You can find the URL format in the Lob documentation. For our example, we’ll use a postcard: https://example-api-key@api.lob.com/v1/postcards Construct your webhook payload, including the person you want to send to and the merge_variables you want to use to personalize the message. You can use 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 set variables from a person’s profile in Customer.io as properties in your payload. For this example, we’re using the Lob /postcards API, but you’ll want to replace the payload structure here with whatever type of mail you send to Lob. { "description": "Customer.IO to Lob Postcard Creation", "to": { "name": "{{ customer.first_name | capitalize }} {{customer.last_name | capitalize }} ", "address_line1": "{{ customer.address_line1 }}", "address_line2": " ", "address_city": "{{ customer.address_city }}", "address_state": "{{ customer.address_state }}", "address_zip": "{{ customer.address_zip }}", "address_country": "US" }, "from": { "name": "Lob Solutions", "address_line1": "210 King St", "address_line2": "3rd Floor", "address_city": "San Francisco", "address_state": "CA", "address_zip": "94107", "address_country": "US" }, "front": "tmpl_xxxxxxxxxxxx", "back": "tmpl_xxxxxxxxxxxx", "size": "6x9", "merge_variables": { "name": "{{ customer.first_name | capitalize }}" }, "metadata": { "source": "Customer.IO" } } Save the webhook. (Recommended) Test your webhook to make sure that you send the right message with the correct template variables. Now when you start your campaign, people will automatically trigger direct mail through Lob when they reach the appropriate step in your workflow. Test your Lob webhook While setting up your webhook, you can test it to make sure that you send the right message and template variables to Lob—and that your audience gets mail with the right information. When setting up your webhook: Select a test user in the left-column. If you’re using a test API key, your test user could be anybody. But, in general, we recommend that you use a dedicated test user—maybe even yourself—to make sure that you don’t accidentally send test messages to your audience. Click Send Test in the upper-right corner to send a test webhook. Customer.io will report the status code and test results to show the payload you sent. You can check out the payload to make sure that everything looks correct. Then you can log into Lob and verify that your test message was received correctly. Now you can swap your test credentials for your real, live credentials and confidently send personalized direct mail from Lob. Trigger campaigns based on events from Lob Integrating Lob and Customer.io helps you synchronize your digital and direct mail channels. For example, you might want an email to land in your customers’ inboxes the same day they receive postcards from Lob. To do this, you’ll set up a webhook-triggered campaign using webhooks from Lob. Go to Campaigns and click Create Campaign. Click Choose trigger and select Webhook. Copy the webhook URL. Go to Webhooks in your Lob dashboard. Paste the URL under Create a New Webhook. Select the event(s) that should trigger your campaign, like the Delivered event. Now you can finish setting up your campaign in Customer.io, and Lob will automatically trigger your campaign when the event(s) you selected occur. --- ## Default sending settings URL: https://docs.customer.io/journeys/sending-behavior/ Sending behavior determines how we handle messages for your campaign or broadcast—whether messages send automatically or not, whether we track links or not, etc. Defaults In campaigns and API-triggered broadcasts, messages are set to queue, not send automatically. All campaigns and API-triggerd broadcasts have the setting “Track opens and link clicks” on. Messages inherit a campaign or broadcast’s subscription preference setting. You can, however, override this setting on each message. If you use our global unsubscribe functionality (this is our out-of-the-box option), we send to all subscribed by default. If you use our subscription center, you must choose to send to people subscribed to a specific topic or all subscribed AND unsubscribed. If you include custom recipients when triggering an API-triggered broadcast via our Track API, we will respect the subscription preference setting of the broadcast. That is, if your custom recipients include people that are unsubscribed from the subscription preference topic, they will NOT receive emails, SMS, or push. In-app messages will send regardless of a person’s subscription status. You can change sending behavior, tracking, and subscription preference settings for each individual message in a campaign or broadcast. Click a message to display its settings. After you make your changes, click Save, and you’re done! Notes If you want the tracking links to use your sending domain instead of ours, you will need to add a CNAME record to your domain configuration as indicated in your Customer.io account under Settings > Email. Click Verify Domain then the Link Tracking tab to find it. --- ## Sending behavior options URL: https://docs.customer.io/journeys/queue-draft/ We offer three sending behavior options for your messages: queue draft, send automatically, and don't send. How it works By default, we set messages to “Queue Draft.” This means we generate drafts, but don’t automatically send them to your audience. This is meant to be temporary; you should update your messages to “Send Automatically” when you’re confident your messages are ready to go. With “Queue Draft” you can see a campaign in action before you actually send messages to your audience. Queueing drafts allows your complex workflow to run - matching recipients, rendering content, and emulating the campaign’s behavior - without any content being sent. You can see exactly how the message you’ve prepared for a user looks (and check many users for specific personalized content). Think of this as a staging feature: everything works as it normally would, but you can audit the output before it gets sent out in the real world. Then when you’re ready, you can send drafts to your audience and update the sending behavior to “Send Automatically.”  We only store drafts for 30 days Make sure you send drafts within 30 days; otherwise, we delete them. Change sending behavior To change the sending behavior for your messages, you have to modify each message. There is no way to change the sending behvaior for all messages at once. Click the message block. Click Settings. Click the status you want from the Sending behavior dropdown. Click Save. Sending behavior in a live campaign Change from Queue Draft to Send Automatically If your campaign is live, “Send Automatically” only applies to future messages. You’ll still need to manually send the existing drafts to ensure all of the people in your campaign receive the messages they should. Change from Send Automatically to Queue Draft If your campaign is live, changing a message from “Send Automatically” to “Queue Draft” will cause future messages to queue as drafts. Moving forward, you’ll need to manually send the drafts to ensure all of the people in your campaign receive the messages they should. Keep in mind, people will continue to move through your campaign, so it’s possible the drafted messages are no longer relevant after a time. In that case, you might delete the drafts instead.  We only store drafts for 30 days Make sure you send drafts within 30 days; otherwise, we delete them. Edit drafts You can edit drafts to further customize them for your recipients. Click Drafts on a campaign or on a person’s profile. Click the Action of one of the messages. Click Edit. Make your changes. Click Send when you’re ready. Edit messages with drafts When you edit a message in your campaign, your drafts automatically update to match your changes! After you save your changes, all drafts will render the new content. When you send them, people receive the up-to-date message from your campaign. Send drafts To send draft messages in a live campaign: Click your campaign. Click Drafts. Click Send All at the top of the table or check the box next to specific messages then click Send Selected. You’ll see this action reflected in a person’s journey.  We only store drafts for 30 days Make sure you send drafts within 30 days; otherwise, we delete them. Send rate for drafts Messages that are queued to draft can take longer to send than messages set to send automatically. Once you’ve verified your campaign configuration and content, change your messages to “Send Automatically.” Delete drafts If you no longer need your drafts, you can permanently delete them. Click your campaign. Click Drafts. Click Delete All at the top of the table or check the box next to specific messages then click Delete Selected. Confirm your action. The person’s journey will show “Drafted email.” The deletion is not reflected in the activity log. Export drafts You can export a list of drafts that have been generated for your campaign, but you can’t export the content of all of your drafts. To export a list of drafts: Click Drafts in your campaign. Click Export to CSV at the top right. You’ll receive an email with the CSV to download. Or you can go to the Exports page (under Configure data, click More) to download the CSV. FAQs If I change the trigger or filter of a live campaign, do you re-evaluate drafts? Changing a trigger or filter does not change the drafts we’ve queued. This means people that no longer match your trigger or filter criteria would still have a draft ready to send. If you need to change the trigger or filter setting of a live campaign, consider why you need to do this. Can you make a new campaign instead? That would help you keep your metrics in check. Otherwise, consider stopping your campaign, then deleting existing drafts before you change the trigger or filter criteria. Then make the changes to your campaign’s settings and restart the campaign. This way, when you restart the campaign, you won’t have to differentiate between who should receive drafts and who should not. Consider adding a note in the description of the campaign indicating when and why you changed the trigger/filter conditions so you can reconcile this with your metrics. To help you differentiate between which people should still receive drafts and which shouldn’t, you can export a CSV from the Drafts tab that includes a list of generated drafts and your customers’ IDs. You could compare the customer IDs against those that are aligned to your new trigger and filter criteria.  We only store drafts for 30 days Make sure you send drafts within 30 days; otherwise, we delete them. Can I make a segment based on drafts? No, drafts are meant for testing, so we haven’t included them in conditions. Troubleshooting I deleted all the existing drafts, but I don’t see any new ones showing up. What went wrong? People may not have reached the message in your workflow yet. After we draft a message, people continue moving forward whether the message is sent or not. If you delete a draft, we will not re-draft the message in the same journey for that person. --- ## Queue Draft for Campaign QA URL: https://docs.customer.io/journeys/queue-as-draft/ Great messaging is no simple task. Deep personalization drives higher opens, clicks, engagement, and results. But it also makes your lifecycle campaigns more complex to design, build, and deploy. The more moving parts you add — automation, segmentation, dynamic content, and filters — the more likely that something can go wrong. This article shows you three ways to use 'Queue Drafts' to review your campaigns. In most ESPs you’re either “building” your email campaign or “sending” it. This means that once you hit “Start campaign”, your messages go live and you can only catch errors afterwards. Customer.io takes a different approach with a feature called Queue Draft, which enables you to start campaigns and generate drafts of messages without sending them. Think of this as a dry-run for your campaigns. You get to see who would have received what message when before actually going live. This feature makes a better quality-assurance process possible. By first setting your messages to queue drafts, you can debug your content and check the logic, all without ever sending a single message. Check liquid and copy Personalizing your messages is a great way to make subscribers feel warm and fuzzy. But what happens when your personalization goes wrong? Imagine that, for example, some users don’t have a specific attribute you want to personalize on, like city_name. When you try to use an attribute that doesn’t exist for a particular user (without adding a fallback), Customer.io won’t send the message and the delivery will fail. While this is 1000x better than sending a broken message, this doesn’t solve the fact that our message wasn’t sent! When you set messages to Queue Draft, we will generate a draft rather than sending that message automatically. Then, you can spot-check the drafts that have been generated. Here’s an example drafted email where we tried using a variable customer.plan_name that is missing for this particular user, and we didn’t catch the error message in the composer. When you catch message content mistakes and fix them in the composer, drafts will automatically update. Check campaign configuration Ever sent the same message to the same person multiple times in one day? That’s every email marketer’s nightmare. When your messages are queueing as drafts, you can see exactly who would receive what message— for example, if a user would have received the same message multiple times in a single day. (This can happen with event-triggered campaigns without proper filters.) If this is the case, you can add a rule to the campaign so this message only goes out if the recipient has NOT already received it in the last week. This also helps you discover any other issues you might have never thought about, like a person receiving two drips at the same time, an exclusion filter that’s critical but missing, or something else in the campaign workflow logic that you didn’t quite get right.  If you need to update a campaign’s configuration, create a new campaign If you find errors in your campaign workflow setup (i.e., triggers, filters, action conditions, delays, etc), it’s best to re-create the campaign and start fresh. Check how people progress through the campaign You can go to a user’s “Journeys” tab to see a message’s status. Here we see that that an email in the “Onboarding second series” campaign will draft an A/B test in two days. When you mouse over the action icon with the yellow drafting status, you can see which specific message is the next step. If you think this message shouldn’t be drafting in 2 days, then you can make the necessary edits to your campaign flow! When you finish reviewing Rather than sending drafts after you finish review, you might stop the campaign, duplicate it, update messages to send automatically, then start the new campaign. This way, everyone starts receiving messages automatically and all of your messages and actions are timed as specified in your workflow. --- ## Geolocation and time zone data URL: https://docs.customer.io/journeys/geolocation/ Customer.io can gather geolocation and time zone data from your audience, helping you coordinate messages at times that are appropriate for them. How it works Customer.io determines a person’s location from two possible sources: IP-based geolocation: We infer location from IP addresses. This happens automatically when you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone through our JavaScript client, mobile SDKs, or any integration that includes a context.ip field. Mobile device-based geolocation: Our mobile SDKs (iOS, Android, Flutter, React Native, and Expo) can capture GPS coordinates directly from a person’s device. This is more accurate than web-based geolocation. We store one canonical location per person. When both sources are available, device-based location takes precedence because it’s more precise. The cio_location_source attribute on a person’s profile tells you which source provided their current location—ip or device. You can use geolocation data to segment your audience by location or time zone. You can also use time zone data to send messages at the right time for your audience with our recommended send time feature or our time zone match features. IP-based geolocation IP-based geolocation is the default source of geolocation data because more have more sources of IP address than we do for mobile device coordinates. When you enable Automatic Geolocation Data Collection and identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone with a context.ip field, we look up their approximate location using MaxMind’s GeoIP database. Our JavaScript client and mobile SDKs capture IP addresses automatically; you need to pass context.ip manually with server-side libraries and the Pipelines API. Device-based geolocation (mobile SDKs) Our mobile SDKs can provide GPS-level accuracy by capturing coordinates directly from a person’s device. This is more accurate than IP-based geolocation, which can only approximate location to a city or region. Device-based location reaches a person’s profile through two pathways: Identify calls: When a mobile SDK sends an identify call with the Location module enabled, it includes GPS coordinates alongside the IP address. We check device coordinates first—if they’re present, we use them instead of the IP address. Location Update events: Mobile SDKs can send a Location Update track event with latitude and longitude. This updates the person’s geolocation attributes on their profile. To set up device-based location tracking, see the location tracking guides for your platform: iOS Android Flutter React Native Expo Location precedence We only store one set of location values per person. When we receive new location data, we compare it to the existing data and apply the following rules: flowchart TD A([Person identified with location]) --> B{What type of\nlocation data\ndid we receive?} B -->|Device coordinates| C{What type of location data are\nwe storing for this person?} B -->|IP address| D{What type of location data are\nwe storing for this person?} C -->|No data| G(Update location) C -->|IP address| E(Device location overwrites IP geolocation) E --> G C -->|Device GPS| F{Has the person moved more than 1 km?} F -->|Yes| G F -.->|No| H(Keep existing location) D -->|No data| G(Update location) D -->|IP address| I(Device location takes precedence over IP geolocation) I --> G D -->|Device GPS| J{Has the person moved more than 1 km?} J -->|Yes| G J -.->|No| H  Device location is sticky When valid device coordinates are present, we skip IP geolocation entirely—even if the device location wasn’t updated because the person hasn’t moved more than 1 km. The device source is preserved. This prevents less-accurate IP data from overwriting precise GPS coordinates. When the source changes (for example, from ip to device), we rewrite all geolocation attributes to clear stale values from the previous source. Enable or disable automatic geolocation data collection Enabling Automatic Geolocation Data Collection lets us gather and set location and time zone data for people you identify based on their IP addresses or device GPS coordinates. This setting is on by default in our US data center. If you’re in our EU data center, you’ll need to enable it manually. Go to Workspace Settings > Time Zone & Geolocation Settings. Enable or disable Automatic Geolocation Data Collection. Note that turning this setting on doesn’t: Enable geolocation for server-side libraries. You’ll need to enable geolocation for server-side libraries manually. Backfill data for existing people. You’ll need to identify people from our JavaScript client or mobile SDKs to get their geolocation data. Enable or disable geolocation for server-side libraries To support geolocation for server-side libraries (like our Node.JS, Go, and Python libraries), you’ll need to enable geolocation services. Then you’ll need to capture your users’ IP addresses and set them in the context.ip field in your identify requests. Go to Integrations. Go to your server-side integration—Node.JS, Go, or Python—and click the Settings tab. Change the Enable Geolocation setting.  Make sure you capture your users’ IP addresses If you don’t set the context.ip in your requests, we won’t be able to capture geolocation data for your users. If our libraries infer the address as your server’s IP address, it’ll look like everyone is in the same location as your server. Capturing IP addresses for geolocation Automatic geolocation is based on the context.ip field in identify requests. We automatically capture IP addresses from our mobile SDKs or JavaScript client. If you use our backend libraries (like our Node.JS or Go SDKs) or our Pipelines API, you’ll have to capture the IP address yourself and pass it in the context.ip field in your identify requests. We’ve added some examples below showing the context.ip field in your identify requests to our backend SDKs. SDK/Integration context.ip field JavaScript client Automatic Mobile: iOS Automatic Mobile: Android Automatic Mobile: React Native Automatic Mobile: Flutter Automatic Mobile: Expo Automatic Node.JS SDK Manual Go SDK Manual Python SDK Manual Pipelines API Manual Node.JS SDK Node.JS SDK cioanalytics.identify({ userId: '019mr8mf4r', traits: { name: 'Cool Person', email: 'cool.person@example.com', plan: 'Enterprise', friends: 42 }, context: { ip: '123.45.67.89' } }); Go SDK Go SDK func main() { client := analytics.New(os.Getenv("WRITE_KEY")) client.Enqueue(analytics.Identify{ UserId: "user123", Traits: analytics.NewTraits(). SetEmail("john@example.com"). SetName("John Doe"), Context: &analytics.Context{ IP: net.ParseIP("192.168.1.100"), }, }) } Python SDK Python SDK cioanalytics.identify('f4ca124298', { 'email': 'cool.person@example.com', 'first_name': 'cool', 'last_name': 'person', 'context': { 'ip': '123.45.67.89' } }) What happens to data when you disable automatic geolocation data collection? When you disable automatic geolocation data collection, we’ll stop gathering location and time zone data for people you identify. You can also choose to delete existing geolocation data for people you’ve already identified. If you disable automatic geolocation data collection, there’s no harm in keeping this data around. But it might clutter profiles if you don’t use this data. We geolocate people when you identify them We don’t add or update geolocation data until you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone with a context.ip field or device GPS coordinates in the same request. Remember, our mobile SDKs and JavaScript client automatically capture IP addresses for you, but you’ll need to capture the IP address yourself if you use our backend libraries (like our Node.JS or Go SDKs) or our Pipelines API directly. Even if you’ve already identified someone, additional identify requests can update a person’s geolocation data. flowchart LR z(Identify person)-->a a{Does the request include location data?} a--->|Yes|b(Update geolocation data) a-.->|No|c(Do not update geolocation data)  Identify people in new sessions to keep geolocation data up to date Your website and mobile app might “remember” users across sessions. But, even if you don’t need to update any information about them, you should identify them again in a new session to keep their geolocation data up to date. This can help you keep up with people as they move around the world. Automatic geolocation attributes When you enable automatic geolocation data collection, we automatically capture the following attributes when you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone. You shouldn’t set these attributes yourself. If you do, it’s likely that we’ll overwrite your data with our own values whenever you identify a person. Attribute Description cio_iso_country The person’s ISO country code in alpha-2 format (like “US”). cio_country_name The person’s country name in ISO 3166-1 format (like “United States”). cio_city The person’s city name. cio_latitude The latitude of the person’s location. For IP-based geolocation, this is the approximate latitude of the city. For device-based geolocation, this is the precise GPS latitude. cio_longitude The longitude of the person’s location. For IP-based geolocation, this is the approximate longitude of the city. For device-based geolocation, this is the precise GPS longitude. cio_timezone The person’s time zone. This is the value we use for recommended send times. cio_iso_continent The person’s ISO continent code in alpha-2 format (like “NA” for North America). cio_continent The person’s continent name (like “North America”). cio_iso_region The person’s ISO region code (like “CA” for California). cio_region The person’s region name (like “California”). This typically represents a state, province, or administrative division. cio_postal_code The person’s postal code or ZIP code. Note that this is an approximation based on the geolocated IP address. It may not be exact. cio_location_source The source of the person’s location data. Values are ip (from IP address lookup) or device (from mobile SDK GPS coordinates).  Additional fields added November 17, 2025 If you identified someone before November 17, 2025, their geolocation attributes won’t include cio_iso_continent, cio_continent, cio_iso_region, cio_region, or cio_postal_code. The next time you identify them with their IP address, we’ll automatically add the additional fields. The timezone attribute You can set the timezone attribute (in a recognized format) to support our recommended send time feature or our time zone match features. This attribute takes precedence over our cio_timezone attribute. You should set it if: You don’t use automatic geolocation data collection and you want to support recommended send times or time zone match features. You have one or more integrations that don’t support automatic geolocation data collection—like our Node.JS or Go SDKs. You want to set a “home” time zone for people who frequently travel across multiple time zones. When you send someone a message using a time zone-related feature, we’ll use the timezone attribute if it exists and conforms to our supported formats. If it doesn’t exist, we’ll use the cio_timezone attribute as a fallback. If the timezone and cio_timezone attributes don’t exist, we’ll use the fallback time zone that you set for a message. This ensures that people who don’t have time zone data still get messages. flowchart LR z(Send message to person)-->a a{Does timezone attribute exist?} a--->|Yes|b(Send message in customer time zone) a-.->|No|c{Does cio_timezone attribute exist?} c-->|Yes|b c-.->|No|d(Send message in fallback time zone) Test your timezone attribute If you set your time zone attribute manually, you can test it to make sure that you’re using a format we support. Go to Workspace Settings > Time Zone & Geolocation Settings. Enter the id or email address of a person in your workspace, and we’ll tell you if the timezone attribute is in the correct format. --- ## Recommended send time URL: https://docs.customer.io/journeys/recommended-send-time/ When you send an email from a campaign or a newsletter, Customer.io can recommend an appropriate send time based on your users' time zones.  Is Customer.io AI enabled? You need to enable Customer.io AI in your Data & Privacy settings to use this feature—if you haven’t already. See our AI overview to learn more about how you can use AI in Customer.io. How it works When you set up a newsletter or an email in a campaign, you can click Recommend send time to schedule the email to send at the best time for your audience. To generate recommended send times, we use AI along with the content of your message and your audience’s time zone. The recommendation also sets a fallback send time, which we’ll use whenever we don’t know someone’s time zone. flowchart LR z(Send message to person)-->a a{Does timezone attribute exist?} a--->|Yes|b(Send message in customer time zone) a-.->|No|c{Does cio_timezone attribute exist?} c-->|Yes|b c-.->|No|d(Send message in fallback time zone)  Recommended send times don’t account for holidays Recommended send times help you send messages at the right times when users encounter a message, but they don’t account for holidays and other dates that might be special to your audience (like birthdays, anniversaries, etc.). Capture your audience’s time zone To take advantage of send time recommendations, you need to capture your audience’s time zone information. You can do this using our automatic geolocation data collection feature or by setting a timezone attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in one of our supported formats. Anybody who doesn’t have a cio_timezone or timezone attribute set will get the message at the fallback time. If you’re not sure if people have a cio_timezone or timezone attribute, you can browse People in your workspace. Using recommended send time in a newsletter In the Review step of a newsletter, click Recommend send time to schedule the newsletter to send at the best time for your audience. If you don’t see this setting, you may need to enable Customer.io AI features in your Data & Privacy settings. When we recommend a send time, you can click Why this send time? to get an explanation of why we recommended that time. You can also provide additional context to help us tune the recommendation to better suit your message and audience. Using recommended send time in a campaign When you set up an email in a campaign, open your message Settings and you’ll see a Recommended Send Time option. If you don’t see this setting, you need to enable Customer.io AI features in your Data & Privacy settings. Click Recommend Send Time to schedule the email to send at the best time for your audience. Select the fallback time zone and then click the recommended option. (Optional) Click Why this send time? to get an explanation of why we recommended that time. You can also provide additional context to help us tune the recommendation to better suit your message and audience. This creates a new Time Window step directly above the message in the workflow. This is how we manage time zone-based sending in a campaign workflow. The Time Window step holds people in the campaign until their recommended send time. When they fall into the recommended time window, they’ll get your message and continue through the campaign. If the person is already in the time window when they reach that step, we’ll send the message immediately and they’ll continue through the campaign. They don’t have to wait for the next time window.  Set your messages to send automatically If your messages are set to Queue Draft, we’ll queue your draft at the recommended send time but we won’t send it. That might defeat the purpose of using recommended send times. Make sure you set your messages to Send Automatically to take advantage of recommended send times. Removing or updating the recommended send time You can edit the time window step directly. If you want to recommend a new time, you’ll need to delete the time window step above your message. Select your message, click Settings and then you can recommend a new send time. When you recommend a new time, we’ll create a new Time Window step above your message. --- ## Send messages in users' time zones URL: https://docs.customer.io/journeys/timezone-match/ Customer.io aims to help you send the right messages at the right time to your users. Two critical parts of this are sending messages in a given person's time zone and localizing time in emails. Here, we'll explain how to do both. How it works You can schedule messages for your users in their time local timezones, so you engage customers at the right times, wherever they are. This feature relies on a timezone attribute you set yourself or the cio_timezone attribute, which is set by our automatic geolocation data collection. If the timezone attribute exists, we’ll use it to send the message in the customer’s time zone. If it doesn’t exist, we’ll use the cio_timezone attribute as a fallback. If neither of these attributes exist, we’ll use a fallback timezone. This ensures that people who don’t have timezone data still get messages. Use our recommended send time feature You can use our recommended send time feature to automatically schedule messages at the best times for your audience. Campaigns: schedule a message with a time window Time zone match is available when using a “Time Window” delay in your workflows: With this setting, you can tell Customer.io to wait until a given time in a customer’s time zone before taking the next action. You must also set a fallback here, telling Customer.io which time zone to use if a customer doesn’t have the timezone attribute. Send newsletters in recipients’ timezones You can schedule delivery of a newsletter in the Review step. Here you have the option to Send in recipient’s timezone. You must also set a Fallback Time Zone, telling Customer.io which time zone to use if a customer doesn’t have the timezone or cio_timezone attributes. If a customer has an invalid value (not empty/missing) for the timezone attribute, they will receive the newsletter during the last date/time we send to across all timezones. Keep in mind, you cannot send in a recipient’s timezone in these situations: If your newsletter contains variants for A/B tests. If this is important for your company, please email win@customer.io with your use case! If your newsletter has a daily rate limit. This is to ensure your customers receive newsletters in a timely fashion. If you’re sending across multiple timezones with a daily rate limit, it’s possible a significant number of your customers won’t receive this newsletter until days after. Use time zone data in messages You can also localize time in your messages using liquid syntax. Imagine that you want to send an appointment reminder, and you have an appointment_time attribute formatted as a UNIX timestamp. The way you use time zone data in your messages depends on the liquid version you’re using. Latest liquid Latest liquid {{ customer.appointment_time | date: "%H:%M %A %b %d, %Y", customer.timezone }} Legacy liquid Legacy liquid {{ customer.appointment_time | timezone: customer.timezone | date: "%H:%M %A %b %d, %Y" }} If customer.timezone is US/Pacific, then the above liquid code will display: 05:00 Friday Oct 28, 2033. --- ## Supported time zone formats URL: https://docs.customer.io/journeys/example-timezones/ These are the values we support when you use the timezone attribute in your emails and attributes. Note that we support automatic geolocation data collection as well. If you use automatic geolocation data collection, we’ll set a cio_timezone attribute that you can use with our time zone-related features, so you don’t need to set the timezone attribute. Region Format - Supported in our emails and attributes as well as the Time Zone Match feature. Detailed Format - Supported in our emails and attributes but NOT in our Time Zone Match feature. Region Format Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Algiers Africa/Asmara Africa/Asmera Africa/Bamako Africa/Bangui Africa/Banjul Africa/Bissau Africa/Blantyre Africa/Brazzaville Africa/Bujumbura Africa/Cairo Africa/Casablanca Africa/Ceuta Africa/Conakry Africa/Dakar Africa/Dar_es_Salaam Africa/Djibouti Africa/Douala Africa/El_Aaiun Africa/Freetown Africa/Gaborone Africa/Harare Africa/Johannesburg Africa/Juba Africa/Kampala Africa/Khartoum Africa/Kigali Africa/Kinshasa Africa/Lagos Africa/Libreville Africa/Lome Africa/Luanda Africa/Lubumbashi Africa/Lusaka Africa/Malabo Africa/Maputo Africa/Maseru Africa/Mbabane Africa/Mogadishu Africa/Monrovia Africa/Nairobi Africa/Ndjamena Africa/Niamey Africa/Nouakchott Africa/Ouagadougou Africa/Porto-Novo Africa/Sao_Tome Africa/Timbuktu Africa/Tripoli Africa/Tunis Africa/Windhoek America/Adak America/Anchorage America/Anguilla America/Antigua America/Araguaina America/Argentina/Buenos_Aires America/Argentina/Catamarca America/Argentina/ComodRivadavia America/Argentina/Cordoba America/Argentina/Jujuy America/Argentina/La_Rioja America/Argentina/Mendoza America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia America/Aruba America/Asuncion America/Atikokan America/Atka America/Bahia America/Bahia_Banderas America/Barbados America/Belem America/Belize America/Blanc-Sablon America/Boa_Vista America/Bogota America/Boise America/Buenos_Aires America/Cambridge_Bay America/Campo_Grande America/Cancun America/Caracas America/Catamarca America/Cayenne America/Cayman America/Chicago America/Chihuahua America/Coral_Harbour America/Cordoba America/Costa_Rica America/Creston America/Cuiaba America/Curacao America/Danmarkshavn America/Dawson America/Dawson_Creek America/Denver America/Detroit America/Dominica America/Edmonton America/Eirunepe America/El_Salvador America/Ensenada America/Fort_Wayne America/Fortaleza America/Glace_Bay America/Godthab America/Goose_Bay America/Grand_Turk America/Grenada America/Guadeloupe America/Guatemala America/Guayaquil America/Guyana America/Halifax America/Havana America/Hermosillo America/Indiana/Indianapolis America/Indiana/Knox America/Indiana/Marengo America/Indiana/Petersburg America/Indiana/Tell_City America/Indiana/Vevay America/Indiana/Vincennes America/Indiana/Winamac America/Indianapolis America/Inuvik America/Iqaluit America/Jamaica America/Jujuy America/Juneau America/Kentucky/Louisville America/Kentucky/Monticello America/Knox_IN America/Kralendijk America/La_Paz America/Lima America/Los_Angeles America/Louisville America/Lower_Princes America/Maceio America/Managua America/Manaus America/Marigot America/Martinique America/Matamoros America/Mazatlan America/Mendoza America/Menominee America/Merida America/Metlakatla America/Mexico_City America/Miquelon America/Moncton America/Monterrey America/Montevideo America/Montreal America/Montserrat America/Nassau America/New_York America/Nipigon America/Nome America/Noronha America/North_Dakota/Beulah America/North_Dakota/Center America/North_Dakota/New_Salem America/Ojinaga America/Panama America/Pangnirtung America/Paramaribo America/Phoenix America/Port-au-Prince America/Port_of_Spain America/Porto_Acre America/Porto_Velho America/Puerto_Rico America/Rainy_River America/Rankin_Inlet America/Recife America/Regina America/Resolute America/Rio_Branco America/Rosario America/Santa_Isabel America/Santarem America/Santiago America/Santo_Domingo America/Sao_Paulo America/Scoresbysund America/Shiprock America/Sitka America/St_Barthelemy America/St_Johns America/St_Kitts America/St_Lucia America/St_Thomas America/St_Vincent America/Swift_Current America/Tegucigalpa America/Thule America/Thunder_Bay America/Tijuana America/Toronto America/Tortola America/Vancouver America/Virgin America/Whitehorse America/Winnipeg America/Yakutat America/Yellowknife Antarctica/Casey Antarctica/Davis Antarctica/DumontDUrville Antarctica/Macquarie Antarctica/Mawson Antarctica/McMurdo Antarctica/Palmer Antarctica/Rothera Antarctica/South_Pole Antarctica/Syowa Antarctica/Vostok Arctic/Longyearbyen Asia/Aden Asia/Almaty Asia/Amman Asia/Anadyr Asia/Aqtau Asia/Aqtobe Asia/Ashgabat Asia/Ashkhabad Asia/Baghdad Asia/Bahrain Asia/Baku Asia/Bangkok Asia/Beirut Asia/Bishkek Asia/Brunei Asia/Calcutta Asia/Choibalsan Asia/Chongqing Asia/Chungking Asia/Colombo Asia/Dacca Asia/Damascus Asia/Dhaka Asia/Dili Asia/Dubai Asia/Dushanbe Asia/Gaza Asia/Harbin Asia/Hebron Asia/Ho_Chi_Minh Asia/Hong_Kong Asia/Hovd Asia/Irkutsk Asia/Istanbul Asia/Jakarta Asia/Jayapura Asia/Jerusalem Asia/Kabul Asia/Kamchatka Asia/Karachi Asia/Kashgar Asia/Kathmandu Asia/Katmandu Asia/Khandyga Asia/Kolkata Asia/Krasnoyarsk Asia/Kuala_Lumpur Asia/Kuching Asia/Kuwait Asia/Macao Asia/Macau Asia/Magadan Asia/Makassar Asia/Manila Asia/Muscat Asia/Nicosia Asia/Novokuznetsk Asia/Novosibirsk Asia/Omsk Asia/Oral Asia/Phnom_Penh Asia/Pontianak Asia/Pyongyang Asia/Qatar Asia/Qyzylorda Asia/Rangoon Asia/Riyadh Asia/Riyadh87 - not supported in latest Liquid version Asia/Riyadh88 - not supported in latest Liquid version Asia/Riyadh89 - not supported in latest Liquid version Asia/Saigon Asia/Sakhalin Asia/Samarkand Asia/Seoul Asia/Shanghai Asia/Singapore Asia/Taipei Asia/Tashkent Asia/Tbilisi Asia/Tehran Asia/Tel_Aviv Asia/Thimbu Asia/Thimphu Asia/Tokyo Asia/Ujung_Pandang Asia/Ulaanbaatar Asia/Ulan_Bator Asia/Urumqi Asia/Ust-Nera Asia/Vientiane Asia/Vladivostok Asia/Yakutsk Asia/Yekaterinburg Asia/Yerevan Atlantic/Azores Atlantic/Bermuda Atlantic/Canary Atlantic/Cape_Verde Atlantic/Faeroe Atlantic/Faroe Atlantic/Jan_Mayen Atlantic/Madeira Atlantic/Reykjavik Atlantic/South_Georgia Atlantic/St_Helena Atlantic/Stanley Australia/ACT Australia/Adelaide Australia/Brisbane Australia/Broken_Hill Australia/Canberra Australia/Currie Australia/Darwin Australia/Eucla Australia/Hobart Australia/LHI Australia/Lindeman Australia/Lord_Howe Australia/Melbourne Australia/NSW Australia/North Australia/Perth Australia/Queensland Australia/South Australia/Sydney Australia/Tasmania Australia/Victoria Australia/West Australia/Yancowinna Brazil/Acre Brazil/DeNoronha Brazil/East Brazil/West CET CST6CDT Canada/Atlantic Canada/Central Canada/East-Saskatchewan Canada/Eastern Canada/Mountain Canada/Newfoundland Canada/Pacific Canada/Saskatchewan Canada/Yukon Chile/Continental Chile/EasterIsland Cuba EET EST EST5EDT Egypt Eire Etc/GMT Etc/GMT+0 Etc/GMT+1 Etc/GMT+10 Etc/GMT+11 Etc/GMT+12 Etc/GMT+2 Etc/GMT+3 Etc/GMT+4 Etc/GMT+5 Etc/GMT+6 Etc/GMT+7 Etc/GMT+8 Etc/GMT+9 Etc/GMT-0 Etc/GMT-1 Etc/GMT-10 Etc/GMT-11 Etc/GMT-12 Etc/GMT-13 Etc/GMT-14 Etc/GMT-2 Etc/GMT-3 Etc/GMT-4 Etc/GMT-5 Etc/GMT-6 Etc/GMT-7 Etc/GMT-8 Etc/GMT-9 Etc/GMT0 Etc/Greenwich Etc/UCT Etc/UTC Etc/Universal Etc/Zulu Europe/Amsterdam Europe/Andorra Europe/Athens Europe/Belfast Europe/Belgrade Europe/Berlin Europe/Bratislava Europe/Brussels Europe/Bucharest Europe/Budapest Europe/Busingen Europe/Chisinau Europe/Copenhagen Europe/Dublin Europe/Gibraltar Europe/Guernsey Europe/Helsinki Europe/Isle_of_Man Europe/Istanbul Europe/Jersey Europe/Kaliningrad Europe/Kiev Europe/Lisbon Europe/Ljubljana Europe/London Europe/Luxembourg Europe/Madrid Europe/Malta Europe/Mariehamn Europe/Minsk Europe/Monaco Europe/Moscow Europe/Nicosia Europe/Oslo Europe/Paris Europe/Podgorica Europe/Prague Europe/Riga Europe/Rome Europe/Samara Europe/San_Marino Europe/Sarajevo Europe/Simferopol Europe/Skopje Europe/Sofia Europe/Stockholm Europe/Tallinn Europe/Tirane Europe/Tiraspol Europe/Uzhgorod Europe/Vaduz Europe/Vatican Europe/Vienna Europe/Vilnius Europe/Volgograd Europe/Warsaw Europe/Zagreb Europe/Zaporozhye Europe/Zurich GB GB-Eire GMT GMT+0 GMT-0 GMT0 Greenwich HST Hongkong Iceland Indian/Antananarivo Indian/Chagos Indian/Christmas Indian/Cocos Indian/Comoro Indian/Kerguelen Indian/Mahe Indian/Maldives Indian/Mauritius Indian/Mayotte Indian/Reunion Iran Israel Jamaica Japan Kwajalein Libya MET MST MST7MDT Mexico/BajaNorte Mexico/BajaSur Mexico/General Mideast/Riyadh87 - not supported in latest Liquid version Mideast/Riyadh88 - not supported in latest Liquid version Mideast/Riyadh89 - not supported in latest Liquid version NZ NZ-CHAT Navajo PRC PST8PDT Pacific/Apia Pacific/Auckland Pacific/Chatham Pacific/Chuuk Pacific/Easter Pacific/Efate Pacific/Enderbury Pacific/Fakaofo Pacific/Fiji Pacific/Funafuti Pacific/Galapagos Pacific/Gambier Pacific/Guadalcanal Pacific/Guam Pacific/Honolulu Pacific/Johnston Pacific/Kiritimati Pacific/Kosrae Pacific/Kwajalein Pacific/Majuro Pacific/Marquesas Pacific/Midway Pacific/Nauru Pacific/Niue Pacific/Norfolk Pacific/Noumea Pacific/Pago_Pago Pacific/Palau Pacific/Pitcairn Pacific/Pohnpei Pacific/Ponape Pacific/Port_Moresby Pacific/Rarotonga Pacific/Saipan Pacific/Samoa Pacific/Tahiti Pacific/Tarawa Pacific/Tongatapu Pacific/Truk Pacific/Wake Pacific/Wallis Pacific/Yap Poland Portugal ROC ROK Singapore Turkey UCT US/Alaska US/Aleutian US/Arizona US/Central US/East-Indiana US/Eastern US/Hawaii US/Indiana-Starke US/Michigan US/Mountain US/Pacific US/Pacific-New US/Samoa UTC Universal W-SU WET Zulu A note on Etc/GMT time zones We support Etc/GMT time zones, but note that the standard is not intuitive. Etc/GMT+2, for instance, means two hours BEHIND GMT, though you might expect it to be ahead because of the “+”. “+” translates to behind GMT and “-” translates to ahead of GMT: Etc/GMT+2 - two hours behind GMT Etc/GMT-2 - two hours ahead of GMT Detailed Format Remember this format is supported in our emails and attributes. It IS NOT supported for the Time Zone Match feature. For that, use the Region format above instead. (GMT-11:00) American Samoa (GMT-11:00) International Date Line West (GMT-11:00) Midway Island (GMT-10:00) Hawaii (GMT-09:00) Alaska (GMT-08:00) Pacific Time (US & Canada) (GMT-08:00) Tijuana (GMT-07:00) Arizona (GMT-07:00) Chihuahua (GMT-07:00) Mazatlan (GMT-07:00) Mountain Time (US & Canada) (GMT-06:00) Central America (GMT-06:00) Central Time (US & Canada) (GMT-06:00) Guadalajara (GMT-06:00) Mexico City (GMT-06:00) Monterrey (GMT-06:00) Saskatchewan (GMT-05:00) Bogota (GMT-05:00) Eastern Time (US & Canada) (GMT-05:00) Indiana (East) (GMT-05:00) Lima (GMT-05:00) Quito (GMT-04:30) Caracas (GMT-04:00) Atlantic Time (Canada) (GMT-04:00) Georgetown (GMT-04:00) La Paz (GMT-04:00) Santiago (GMT-03:30) Newfoundland (GMT-03:00) Brasilia (GMT-03:00) Buenos Aires (GMT-03:00) Greenland (GMT-02:00) Mid-Atlantic (GMT-01:00) Azores (GMT-01:00) Cape Verde Is. (GMT+00:00) Casablanca (GMT+00:00) Dublin (GMT+00:00) Edinburgh (GMT+00:00) Lisbon (GMT+00:00) London (GMT+00:00) Monrovia (GMT+00:00) UTC (GMT+01:00) Amsterdam (GMT+01:00) Belgrade (GMT+01:00) Berlin (GMT+01:00) Bern (GMT+01:00) Bratislava (GMT+01:00) Brussels (GMT+01:00) Budapest (GMT+01:00) Copenhagen (GMT+01:00) Ljubljana (GMT+01:00) Madrid (GMT+01:00) Paris (GMT+01:00) Prague (GMT+01:00) Rome (GMT+01:00) Sarajevo (GMT+01:00) Skopje (GMT+01:00) Stockholm (GMT+01:00) Vienna (GMT+01:00) Warsaw (GMT+01:00) West Central Africa (GMT+01:00) Zagreb (GMT+02:00) Athens (GMT+02:00) Bucharest (GMT+02:00) Cairo (GMT+02:00) Harare (GMT+02:00) Helsinki (GMT+02:00) Istanbul (GMT+02:00) Jerusalem (GMT+02:00) Kyiv (GMT+02:00) Pretoria (GMT+02:00) Riga (GMT+02:00) Sofia (GMT+02:00) Tallinn (GMT+02:00) Vilnius (GMT+03:00) Baghdad (GMT+03:00) Kuwait (GMT+03:00) Minsk (GMT+03:00) Nairobi (GMT+03:00) Riyadh (GMT+03:30) Tehran (GMT+04:00) Abu Dhabi (GMT+04:00) Baku (GMT+04:00) Moscow (GMT+04:00) Muscat (GMT+04:00) St. Petersburg (GMT+04:00) Tbilisi (GMT+04:00) Volgograd (GMT+04:00) Yerevan (GMT+04:30) Kabul (GMT+05:00) Islamabad (GMT+05:00) Karachi (GMT+05:00) Tashkent (GMT+05:30) Chennai (GMT+05:30) Kolkata (GMT+05:30) Mumbai (GMT+05:30) New Delhi (GMT+05:30) Sri Jayawardenepura (GMT+05:45) Kathmandu (GMT+06:00) Almaty (GMT+06:00) Astana (GMT+06:00) Dhaka (GMT+06:00) Ekaterinburg (GMT+06:30) Rangoon (GMT+07:00) Bangkok (GMT+07:00) Hanoi (GMT+07:00) Jakarta (GMT+07:00) Novosibirsk (GMT+08:00) Beijing (GMT+08:00) Chongqing (GMT+08:00) Hong Kong (GMT+08:00) Krasnoyarsk (GMT+08:00) Kuala Lumpur (GMT+08:00) Perth (GMT+08:00) Singapore (GMT+08:00) Taipei (GMT+08:00) Ulaan Bataar (GMT+08:00) Urumqi (GMT+09:00) Irkutsk (GMT+09:00) Osaka (GMT+09:00) Sapporo (GMT+09:00) Seoul (GMT+09:00) Tokyo (GMT+09:30) Adelaide (GMT+09:30) Darwin (GMT+10:00) Brisbane (GMT+10:00) Canberra (GMT+10:00) Guam (GMT+10:00) Hobart (GMT+10:00) Melbourne (GMT+10:00) Port Moresby (GMT+10:00) Sydney (GMT+10:00) Yakutsk (GMT+11:00) New Caledonia (GMT+11:00) Vladivostok (GMT+12:00) Auckland (GMT+12:00) Fiji (GMT+12:00) Kamchatka (GMT+12:00) Magadan (GMT+12:00) Marshall Is. (GMT+12:00) Solomon Is. (GMT+12:00) Wellington (GMT+13:00) Nuku’alofa (GMT+13:00) Samoa (GMT+13:00) Tokelau Is. --- ## Message Limits URL: https://docs.customer.io/journeys/message-limits/ You can set a message limit to determine the maximum number of messages that you can send people within a time frame. Setting a message limit can prevent you from over-messaging people in your workspace. How it works You can set a message limit at the workspace level, and then you can determine which campaigns, broadcasts, and messages count towards the limit. For example, you might have marketing campaigns that you want to count towards limits but a broadcast about changes to your terms and conditions that you need to send to everyone, whether or not they’ve reached their message limit. When a message encounters a limit, it’s marked as Undeliverable. You can retry undeliverable messages on the Deliveries & Drafts page and click Retry to re-send a message. When you do this, you can opt to ignore your message limit, ensuring that a person gets your message. For campaigns, you can also enable automatic retries, so you don’t need to manually re-send messages to people who’ve reached their limit. You cannot set automatic retries for newsletters or broadcasts. flowchart LR b{Has person reached the message limit?} b------>|no|c[Message is sent] b-.->|yes|d{"Is message set to auto retry? (Campaigns only)"} d-->|yes, retry|e[Message is attempted]-->h{Is retry successful?} d-.->|no|f[Message is undeliverable] h-->|yes|c h-.->|no|g{is retry window over?} g-.->|yes|f g-.->|no|e  Message limits don’t apply to transactional messages or in-app messages By their nature, your audience explicitly requests and expects transactional messages like receipts, password reset requests, etc. Message limits don’t apply to in-app messages, because their delivery time is unpredictable; after you send a message, a person has to open your app to the correct page or screen to see the message. Learn more Message limits and timing Message limits are based on timestamps between message deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. calculated down to the second; they are not based on calendar days. For example, if you set a message limit of 1 per day, and you successfully send a message, any message sent within 24 hours of the first message will hit your message limit and be marked undeliverable. Because we calculate message limits down to the second, small time discrepancies can cause messages to hit your limit. For example, if you have a message limit of 1 message per day, and you run a daily recurring campaign with a single message, messages from your daily campaign might hit the limit because we don’t process messages at exactly the same second every day. The exact time at which we process deliveries can change based on fluctuations in the number of people in your campaign, the number of active people in your workspace, current system load, etc. To prevent situations in which messages are made undeliverable within minutes or seconds of your message limit, we generally recommend that you set the time period for your limit slightly lower than your actual need. So, if you were to repeat a campaign daily, and you want to prevent your audience from receiving more than 1 message per day, you should consider setting your limit to 23 hours to prevent yourself from inadvertently limiting your messaging capacity. Message Limit Metrics The Delivery section of your dashboard shows the percentage of your messages that were undeliverable, helping you understand how often your audience reaches the message limit, and whether you should adjust the limit or your campaigns. Limits don’t apply to transactional or in-app messages By their nature, your audience explicitly requests and expects transactional messages, so we don’t apply message limits to them. In-app messages are a little more complicated. Message limits apply at send time. But when you send an in-app message, a person doesn’t receive it right away. A person won’t receive an in-app message until they open your app and go to the correct page(s) or screen(s) where they can see your message. This makes delivery time unpredictable; it could be days or weeks until a person actually receives a message. Because we can’t predict when a person will receive a message, we don’t apply message limits to in-app messages. In general, we recommend that you limit in-app messages using page rules and other targeting criteria to make sure that your in-app messages are relevant and useful to your audience when they see them. Set a workspace message limit Go to Settings > Workspace Settings and click Message Limit. Click Enable a message limit. Enter your limit and time frame. The maximum time frame is 7 days. (Optional, for campaigns only) Set a retry interval if you want to automatically retry messages for up to 48 hours after a person reaches the message limit. Automatic retries do not apply to newsletters or broadcasts.  You can still set auto-retry for individual campaigns The Auto-Retry setting is meant as a global default value for campaigns. But you can set retry intervals on a campaign-by-campaign basis, even without setting a global auto-retry value. Click Save Changes. By default, campaigns, broadcasts, and messages do not observe your workspace message limit settings. You must enable the Count toward message limit setting on the campaigns, broadcasts, and messages that you want to count towards the limit. Automatic retries (campaigns only) The Automatic Retry setting is a default retry window (up to 48 hours) whenever someone reaches the message limit. While you’ll set an Auto Retry setting at the workspace level, this is just a global default. You must enable automatic retries on campaigns for them to observe your auto-retry settings. While the retry window can be up to 48 hours, we’ll retry a message at the earliest possible time it could send. You can also override this setting at the campaign level and set a retry window for individual campaigns.  Automatic retries ignore exit conditions We’ll retry messages even if a person has met conditions to exit your campaign. Take your exit conditions into account before you enable automatic retries. Changing or disabling limits Changes to your message limit and retry settings take effect within moments—on the order of a few minutes. But within that short period, people can still observe your previous message limit settings! Disabling message limits all together prevents your workspace from limiting messages to people. Disabling the Auto-Retry setting: Prevents you from applying a global auto-retry value in the future. Turns off automatic retries for campaigns that previously used the global auto-retry value. Does not turn off automatic retries for campaigns that you’ve set to use a custom auto-retry value. Enable the message limit for a campaign While you set a message limit at the workspace level, you must enable the limit for broadcasts, campaigns, and messages that you want to observe the limit. To count a broadcast or campaign towards the limit: Open your campaign settings. Click Messages and enable Count toward message limit (Optional) Change the Auto-Retry Settings for messages. By default, retries are disabled—even when you enable message limits. Enable the message limit for a message Messages within a campaign or broadcast use the message limit setting from the campaign or broadcast by default. You can change the setting for individual messages within a campaign—to observe or ignore your message limit. Select a message and change the Message Limit to observe or ignore your message limit and retry settings. Manually retry messages after going over the limit We mark messages that go over the message limit as Undeliverable. From the Deliveries log, you can see which of your messages hit the message limit and retry undeliverable messages. When you retry a message, you can determine whether to observe or ignore your message limit. Go to Deliveries & Drafts. In the Deliveries tab, filter for messages with the Undeliverable status. Select a message and then click Retry. Select whether or not to observe the message limit and track links. If you use the Do not send the message if it reaches the limit option, your retry counts toward the limit. Click Resend message. Find campaigns or broadcasts with unsent messages You can find unsent messages in your campaigns or broadcasts. This can help you sort and decide which messages you want to resend despite your message limit. Go to the Campaigns or Broadcasts pages. Items showing a icon have messages that were undeliverable due to your message limit. Hover over the icon and click View undeliverable to see the undelivered messages within the campaign or broadcast. From here you can resend messages to your audience. Find campaigns and broadcasts with a message limit All of your campaigns and broadcasts that observe the limit appear in your Message Limit settings, helping you understand the scope of your message limit. --- ## Email: Getting Started URL: https://docs.customer.io/journeys/email-getting-started/ There are two options for sending mail through Customer.io: you can deliver email through us, or use your existing account with a delivery provider. How it works When you get started with Customer.io or create a new workspace, you can choose how you want to send emails: Through Customer.io as your managed provider Through your own provider/SMTP server In most cases, the choice is simple: if you already have your own provider, you can use them. If you don’t, it’s probably easiest to use Customer.io. In either case, you’ll need to verify your domain to send email. This process can take up to 24 hours, so we recommend you do it as soon as possible so you can start sending messages. Use Customer.io to send email This is the default option when you get started with Customer.io and a new workspace. We use a pool of shared IP addresses to deliver email. We ensure that all email sent from us is authenticated and meets industry standards. We have a no-tolerance policy towards spammers and diligently monitor our network for denylistings, problematic senders, and other factors that could negatively impact your deliverability. To set up Email with Customer.io, you’ll need to: Provide your domain. Enter your From address(es). Verify and authenticate your domain(s). This involves adding some records to your domain host. (Recommended) Set up link tracking so you can record the links that people click in your messages. This involves adding a CNAME record to your domain host(s). When you’re done, you’ll want to start sending slowly to build your inbox reputation—even if you’re migrating from another provider. See our guide to sending slowly for more information. Use your own email account We’re perfectly happy for you to use your own account with Customer.io. We fully support the following platforms, meaning that we receive and report the statuses of your messages after they leave our servers—like when messages are delivered, opened, and clicked. Click a platform below to learn how to use it with Customer.io: Mailgun Mailjet Mandrill Sendgrid Sparkpost Oracle Dyn You can also use any other mail provider allowing SMTP like Amazon SES, etc. If you want us to support your provider, please get in touch and let us know. --- ## Transitioning to Customer.io as a sender URL: https://docs.customer.io/journeys/deliverability-getting-started/ There will always be some measure of deliverability performance variability when you start sending from a new email provider. This article should help you navigate this critical transition and avoid problems. What is a sender identity? Your sending identity, sometimes called a sender “fingerprint,” is comprised of: Sending domain Sending IP Other non-public fingerprints Every inbox provider tracks your sender identity differently. The goal of establishing sender fingerprints is to properly identify senders and calibrate their reputation and anti-spam systems based on historical behavior and performance. When you move to a new sending platform, some aspects of your sender identity will change. While some changes are more impactful than others, there is always going to be a measure of performance variability with your deliverability when any part of your sender identity changes. What changes when you send from Customer.io? When you start sending emails from Customer.io, two things happen: You’ll send from new IP addresses The actual domain of your From addresses changes. There are two types From addresses at play: Display-From: This is the domain that your audience sees in the From address field in their inbox clients—for example, hello@customer.io. In this case, customer.io is the visible domain. Envelope-From: This is the domain that is actually signed with SPF / DKIM and communicates with your recipient’s server. When sending from Customer.io or other similar platforms that send email on behalf of your domain, this domain can sometimes utilize a unique subdomain that differs from the root domain. While many email-sending platforms utilize an entirely different subdomain, we simply add a unique subdomain to your existing root domain for better deliverability and Return-Path parity. When you send from Customer.io, your envelope’s from domain will always be in the format: cio#####.mydomain.com where ##### is your account number. While your recipients will always see your domain or subdomain in the From address, a unique subdomain is created for use as your Envelope-From address. Easing the transition to Customer.io In your first weeks of sending email from Customer.io, the following critical steps will minimize the impacts of sending from a new provider. Start slowly Above all else, you should start slowly when you send messages from a new provider. While there aren’t any hard-and-fast rules, your best bet is to start with a small list—no greater than 1000 recipients—and slowly increase your sending volume over a couple of weeks. This allows inbox providers to recognize your new sending source and properly calibrate their inbound systems accordingly. If possible, you should only send to recipients who have recently engaged with your emails during this critical time, which leads to our next piece of advice…. Import engagement data Keeping track of your most engaged recipients is critical to deliverability success. The best way to do this is to identify recipients who have engaged with your messages in the previous 120 days in your old platform, and keeping track of these recipients in Customer.io by adding an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to engaged people. For example, you could identify your most recently-engaged recipients and then import these recipients to your Customer.io workspace with an attribute of recently_engaged: true. You can then create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. for people with this attribute and use it to ensure that only engaged people receive your messages. Import suppression lists from your previous provider To ensure that you don’t inadvertently send email to recipients that experienced hard bounces or were suppressed for other suppression reasons, we recommend either importing your suppression lists (by reaching out to our support team), or, simply not importing any recipients on these lists to Customer.io. --- ## Introduction to Creating Emails URL: https://docs.customer.io/journeys/2-email-basics/ This article helps you understand how to create emails in Customer.io at a high level.  Check out Design Studio! Design Studio is our newest, most flexible email editor. Use components to create a block-based email from scratch, and set global styles to create a consistent brand across your messages made in Design Studio. No longer do you have to decide between a visual or code-based editor; you can use both! Writing your message You can use one of four editors to create your email copy: Design Studio, drag-and-drop, rich text, or code. To create reusable content and structure: With Design Studio, use components. With the drag-and-drop editor, use snippets and saved rows. For the rich text and code editors, use snippets and layouts. Dynamic content, Level I Using a templating language called Liquid, you can merge in the attributes you’re sending us, like customer.first_name or customer.company_name, using the syntax {{customer.first_name}}. So if the first_name attribute for a customer is Alex, for example, inputting {{customer.first_name}} into the composer will render Alex in the sent message! On the other hand, if you’re not sending us that attribute, we’ll show an error. Something like this: Dynamic content, Level II Liquid supports minimal logic like if/else/unless, and allows you to loop through an array. For example, if you only wanted to show trial expiry information, but only if the user is on a trial plan, you could do something like this: {% if customer.is_trial_user %} <p>Your trial expires on {{customer.trial_expiry_date}}</p> {% endif %} There’s a lot you can do with Liquid, and we have separate documentation for that, once you’re ready to dive in. Testing your emails It’s good practice to send some test emails to make sure that Liquid code is rendering correctly, among other things. There’s a ‘Send Test’ box in the top-right hand corner of the composer that you can use for this: So pick a customer from the sample data on the left (here we chose “ami@customer.io”), and then send your test! One thing to note: dynamically generated links like a view in browser link and an unsubscribe link are ignored during tests. If you click a test email’s unsubscribe link, you’ll see something like this: Laying out your emails Want control over how your email looks? Use Layouts. A Customer.io layout is the code that wraps around your message text and gives it its appearance and structure. See this example: {{content}} is where your email’s text is inserted when you write it. Everything else is the Layout. When you’re ready to tackle these, head over to our Layouts documentation.  Layouts are only available in our rich text or code editors. You cannot use layouts with Design Studio or our drag and drop editor; instead, start from a template. Or learn more about the reusable content features available in Design Studio (components) and the drag and drop editor (saved rows, snippets). Ready to send? Here’s how it happens! When a user meets your campaign trigger and filter conditions and it’s time to send them an email, a lot happens behind the scenes to get data into that email, the email out of Customer.io, and to your user! After checking that you’re verified, we: Fetch everything that email needs: things like customer and event data, message content and layout. Build and render that email. This has a few steps, and their order is very important: Create the email body. This means that we take all the customer and event data, HTML code, and everything that’s in your message body in the composer, and put it all together. Take that body, and insert it into the Layout where the {{ content }} tag is. Add everything else – things like Subject and sender IDs, and render the full email! Perform some more checks, append URL parameters, and add any attachments. And then the email is sent. Have questions? Need help with the Customer.io basics? No problem, we’re here for you! Just get in touch! --- ## Test your emails URL: https://docs.customer.io/journeys/testing-emails/ After you create your first email, send yourself a test before activating your campaign to make sure your emails render as you'd expect in your inbox.  Are you using Design Studio to create your email? You can send tests from both the email editor and the connected automation.  Are you on a trial? If you’re on a trial, you’re allowed a limited number of test emails per day. From any workflow (campaign, broadcast, or transactional message), you can send test messages to make sure your emails look right in your inbox. Select the email block, and then click Edit Content. Choose a person from the Sample Data panel. Your test send will replace customer liquid statements with this person’s data. Click Send test… above your email. Enter up to 25 email addresses. Separate multiple email addresses with commas. Choose whether to include “[TEST]” in the subject line. If your email is translated, you can choose to send tests for all languages; otherwise, test addresses will only get the default message. Click Send test. For event-triggered campaigns, you can only test with users who already performed the event. If they haven’t, you’ll see an error that event or event.<DATA_NAME> is missing in the “Preview” tab.  Test emails only send to the addresses you specify Don’t worry, the email will only send to the address you’ve specified; the customer’s attributes you’re merging in are just for testing purposes. Any email addresses set in the BCC or Fake BCC fields will not receive a test message. 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. --- ## Email Attachments URL: https://docs.customer.io/journeys/email-attachments/ Customer.io doesn't support attachments of any type in [Campaigns or Broadcasts](/journeys/types-of-campaigns-and-broadcasts/). But, there are other, better ways to send files to recipients!  Try sending a transactional message Rather than trying to send links, or add attachments to events, you can send attachments as a part of our transactional message service. Why you shouldn’t send attachments Sending attachments can create problems for your deliverability. Here’s a non-exhaustive list of potential problems stemming from attachments: Email attachments are often used to hide viruses and malware, so they are more likely to end up in spam folders or flagged as potentially malicious. Some email servers have strict policies around message size limits and/or attachments which can cause messages to bounce. Attachments impose a larger download for recipients, which can impact their data consumption and/or lead them to ignore your email. All of these can damage your deliverability. Send a link rather than an attachment  Include PDF attachments from the asset library While you can encode attachment and send them directly to people, you can also host PDFs in our asset library and link to them in your messages. This can save you the trouble of encoding and sending files directly. If you want to send a file to your recipients, the best approach is to send them a link to a landing page where they can download the file. You should host the file online—either on your own server or a cloud storage service like AWS S3, Google Drive, Dropbox, Box, etc. Create a page that links to the file. Your hosting service might already create one for you. Otherwise, you’ll need to add a page to your website. Copy that page’s URL and use it in your email.  Avoid sending a link to a file directly You can send a link to the file itself, but be aware that some spam filters and algorithms have been known to follow links to evaluate them, and if the link leads to a file, it could be considered suspicious. Microsoft Office files are particularly risky. While more convoluted than using a direct link, this approach helps ensure that your emails are not flagged as spam. Send attachments with transactional messages In cases where an attachment is unavoidable, you can send them using our Transactional API through an attachments parameter. With this parameter, you can specify a dictionary of attachments where the filename is the key and the value is the base64-encoded content. The total size of all attachments must be less than 2 MB. Do note that some filetype extensions are restricted. --- ## Email Deliverability Best Practices URL: https://docs.customer.io/journeys/email-deliverability-best-practices/ Email deliverability is whether or not the emails you are sending are reaching the inbox of your recipients. There are a number of factors that can directly influence whether or not your emails arrive in the inbox. All of these best practice guidelines should be addressed to ensure maximum deliverability. What is email deliverability? Email deliverability is whether or not the emails you are sending are reaching the inbox of your recipients. There are a number of factors that can directly influence whether or not your emails arrive in the inbox. All of these best practice guidelines should be addressed to ensure maximum deliverability. Deliverability factors Technical Setup Configuring your MX, DKIM and SPF records is the most important step in letting inbox providers know that Customer.io is authorized to send email for your domain. These records are configured in your account under Workspace Settings > Email > Sending Domains. Check out our documentation on authenticating domains in Customer.io to learn more! Spam and Complaint Rates It’s always a good idea to keep an eye on complaint rates. Any complaint rate in excess of 0.10% may result in damage to your sender reputation at both the IP and Domain levels. Increased spam rates over a short period of time can also lead to increased filtration, reduced delivery speed, throttling, or bounces. How to decrease spam rates Unlike technical or engagement factors, reducing spam rates has a simple solution: Send email that people both desire and expect to receive that is relevant to their relationship with your brand. To decrease spam rates, we recommend taking all of the following actions: Think about how your sending relationship with a recipient was started. Were messaging expectations set at any point? Have you clearly outlined to a person who uses your services that you will send email? When a person doesn’t expect an email, they are much less likely to open it, and by extension, more likely to flag it as spam. When you first start a new campaign or chain of marketing newsletters for a new sign up, it’s best to remind the recipient why they are receiving a given email. Ensure that the branding of your emails is consistent. If a recipient signs up for a product or service on your website, make sure that your email domains and email branding align with what the recipient has seen so far and on your sign up page. If you send an email from a brand or domain the recipient doesn’t recognize, it can increase the likelihood that the recipient will mark it as spam. Denylists Denylistings can have an impact on your inbox placement for the duration of the denylisting. We monitor all of the shared and dedicated IPs that we control for denylisting activity and work to mitigate these listings whenever we can. Bounces Bounces can occur for a variety of reasons, some technical, some reputation-based. With that in mind, a large number of bounces caused by sending to non-existent or inactive email accounts can definitely reduce your sender reputation with inbox providers and result in increased filtering to the spam folder. We recommend keeping your overall bounce percentage under 5%. Keeping this value lower than 2% is ideal. Follow best practices under List Health to reduce bounces.  Do not migrate hard bounced recipients If you are migrating to Customer.io from another platform, be sure to omit any recipients who have previously hard bounced (email was permanently rejected) to avoid a high number of bounces on Customer.io. Volume Sending at a consistent rate over time can reduce the impact that larger sends can have on your reputation. For example, if you are sending a single large newsletter once per month, think of ways to break that information up into smaller, more-targeted drip campaigns. Volume has two primary considerations: Overall volume Sender-specific volume For example, Gmail will get a sense of a given sender’s volume over time and utilize this benchmark as the foundation for what their expected volume should be. If you notice a decline in your open rates (or click rates if you disabled open tracking), try reducing your sending volume to individual recipients. IP and Domain Reputation Inbox providers use either the IP reputation or your domain reputation as the unique identifier for a sender. (Some inbox providers, like Gmail, track reputation with both.) IP Reputation: This represents the reputation designation for a given IP address. At Customer.io, this may represent the reputation of our shared IPs or any dedicated IPs that have been assigned to your account. Domain Reputation: This represents the reputation rating for all sending from a given domain. Your domain reputation can be influenced by the domain in your from address (Primary) and by the complaint rates of emails that include your domain in any links. A low IP or domain reputation is the most common reason that a given email might be filtered to spam. You can check the reputation of your sending domain (cio#####.<your-domain>.com) with tools like MxToolBox. To find your sending domain, go to Settings > Workspace Settings > Email > Sending Domains. Click Show Records, copy the host name and prepend to your domain. List Health Maintaining a list of engaged and active subscribers is essential to your email success. With that in mind, we recommend focusing on the following: Double Opt-in Filter on Engagement Sunset Policy Double Opt-in We advise all senders to verify any email address that is imported into Customer.io prior to sending email messages to them. Inbox providers are becoming increasingly strict in this regard. By verifying that the email addresses are correct through double opt-in, you can ensure that the contact information your recipients provide is valid and current. This will minimize the reputation damage that can arise from high bounce levels and low engagement. If you do not currently have a double opt-in mechanism in place, you can create a campaign to achieve this in Customer.io. Check out our recipe for building a double opt-in campaign within Customer.io. Filter on Engagement All non-transactional email audiences should be limited to only those recipients who have opened or clicked an email in the last 4 months to be considered engaged by the standards of major inbox providers like Gmail and Microsoft. Your own subscriber behavior and sending frequency can also influence this time frame. You can create a segment that matches only engaged recipients like this: Sunset Policy Are you removing people from your lists after a long period of no engagement? For example, anyone who hasn’t opened or clicked an email in the last 6 months should be removed from all non-transactional campaigns or newsletter audiences. To do this in Customer.io, you can identify inactive recipients by creating a segment that matches only people who have been sent emails in the last 6 months, but haven’t opened any of them. For better results, you could decrease the timeframe from 6 months to 5 months. The following image shows what such a segment configuration might look like: Keep in mind, people must be in Customer.io for at least the value of X days, or must have performed an event at least once before X days, for Customer.io to evaluate them against your conditions. Industry-specific engagement criteria will vary from business to business so the exact sunset criteria you adopt is entirely up to you. The goal here is to ensure that you have established a plan for removing recipients from your lists if they don’t engage or respond to re-engagement attempts. Unsubscribes Including an unsubscribe link in your emails is one of the cornerstones of modern deliverability best practices. All recipients should have the option to unsubscribe from your email whenever they choose to via an unsubscribe link (usually placed at the footer of an email). In addition to offering unsubscribes, it is of the utmost importance that people who have unsubscribed stop receiving your emails. At Customer.io, we make this part of the equation easy. Check out the documentation on our built-in unsubscribe functionality and subscription center to learn more. Content Spam filters are better than ever at recognizing spam content in an email. Most major providers will compare a given email to other emails that have received high complaint rates. By personalizing your content and ensuring that your emails are relevant to your recipients’ relationship with your brand, you can avoid potential filtering caused by sending content that is deemed spammy.  Don’t underestimate your sender reputation The landscape of email deliverability has changed a lot in the last few years. While your email content is a major factor, inbox providers heavily weigh your individual sender reputation when analyzing the content of a given message. Content scanning is primarily designed to identify phishing attempts or other unwanted email. With that said, here are a few general guidelines: Spammy subject lines and content: LOTS OF CAPITAL LETTERS, dollar signs or emojis. Use your best judgment here. Content relevance to the audience: Are you sending email about topics your recipients will look forward to reading? More importantly, are your recipients expecting an email from you? Image to text ratio: Ideally, aim for an image to text ratio with images no more than 40% of the content. Content that only contains images will generally perform very poorly with Gmail’s content filter. We also suggest not having content with a large number of CTAs (‘calls to action’ like buttons or links). The main CTA should be above the fold (visible on-screen when first opened) and we say no more than 3 to 4 calls to action total should be in the content. The ReallyGoodEmails website has some really strong examples of good email content. We recommend looking there for examples that are similar to what you’re planning to send in order to get some ideas and guidance. Mobile-friendly content: Use responsive/mobile-friendly code. URL Shorteners: Avoid them at all costs. These are commonly used by spammers to hide malicious links, so inbox providers will very likely filter emails with shortened links in the body. --- ## Domain Authentication URL: https://docs.customer.io/journeys/authentication/ You will need to add certain records to your DNS provider to allow Customer.io to send emails using your domain. Why we require domain authentication Creating great copy means nothing if your messages don’t make it to the People you’re trying to reach. Although it’s just one piece of the deliverability puzzle (along with your copy and overall reputation), authenticating your email domains helps your messages reach your users. Check out our post on Email Deliverability to know more about how it works. In addition to improving email deliverability, authenticating your sending domains in Customer.io will also let you control the appearance of your tracked links. How about Universal Links? If you need to enable them for your mobile app, HTTPS domain authentication is required as well.  Do you use Apple’s Private Email Relay? If your app or website uses the Sign In with Apple feature, you’ll need to set up your domain with Apple to support users who enable the Hide My Email option. See Authenticating for Apple Private Email Relay. We will not take over your root domain To verify and authenticate your domain for sending in Customer.io, you need to place our DNS records in your account-specific subdomain (like: cio#####.yourdomain.com and krs._domainkey.cio#####.yourdomain.com) rather than the root domain. Our authentication records are stored in a subdomain so they won’t conflict with other settings in your DNS host. By using a subdomain for authentication, we can ensure that: our SPF record is valid, passes DMARC alignment, and will not count towards the lookup limit of your existing SPF record our MX record does not disrupt incoming emails to your domain’s email server our DKIM record has the correct public-key needed to sign the emails that you send from our servers  Do not remove your domain’s DNS records When adding your Customer.io DNS records, don’t delete records for your own domain! If you do, you won’t be able to receive emails.  Set up DMARC on your root domain In your hosting provider, make sure you only include _dmarc as a subdomain on your root domain, like _dmarc.root-domain.com. You should not add it to your subdomains. Add a sending domain A sending domain defines who and where your emails are from. You can add multiple sending domains to a workspace, but you must set up at least one before you can send emails from your workspace. If you need more than 10 sending domains, please let us know why so we can make sure we support your use case. If you’re setting up a new workspace, you can configure your domain during the set up process. We show you how that works in the Domain Authentication section below. If you did not configure your email messaging channel when you set up your workspace, or you just want to add new sending domains, you can: Go to Settings > Workspace Settings > Email and click Add Sending Domain. Enter the Domain, Display Name, and Email Address that you want to send messages from, and click Add Domain.  Email address is optional at this stage, but necessary for domain verification. A from address is required to finish domain authentication. While you do not need to add it in this step, you will need to before finishing set up. Unless you use a custom SMTP server, you must authenticate your sending domain before you can use your new sender. Authenticate your domain automatically When you add a sending domain, you can use the Automatic setup option to configure your DNS records without leaving Customer.io—so you don’t have to go through the hassle of configuring your DNS records manually. Automatic setup uses Entri, a secure third-party service that connects to your DNS provider and applies the required records for you. Entri supports over 50 DNS providers, including GoDaddy, Namecheap, Cloudflare, Amazon Route 53, and more. You still need access to your DNS provider. As a part of this process, you’ll log into your DNS provider via Entri; from there, Entri will apply the required DNS records for you. Entri is SOC2 and GDPR compliant and doesn’t store your credentials or personal information. Go to Settings > Workspace Settings > Email and add a sending domain or click Show Records for the domain you want to authenticate. Click Automatic setup and then click Continue. Enter or confirm your domain, and Entri detects your DNS provider. Log into your DNS provider when prompted. Entri uses this connection to apply the required DNS records. When the setup completes, close the window. A propagation banner appears while your DNS records take effect. Verification can take up to 72 hours, though it typically happens much faster. Authenticate your domain with Cloudflare After you’ve added a sending domain, you’ll be prompted to configure your domain manually or with Cloudflare. If Cloudflare is your domain provider, you can save time and avoid inputting your domain records manually: Click Configure with Cloudflare. We’ll redirect you to sign in with Cloudflare. Click Authorize to apply the DNS records in Cloudflare. Cloudflare will redirect back to Customer.io. Click Verify domain to finish set up. Note, verification can take up to 72 hours. Authenticate your domain manually Add DNS records to hosting provider After you’ve added a sending domain and included a From address, click Show Records to locate the DNS records: To authenticate your domain, you’ll need to add four DNS records to your DNS hosting provider for each domain you’ll send from: MX Record: One MX record with two hostnames are required for delivering email from your domain. MX (Mail Exchange) records, in this case, have a specialized purpose which is to create a custom return-path using your subdomain. This is specifically for receiving bounce and spam feedback. This not only helps improve deliverability, but also allows your Customer.io emails to pass DMARC alignment. SPF Record: One TXT record that allows Customer.io to sign your emails as an authorized sender. SPF (Sender Policy Framework) records are used to identify which IP addresses are allowed to send email using your domain. DKIM Record: One TXT record that allows Customer.io to create an encrypted signature on emails sent on your behalf. DKIM (Domain Keys Identified Mail) signatures ensure that the message that arrives at the inbox provider is identical to the message that you sent. DMARC Record: As of Feb 1, 2024, we now check whether your DMARC policy meets new sending requirements from Gmail and Yahoo. One TXT record that allows you to specify how you want to handle email that fails authentication. DMARC (Domain-based Message Authentication, Reporting, and Conformance) records specify how to handle email that fails SPF and DKIM checks. The minimum requirement is v=DMARC1; p=none. If a DMARC policy is already in place in your hosting provider, we will surface your record, rather than our minimum requirement. Make sure you only add _dmarc as a subdomain on your root domain, like _dmarc.root-domain.com. You should not add it to any of your subdomains. Next, click Verify domain for the domain you want to authenticate. Note, it may take up to 72 hours to complete the verification process.  Want to send secure HTTPS links? You can enable HTTPS link tracking by adding another CNAME record to your host. See HTTPS link tracking for more information. Verify domain After you’ve added these records to your DNS host, and they’ve had time to propagate, you will need to come back to the Authentication tab of your domain and click Verify domain again. If all three records show green checkmarks, then you’ve successfully verified your domain and are all set to start sending from it! You will need to repeat this process for any other domains you want to send from. Your domain will remain unverified until we are able to process that all three records have the expected values. It can take anywhere from seconds to hours for verification to come through. We recommend double-checking your DNS configuration for typos and spaces before attempting to verify again. If you aren’t sure how to proceed, you’re welcome to reach out to us at win@customer.io for assistance. DMARC policies DMARC stands for Domain-based Message Authentication, Reporting and Conformance. It is an email authentication protocol that provides a way for email domain owners to protect their domain from unauthorized use, such as phishing or email spoofing.  As of Feb 1, 2024, we now check whether your DMARC policy meets new sending requirements from Gmail and Yahoo. DMARC works by allowing domain owners to publish policies that specify which email messages are authorized to come from their domain. When an email is received, the recipient’s email server checks the message’s authenticity by verifying the email’s alignment with the published DMARC policies. If the email passes the authentication checks, it is delivered to the recipient’s inbox. If it fails, the email may be quarantined or rejected, depending on the DMARC policy settings. DMARC also provides reporting capabilities that allow domain owners to receive feedback on email authentication results. This information can be used to monitor email security and identify any attempts to impersonate their domain. In summary, DMARC is a protocol that enables domain owners to protect their domain from email spoofing and provides reporting capabilities to monitor email authentication results. By using DMARC, organizations can improve email security, protect their brand reputation, and reduce the risk of phishing attacks. Relax your aspf and adkim tags If your domain is utilizing your DMARC policy and you plan to send your emails through Customer.io’s built-in delivery servers, you will need to ensure that your DMARC policy is using r (or relaxed) values for the aspf and adkim tags. If your policy doesn’t specify aspf or adkim tags, then they are already relaxed by default and no changes to your policy are necessary. The reason for this requirement is because Customer.io uses an account-specific subdomain of your verified domain (ex. cio#####.yourdomain.com) for signing the return-path, SPF, and DKIM headers in the emails our servers send from. Under the default, relaxed policy, the emails you send from Customer.io will pass DMARC alignment because of the parent/child match of your mail-from domain (from address) and header-from subdomain (return-path) we sign to your emails. For context, a strict (s) policy for these tags would require an identical match of the domains in these headers, which would result in your Customer.io emails failing to pass alignment. Failing DMARC alignment could then cause your emails to either bounce back (p=reject) or filter to spam (p=quarantine), depending on your DMARC policy. For example, you may need to relax your DMARC policy if your recipients see the error Warning: Unverified sender in your emails: Set up link tracking for your domain Customer.io provides link tracking by default, but you can also track links on your own subdomain. Resources For your convenience, here is a list of links to the instructions for adding DNS records at commonly used hosts: 123 Reg - MX | TXT | CNAME bluehost - MX | TXT and CNAME DNS Made Easy - MX | TXT | CNAME DNSimple - TXT | CNAME Dreamhost - MX | TXT | CNAME DYN - MX, TXT, and CNAME GoDaddy* - MX | TXT | CNAME Hostgator - MX | TXT and CNAME Hover - MX, TXT, and CNAME Media Temple - MX, TXT, and CNAME Namecheap - MX | TXT and CNAME Network Solutions - MX | TXT and CNAME Register.com - MX | TXT | CNAME *Instead of entering the full hostname (like cio12345.yourdomain.com), these providers automatically append your domain to the record. Enter just the front portion of the hostname (like cio12345) when adding records to these providers. See FAQ below for screenshot examples. FAQs What is Entri? Entri is a third-party service that Customer.io uses to provide automatic domain setup. When you choose automatic setup, Entri opens a popup window that connects to your DNS provider and applies the required DNS records for you. Entri supports over 50 DNS providers, covering approximately 75% of the market. Is it safe to enter my DNS credentials in the Entri window? Yes. Entri is SOC2 and GDPR compliant. It uses your credentials only to apply the DNS records that Customer.io requires — it does not store your login information or personal data. If you prefer not to use automatic setup, you can always configure your records manually. What if my DNS provider isn’t supported by Entri? If Entri doesn’t support your DNS provider, you can authenticate with Cloudflare or set up your records manually. Do I need to authenticate my domain if I’m using a custom SMTP? When using custom SMTP, you do not need to authenticate your domain in Customer.io. However, you should check your custom SMTP provider’s documentation to see if you still need to add DNS records (such as SPF and DKIM) to your domain to use their services successfully.  Branded link tracking with custom SMTP If you want to use branded custom link tracking in Customer.io (using your domain instead of “customeriomail.com” when generating tracked links), you must verify the domain and add the CNAME record shown in the Domain Settings section of your workspace. The CNAME record will not validate your domain for branded link tracking if your domain has a HSTS policy, but does not currently have SSL coverage. Please see our HTTPS Link Tracking documentation for more information on getting this set up. How do I verify my records are there? On the Sending domains page, we’ll show you the verification status of any domains you’ve added. Domains will have one of the following statuses: Verified: The domain’s DNS records have been verified and the domain can be used to send signed emails. Unverified: The domain’s DNS records have not been verified and the domain cannot be used to send emails. Undetermined: The domain’s status cannot be determined because the From Address uses liquid code. Note: We will not be able to send your emails until you verify your domain. How do I add another “From Address”? The domain list is made up of domains used in the from addresses that are configured in your account. If you want to add another from address, navigate to Workspace Settings in the left-hand menu, choose Email, then select Add From Address beside the appropriate domain name. What if I don’t add the DNS records? What happens? If you are setting up a new sending domain through our built-in delivery server, you must add all three required DNS authentication records. This not only verifies the domain’s ownership, but also authorizes Customer.io to send from the domain. If you do not add the DNS records and verify the domain, you will not be able to send from it. If you have already verified a domain but the DNS records are removed at a later point, this can result in your emails bouncing back or being filtered to spam due to lack of authorization. What if my records change after verification? Customer.io only checks for your DNS records when you click “Verify domain." We do not continuously poll to check that the records are still in place. If you notice deliverability issues, check to make sure your DNS records are current and present, then verify your domain again. Do I need to add both SPF and DKIM? Yes. Our email server requires that both SPF and DKIM are verified in order for us to send email from your domain. If either record is missing, you may see your deliveries fail to send due to the domain no longer being verified. The SPF record is correct, but it’s not verifying. Make sure you’re using a TXT record as indicated in our instructions, and not a SPF type record. It’s also required that your SPF record is placed inside of the Customer.io-specific subdomain, rather than your existing SPF record in your root domain. If the record is still not verified, get in touch and we’ll troubleshoot the issue with you! I’m using GoDaddy and my DNS records are still not verifying. GoDaddy already adds your domain when creating DNS records, so it’s likely that your domain is being posted twice to the records. Simply update the record to be only the subdomain value (as shown below) and re-verify after a few minutes. GoDaddy MX Record example GoDaddy SPF example GoDaddy DKIM example You can confirm this by checking your DNS using a free online tool like viewDNS.info and testing the full hostname URL listed in your Customer.io email settings (like cio12345.yourdomain.com). If the DNS records don’t appear, then double check that your records are set up correctly. I’m getting an error in my DNS panel when trying to add the records, what can I do? Underscores: Some hosts do not support underscores (_) in DNS records, and adding the DKIM record can cause an error. The underscore is required and you’ll want to contact your host to see if they disallow underscores entirely or if they can manually add the record for you. Semicolons: Some hosts require that you escape semicolons in records. If you’re getting an error try replacing ; with \;. Will adding authentication affect my regular email? No. The records are written specifically to allow our servers to send for you but not to disallow other servers. My host doesn’t support TXT records. What do I do? Some DNS hosts won’t allow you to add records yourself, but will add them for you. As a first step we recommend you talk to your hosting company to see if they can help. If you aren’t able to add our DNS records for authentication, you may need to consider moving your DNS hosting to a separate provider (like DNSimple, DigitalOcean, Cloudflare, etc). This would allow you to set up custom records while likely still being able to point your domain to the website hosting provider you’re using. If you do consider this, make sure your current website hosting provider supports this. Why can’t I verify my domain with Wix? Wix doesn’t allow you to add a sub-domain in an MX record, preventing you from verifying your domain. If you use Wix, you might consider setting up a custom SMTP server. How do I add multiple MX records for my AWS-hosted domain? AWS doesn’t allow you to set multiple MX records per hostname. To get around this issue, add a single record that has both MX values on separate lines. For example: 10 mxa.mailgun.org. 10 mxb.mailgun.org. --- ## Delete a domain URL: https://docs.customer.io/journeys/delete-domain/ You can delete a sending domain when it's not in use. Go to Workspace Settings > Email. Click Show Records to the right of the domain you want to delete. Click Delete domain. Confirm your action. The domain will no longer appear in settings and the corresponding From addresses will not be available for emails. Remove a domain from messages If any email uses the From address for the domain you want to delete, you won’t be able to delete the domain. You can either: Change the From address to one belonging to a differenrt sending domain. Delete the email using the From address. To see which messages use your From address, click the three dots next to it and select View usage. You’ll land on Content > Message Library where results are filtered to messages using your From address. --- ## Domain warming URL: https://docs.customer.io/journeys/domain-warming/ To ensure your emails successfully reach your recipients, it's important to build a good reputation for the domain you're sending emails from, which is where a **domain warm-up process** comes in.  Automate your warm-up with Daily ramp for newsletters! If you use newsletters to warm up your sending domain, add a rate limit and set a daily ramp period to get your system up and running. This eliminates the need to create multiple newsletters or messages for your warm-up schedule! Overview Think of warming up your domain like introducing yourself to email providers (like Gmail, Yahoo, etc.) and saying, “Hi, I’m trustworthy and send relevant, valuable emails.” If you suddenly start sending large amounts of emails from a domain that hasn’t sent much before, it’s like yelling into a quiet room — inbox providers may see this as suspicious and will block or filter your emails. By sending small, controlled batches of emails first and gradually increasing the volume over time, you show email providers that: Your emails are legitimate. Recipients are engaging with your messages (opening & clicking). You’re not spamming people or sending unwanted messages. This structured approach helps you build trust with inbox providers and ensures your emails land in inboxes instead of spam folders. It’s especially critical for new domains or those without a solid history of sending emails, as ISPs use this history to evaluate your trustworthiness. Warm-up schedule for established sending domains If your sending domain already has a history, we recommend the following ramp-up schedule. Each stage typically lasts one day. Daily and hourly message rates for established domains Stage / Day Daily Message Rate Hourly Message Rate 1 1,000 100 2 2,500 300 3 5,000 600 4 6,500 800 5 8,000 1,000 6 10,000 1,500 7 14,000 2,000 8 20,000 3,000 9 25,000 3,500 10 35,000 4,500 11 50,000 6,500 12 80,000 10,000 13 125,000 16,000 14 175,000 25,000 15 250,000 50,000 Key Notes: The daily limit is more critical than the hourly rate, but we recommend applying rate limiting whenever possible. Never send more than double the volume from the previous stage on any given day. Warm-up schedule for new domains If your domain is brand new and has no recent traffic, start much slower. Use the following plan tailored for new domains: Daily message rates for new domains Stage / Day Daily Message Rate 1 100 2 200 3 350 4 500 5 750 6 1,000 7 1,500 8 2,000 9 3,000 10 5,000 11 8,000 12 15,000 13 25,000 14 50,000 Core best practices Start with your best subscribers: Always front-load the initial cohorts with your most engaged subscribers based on email open activity (not in-app or purchase activity). Engagement metrics are critical during warm-up. Limit volume increases: Never send more than double the volume from the previous stage. For lists exceeding the totals in the charts above, follow the rule of doubling after day 15. Focus on recent engagement: Only warm up with subscribers who have opened emails within the last 120 days. Use recommended practices: Refer to our Email Deliverability Best Practices for additional tips on maintaining deliverability. Warm up only to your regular sending volume: You only need to warm up your domain to the maximum number of emails you plan to send regularly. For example, even if you have 100,000 subscribers, if you only send bulk emails to 35,000 of them on a regular basis, you only need to warm your sending capacity to 35,000. This helps avoid unnecessary volume increases and maintains deliverability. Broadcasts vs Campaigns Broadcasts: Easier to troubleshoot in case of issues or unexpected results. Ideal for most warm-up scenarios. Broadcasts support daily ramp, which automates the warm-up schedule for you. Campaigns: Some clients use campaigns with low volumes, starting a new one each day during the warming period to align with the ramp-up schedule. Either method works as long as you adhere to the plan. --- ## Track links with your domain URL: https://docs.customer.io/journeys/link-tracking-custom-domain/ Set up link tracking for your domain Customer.io provides link tracking by default, but you can also track links on your own subdomain. To use a custom subdomain for tracked links using HTTP in Customer.io, add a CNAME (Canonical Name) record to your DNS host that aliases our tracking subdomains. These are e.customeriomail.com or e-eu.customeriomail.com depending on your Customer.io account region. To use a custom subdomain for tracking links using HTTPS, you’ll need to take additional steps outside of Customer.io, such as setting up a reverse-proxy server. Automatic setup (HTTPS) If you use HTTPS link tracking, you can click Automatic setup to configure your link tracking records without leaving Customer.io. Automatic setup uses Entri, a secure third-party service that connects to your DNS provider and applies the required records for you. Entri is SOC2 and GDPR compliant and doesn’t store your credentials or personal information. Go to Settings > Workspace Settings > Email, go to the Link Tracking tab for one of your domains, and click Automatic setup. Click Continue and log into your DNS provider when prompted. Entri applies the CNAME and TXT records automatically. When the setup completes, close the window. Verification can take up to 72 hours, though it typically happens much faster. If your DNS provider isn’t supported by Entri, or if you use HTTP link tracking, use the manual setup below. Manual setup To configure link tracking records manually: Click Show Records within Workspace Settings > Email and navigate to the Link Tracking tab. Enter your subdomain. Find the CNAME record and add to your domain’s DNS host. Return to the Link Tracking tab of your domain and click Verify domain. We will check that the record is in place. It may take some time for changes to propogate to your workspace. A green checkmark beside the CNAME header means we verified your configuration.  You must verify your sending domain before your tracked links can use this domain. On the left hand side of the tab, you will see the status of your link tracking. HTTP link status: A green HTTP link status (shown bottom left) means we are able to connect to your CNAME domain overr HTTP without error. Unless you have successfully configured HTTPS Link Tracking, we’ll generate HTTP links whenever link tracking is enabled in your messages. A red HTTP link status (shown below middle) indicates that our check found a HSTS (HTTP Strict Transport Security) policy on your domain. This means that you must set up HTTPS Link Tracking in order for your custom subdomain to resolve your tracked links correctly. HTTPS link status: A green HTTPS link status (shown below right) means you have successfully configured HTTPS Link Tracking and we’ll generate HTTPS links in messages sent from this domain that have link tracking enabled. The domain must also be verified before your tracked links can use this domain. HTTPS Authentication To verify HTTPS for non-mobile links, visit HTTPS link tracking. If you also need support for links to iOS or Android apps, checkout universal links. Disable link tracking for your domain You can revert back to tracking links through Customer.io (https://e.customeriomail.com / https://e-eu.customeriomail.com) if you’re having trouble with your custom subdomain. Go to Workspace Settings > Email and click Show Records. Click the Link Tracking tab. Under Enter your subdomain, change the Host Name to a non-existing value (ex abc). Click Verify Domain to remove the link tracking verification from your domain. Click Close to check that the domain has fallen back to default tracking: --- ## IP addresses: shared vs dedicated URL: https://docs.customer.io/journeys/ip-addresses/ You can use our shared IP pools or dedicated IPs to send messages to your customers. Shared IP pool (default) By default, we add email domains to Customer.io’s shared IP address pool to send emails. We manage and monitor multiple IP pools and remove domains that perform poorly to maintain high deliverability. Send from transactional IP pool We also maintain a separate, transactional IP address pool. This pool has even higher standards—stricter bounce and spam thresholds—than our default, shared IP pool. This ensures that your transactional messages achieve the highest deliverability. Only domains used with the transactional service can send emails over this IP pool. To use our transactional IP pool, you’ll need a separate sending domain from your parent domain dedicated to transactional sending. Once that domain is set up, you can request to have it added to the transactional IP pool from your email settings by selecting Show Records and navigating to the Mail Servers tab.  When we add a domain to the transactional pool, you can no longer use it to send campaigns or broadcasts. Send from dedicated IPs Premium This feature is available for Premium plans. Enterprise This feature is available for Enterprise plans. Sending from a dedicated IP is the only way to take full control over your sending reputation. While we go to great lengths to ensure the health of our shared IP network, there is always the chance that your deliverability could be impacted by the sending habits of other Customer.io accounts. Benefits of a dedicated IP One of the main benefits of using a dedicated IP address is that it allows organizations to manage their own IP reputation. This means that they can control how their emails are perceived by email providers and recipients. By sending emails from a dedicated IP address, you can build a positive reputation directly with email providers at the IP level. A dedicated IP address can also improve email deliverability. This is because the IP address is exclusively used for your email sending, and there is no risk of it being blocklisted due to other senders’ actions. This can increase the likelihood of emails being delivered to recipients’ inboxes, rather than being filtered into the spam folder. Requirements for sending from a dedicated IP The only requirement is that you have a minimum volume threshold of at least 50,000 emails per week on the dedicated IP across all of the domains using it. We set this threshold because low email volume can result in unpredictable deliverability performance with inbox providers. Cost of a dedicated IP Premium level customers can request up to three dedicated IPs at no extra cost. Outside of that, each dedicated IP is $50 per month. Request a dedicated IP To request a dedicated IP for your Customer.io account, first make sure you meet the minimum volume limit. Then go to your email settings and click Show Records. Select the Mail Servers tab followed by Dedicated IPs and fill in the form. We will review your request, sending volume, and any other influencing factors prior to beginning the warming process. Set up and IP Warming At Customer.io, we completely handle the setup, warming, and configuration of your dedicated IPs. All that is required from you is to send email normally, and we’ll take care of the rest. Depending on your volume, the warming process can take 30-45+ days. Senders with a higher volume generally move through the warming stages more quickly. During warming, you will send from your currently assigned, shared IPs as well as your new dedicated IPs. As time progresses, we will slowly increase the amount of email sent via your dedicated IP until warming is complete. When your IP is ready, we’ll shift all sending to your dedicated IP and let you know you’re good to go. --- ## Spamhaus blocklist listings URL: https://docs.customer.io/journeys/spamhaus-blocklist/ If your sending domain appears on a Spamhaus blocklist, your emails may be rejected or filtered to spam. Learn what a listing means and how to resolve it. Spamhaus maintains one of the most widely used blocklists on the internet. When your domain is listed on the Spamhaus Domain Block List (DBL), it can’t be verified or used for sending in Customer.io until it’s removed — and delivery to most major email providers is blocked in the meantime. Listings can escalate quickly: a domain listing can lead to an IP listing, which can result in a broader network block. That’s why it’s important to act as soon as you become aware of one.  When you send through Customer.io’s managed email service, we monitor our shared and dedicated IPs for blocklist activity. Domain-level listings (DBL), however, are tied to your sending domain and are your responsibility to monitor and resolve. What caused this? Most Spamhaus listings are the result of a spam trap hit or a list bomb attack — meaning your domain was likely targeted rather than intentionally misused. That said, the listing still needs to be resolved before your domain can be re-enabled. Resolve a Spamhaus listing Step 1: Check your listing and request removal Start at check.spamhaus.org. Enter your domain and Spamhaus will show you: The reason for the listing The specific steps or dispute process required for removal This is your primary resource — Spamhaus handles the entire removal process there, and their guidance will be specific to your situation. To find your Customer.io sending domain, go to Settings > Workspace Settings > Email > Sending Domains. Click Show Records and note the hostname. Step 2: Complete the required remediation steps Depending on the reason for the listing, Spamhaus typically requires one or more of the following before they’ll approve a removal: Add CAPTCHA to your subscription forms to prevent automated sign-ups Implement confirmed opt-in (COI): send a confirmation email to new subscribers and only add contacts who explicitly confirm. See our guide to double opt-in for setup instructions. Clean up your list: run re-engagement campaign and remove unengaged or invalid contacts Step 3: Submit your removal request Once you’ve addressed the root cause, submit your removal request through check.spamhaus.org. Spamhaus will walk you through the dispute process directly. Spamhaus reviews removal requests manually. Processing can take anywhere from a few hours to several days. You must fix the underlying problem before requesting removal! Submitting a request without addressing the cause will likely result in a rejection. Step 4: Re-verify your domain in Customer.io Once Spamhaus has cleared your domain, return to Customer.io and re-verify it to resume sending. Go to Settings > Workspace Settings > Email > Sending Domains and verify your domain again.  Need to keep sending? Customer.io supports multiple sending domains. If your primary domain is listed, you can set up and send from an alternate domain while you resolve the listing. However, be cautious about suddenly shifting all your email volume to a domain that hasn’t been used for that purpose before — this can trigger additional deliverability issues. If you’re considering this, warm up the new domain first. Tips for avoiding future listings Use confirmed/double opt-in for all new subscribers Regularly clean your list: remove contacts who haven’t engaged in 6–12 months Watch for sudden, unexpected spikes in new sign-ups, which can be an early sign of a list bomb attack Never purchase or import email lists from third-party sources For more deliverability best practices, see our email deliverability guide. --- ## Email suppression lists URL: https://docs.customer.io/journeys/esp-suppression/ Email service providers (ESP) maintain lists of suppressed email addresses, preventing you from messaging people who have experienced a hard bounce or logged a spam complaint. If you manage email deliveries through Customer.io (you don't have a Custom SMTP server), you can view and manage suppressed email addresses in Customer.io. Email service providers (ESP) keep suppression lists to ensure they don’t send messages from servers that rejected them before or from people who explicitly do not want messages from you. A message experiences a hard bounce. This means that either the recipient email address does not exist or the recipient email server blocked delivery. A person lodges a spam complaint against a message. ESP suppression only prevents people from receiving emails. People with suppressed emails can still receive messages from other channels, start campaigns, and more. If you want people with certain identifiers to never again receive messages or start workflows in your workspace, learn how to delete and suppress identifiers. When a suppressed email can still receive emails Email addresses suppressed based on message delivery can still receive emails from domains in your workspace not associated with the message delivery. For instance, if you send transactional messages with a different domain, this address could still receive order receipts. However, suppression based on our ESP Suppression API will prevent the email address from receiving an email from any domain across your workspaces. So even if you send transactional messages with a different domain, this address wouldn’t be able to receive order receipts, password resets, or any email you send as a transactional message. flowchart LR c{"Is the email address suppressed?"} -- yes, using ESP Suppression API --> f["Person won't receive any email across all domains in your account"] c -- yes, due to hard bounce or spam complaint --> h["Person won't receive email from domain the message came from"] c -- no --> g["Person can receive emails"] Suppression based on message delivery Email addresses suppressed based on message delivery (not our API) won’t receive emails from workspaces that use the domain associated with the suppression. For example, if you’re sending from sketcher.io in both Workspace A and Workspace B, and a hard bounce occurs from a message in Workspace A, we won’t send emails from sketcher.io to that recipient in both Workspace A and B. But they could still receive emails from another domain in your workspaces. Suppression based on API If you use Customer.io as your email service provider (ESP), you can look up and manage ESP-suppressions using our API. If you suppress an email address through this endpoint, that recipient won’t receive emails across any of your domains in your workspaces. This is more restrictive than how the ESP suppresses people based on message delivery. View suppressions If you manage email deliveries through Customer.io, you can find your ESP suppression list in Workspace Settings > Email > Suppression List.  Only Account Admins and Workspace Admins have access to view and manage the suppression list within a workspace. If you use a Custom SMTP server, you must manage your email provider’s suppression list directly through the ESP. Remove emails from suppression list To remove an email address suppressed by your ESP, follow these steps: Go to Workspace Settings > Email > Suppression List. Filter for and select the email. Click Unsuppress. ESP suppressions through the API If you use Customer.io as your email service provider (ESP), you can look up and manage ESP-suppressions using our API. You can look up suppressions by email address or by type (bounces, blocks, spam complaints, or invalid addresses). You can also remove addresses from the ESP’s suppression list. This action unsuppresses the email across all domains in your Customer.io account, not just the domain that sent the message that caused the suppression in the first place. How Customer.io observes the suppression list When a message experiences a hard bounce or a spam complaint, our email service provider (ESP) adds the email address to its suppression list. Customer.io observes this suppression list and does not send messages to addresses on the list. However, this does not mean that the person is deleted or otherwise modified in Customer.io. A person whose address is suppressed by the ESP still exists in Customer.io and is eligible to enter campaigns in Customer.io, etc. They just cannot receive emails through our ESP. Suppressed messages and billing For billing purposes, any message sent to our email service provider (ESP) counts towards billable messages. That includes messages that ultimately bounce, are suppressed, or are marked as spam. --- ## Google Postmaster Tools URL: https://docs.customer.io/journeys/google-postmaster-tools/ Google Postmaster Tools (GPT) is a service Gmail provides free of charge that shows data about your sending reputation and Gmail spam rates. (Gmail does not notify senders when emails are marked as spam for privacy reasons.) Senders must stay below a 0.3% spam threshold in GPT. If your recipient marks a message as spam and the email service provider sends that information back to Customer.io, we will automatically suppress that user profile so you can’t send to someone who clearly doesn’t want your messages in the future. However, Google does not send information about spam complaints back to Customer.io. Remember that when viewing your Customer.io dashboard, the spam information displayed does not include Gmail users! Google Postmaster Tools is the only place to view information about Gmail user-reported spam complaints.  Google Postmaster Tools does not show real-time data. It’s often a few days behind, so if you see a spam spike in GPT on a particular day, know that the spike may not actually be from an email sent on that day. Because of this reporting delay, it can be challenging to identify if a particular send caused a spike in spam complaints. Check out our blog article to learn more about the power of Google Postmaster Tools! Set up GPT Log in or sign up for a Google Postmaster Tools account. Locate your subdomain in Customer.io: go to Workspace settings > Email and click Show Records. The subdomain is a combination of the Host Name of your MX or SPF records plus your root domain - like cioXXXXXX.yourdomain.com. Copy the Host name record from the previous step and paste it in the domain section of your Google Postmaster Tools signup (left). Then add your root domain to the end of the record to assemble your Customer.io-specific subdomain (right). Verify your domain: add the records GPT provides you to your domain registry to prove ownership of your domain. (Optional) Grant Customer.io read-access to your account so we can help you troubleshoot deliverability issues. In Google Postmaster Tools, click the three vertical dots by your domain then click Manage Users. Click the red icon. Enter ami@customer.io in the email address field. Click Next to finish. Now you can monitor your sending domains for Customer.io in Google Postmaster Tools. If you gave us read-access, we’ll also be able to better troubleshoot deliverability issues with you! --- ## Custom unsubscribe links (RFC 8058) URL: https://docs.customer.io/journeys/custom-unsubscribe-links/ Offering an unsubscribe option to give subscribers the ability to opt out of your marketing messages is not only standard best practice, it’s now a requirement in Google and Yahoo’s [Bulk Sender 2024 Requirements](https://customer.io/blog/email-sending-requirements-gmail-yahoo/). If you are using a custom unsubscribe solution, you’ll need to successfully implement [RFC 8058 List-unsubscribe-post](https://datatracker.ietf.org/doc/html/rfc8058) before June 1st, 2024. How it works Google and Yahoo have added new requirements, allowing recipients to unsubscribe from your marketing messages with a single click. If you use our default unsubscribe functionality, you don’t need to do anything. But if you use a custom unsubscribe solution, you need to follow the instructions on this page to implement the new requirements before June 1st, 2024. The new requirement is RFC 8058 List-unsubscribe-post. It defines true one-click unsubscribe through an HTTPS URI POST that triggers an unsubscribe within 48 hours. This requires technical development work This guide is intended for technical users and developers. You’ll need to do some development work to implement RFC 8058. If your organization does not have development resources available to implement RFC 8058 before the June 1st, 2024 deadline, we strongly recommend switching to Customer.io’s default unsubscribe functionality. Implement RFC 8058 for custom unsubscribes Because your custom unsubscribe system is unique to your organization, and the consumption and processing of the required HTTPS POST exists within your own system, our Technical Support and Deliverability teams are unable to advise in detail. But we are happy to provide the following general guidance: Step 1: Configure your system to receive the POST when recipients click “Unsubscribe” The core idea behind RFC 8058 is to create a header that defines the provider-surfaced link within Gmail and Yahoo email clients. Note that even if you implement RFC 8058, the link may not always appear. Google and Yahoo will surface the link at their discretion. Per the RFC 8058 documentation, your List-Unsubscribe-Post header: Should contain a unique HTTPS URI POST. Should point to an endpoint that is configured to receive the POST and process the unsubscribe for the recipient (within 48 hours). The message must also have a valid DKIM signature that covers at least the List-Unsubscribe and List-Unsubscribe-Post headers. For example, this email header: List-Unsubscribe: <mailto:listrequest@example.com?subject=unsubscribe>, <https://example.com/unsubscribe.html?opaque=123456789> List-Unsubscribe-Post: List-Unsubscribe=One-Click Should result in this POST request: Resulting POST request POST /unsubscribe.html?opaque=123456789 HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Content-Length: 26 List-Unsubscribe=One-Click Step 2: Update the headers for every email in your workspace You can add custom headers in our email editors, which you’ll now need to do. Learn more about adding custom headers in Customer.io. FAQ I use Customer.io’s built-in unsubscribe functionality, do I need to do anything? Nope! We’ve already implemented RFC 8058 automatically for you, well before the deadline. You only need to follow the instructions on this page if your organization is using a custom unsubscribe solution. What is “RFC ####”? An RFC (Request for Comments) is a document that lets individuals and organizations propose standards, discuss protocols, and share information related to the development and operation of the internet. They come from the Internet Engineering Task Force (IETF) and the Internet Society (ISOC). RFCs cover a wide range of topics related to networking, communication, and the internet in general. Each RFC is assigned a unique number. What are the different types of unsubscribes? RFC 2369 (the previous industry standard) Defines the use of URLs in email headers to convey core mail list commands, including subscribing and unsubscribing Defines the “List-*” family of headers, such as “List-Unsubscribe”, “List-Subscribe”, etc RFC 8058 (the new requirement) Defines true one-click unsubscribe through an HTTPS URI POST that triggers an unsubscribe within 48 hours Provides options for both email-based and web-based unsubscribe methods --- ## Authenticating for Apple Private Email Relay URL: https://docs.customer.io/journeys/authenticating-for-apple-private-email-relay/ If your app or website utilizes the [Sign In with Apple](https://support.apple.com/en-us/HT210318) login service, then your customers have the optional setting to receive emails from you through the Apple Private Email Relay service. This lets customers hide their email addresses as an added layer of privacy when signing up and logging into your app. When a customer enables the Hide My Email option, Apple generates a unique and randomized email address at the @privaterelay.appleid.com domain that you can associate with that customer. After you pass these addresses into your Customer.io workspace, you need to do a few more things to ensure that your messages are delivered to the Apple Private Email Relay. To start, you need to log into your Apple Developer Portal and go to Certificates, Identifiers & Profiles > More > Sign in with Apple for Email Communication > Configure. From there you can: register your Sending Domains register your Communication Emails verify that your domains are authenticated with SPF and DKIM  Apple has multiple Hide My Email services As of iOS 15, Apple has two distinct services called Hide My Email, which are available through both Sign in with Apple and iCloud+. You only need to follow this guide for apps and websites that use the Sign in with Apple option, which generates a unique email for the purposes of signing up for a service and logging in. See Apple: What is Hide My Email? for more information. Register your Sending Domains Apple requires that you add the domains that you plan to send emails from when you send messages to their service. In addition, you need to add your “return-path” domain if it is different from the sending domain. If you use Customer.io’s built-in delivery services, the return-path domain for your emails is different, as it utilizes a subdomain. This is the subdomain that you see for the MX and SPF records in your workspace’s Domain settings, which uses the format cio#####.yourdomain.com. Be sure to add both the domain and subdomain as Apple Email Sources. If you use a SMTP provider with Customer.io, be sure to locate the return-path that service uses in your settings so that you can add it in your Apple Developer Portal. Register your Communication Emails In addition to your Sending Domains, Apple requires you to register all of the email addresses that you plan to send from at those domains. In Customer.io you can see all of your From Addresses listed under your workspace’s Email settings. Apple lets you add these addresses individually or by entering a comma-delimited list. If you have other email sources that you may use to send emails to Private Relay addresses outside of Customer.io, be sure to add those too. Lastly, Apple also requires you to add your domain’s feedback address. This address is used by our email server to receive bounce feedback from the email services you send to. Your domain’s feedback address is “postmaster” at the return-path domain. It looks like: postmaster@cio#####.yourdomain.com. Authenticate your Sending Domains If you use Customer.io’s built-in delivery services and your domain is shown as Verified, then you have already completed this step! If you haven’t verified your domain yet, you can find documentation on how to do that here. By default, Customer.io and our sending partner require that all domains be authenticated with both SPF and DKIM to send email from your account. By meeting these requirements, your sending domain also meets Apple’s authentication requirement as well. If you use a SMTP service with Customer.io, check your provider’s settings and documentation to ensure that your sending domain is authenticated to send from their servers. Troubleshooting You may occasionally see some of your emails bounce back from @privaterelay.appleid.com. While the bounce reasons may not contain a lot of detail, a few common reasons we’ve seen are that: the customer has deleted the Hide My Email address from their Apple settings the customer reached their limit of 100 emails per day to and from their Hide My Email address you may need to go back and check your configurations to ensure that all of your Sending Domains and From Addresses are authenticated and registered with Apple. In addition to monitoring your delivery logs in Customer.io, Apple can notify the Apple Developer account owner and admins when emails aren’t delivered to the relay. You can configure this setting in the Apple Developer Portal. Conclusion After you have completed all 3 of these steps, you should be ready to send emails to your customers who are using Hide My Email addresses through the Apple Private Email Relay. For further reference on Apple Private Email Relay and its configuration settings, see Apple’s documentation: Apple: Configure Private Email Relay Service Apple: Communicating Using the Private Email Relay Service Have more questions? Email our support team at win@customer.io --- ## Verify deliverable email addresses with Kickbox URL: https://docs.customer.io/journeys/verify-email-deliverable-kickbox/ Sending messages to undeliverable email addresses can increase your bounce rate and impact overall deliverability. You can use Kickbox to verify the deliverability of addresses before you send messages, helping you maintain high deliverability. Introduction You don’t just want to know that email addresses are valid (i.e. they comply with RFC 5322); you want to know that addresses are deliverable. Sending messages to undeliverable email addresses can increase your bounce rate and impact your deliverability. If you’re not able to verify the deliverability of an email address before you add it to Customer.io, you can always leverage Webhook actions and a third-party verification service like Kickbox. This allows you to evaluate the deliverability of the email addresses in your workspace and identify the ones you shouldn’t email.  This method is provider-agnostic While we’ll use Kickbox in our example below, you could use any email verification service that has an API. Ingredients A Data-Driven Segment A Segment-triggered Campaign Basic API and Webhooks knowledge A Kickbox account Method We’ll create a Segment-triggered campaign for people who haven’t been verified yet, send an API request to Kickbox to verify their email, and save the result as attributes to leverage that information later. Get your Kickbox API key. Create a Segment of unverified emails (to use as a campaign trigger). Create a Segment of verified emails (to use as a campaign goal). Create a Campaign with a webhook action to verify people. Get a Kickbox API key We’ll start by getting a Kickbox API key. You can go to kickbox.com to create an account. Your account will be provisioned with 100 credits (i.e. 100 email addresses to verify) and you can purchase extra credits. Go to the Verification section of the left sidebar and click API. Click Manage Keys. Click to create a new key. In the modal window, specify a key name (1), activate it for Production Mode (2), select the scope of permissions (3) before finally creating it (4). When this is done, you’ll see your API key in the list and you can click the eye icon to unmask it. Create a Segment to use as a campaign trigger In Customer.io, go to Segments and click on Create Segment. Give your new segment a Name and Description then click Create Data-Driven Segment. Add a single condition for this segment: Attribute email_verified is not equal to true. Save the conditions. Create a Segment to use as a Campaign goal Follow the same steps that you used to create a campaign trigger create a second segment. Make sure it’s set as “Segment for people where All of the following conditions match” and add the following two conditions: Attribute email_verified is equal to true Attribute email_verification_result does not contain undeliverable Create a Campaign in Customer.io Now that we have our API key and our Segments, we can proceed with our Campaign creation. Head over to Campaigns and click on Create Campaign. Click Untitled Campaign to add a name and description. Set up a trigger A trigger determines who enters your campaign. To set one up, click Choose trigger then select Segment change. In this example, the campaign should trigger when people enter the segment you made in a previous step. Set up a webhook Drag a Send and receive data action from the Build menu. You can show/hide the menu by clicking + Build at the bottom of your workflow. Click Add Request. Configure a request to Kickbox’s API. You can learn more about their API here. (1) Select the GET method (2) Add the API URL: https://api.kickbox.com/v2/verify?email={{customer.email | url_encode}}&apikey=[your_api_key]  (3) Use the url_encode filter We use this Liquid filter to convert URL-unsafe characters in a string into percent-encoded characters. Click Send test… (1) to send a test request to the API. If it’s successful, you’ll see a 200 OK status and the response will have a JSON object. Now that we have a functional webhook, we can easily save elements from the Response object as attributes for the person. Start by clicking on the Response tab (1) and then on Set up an attribute (2). Add one or more attributes. In our example, we’ll add 3 attributes: email_verified that we’ll set to true. This lets us know which people had their email verified email_verification_result that we’ll set to {{response.result}}-{{response.reason}}. This combines the result of the verification request (which can be deliverable, risky, undeliverable, or unknown) and the reason behind this result. Learn more about these values. email_verification_disposable that we’ll set to {{response.disposable}}. This can either be true or false. If true, the email address is disposable and shouldn’t be emailed.  You can leverage additional data from the request’s response We’re limiting to these 3 attributes in our example but you could leverage additional data from the request’s response such as if the email address is from a free service, associated with a role, or if it accepts all incoming messages among other things. Now, we can test again and we’ll have a preview of the attributes that will be created. Save and update action settings Back on your workflow, click the webhook action again and click Settings. Change the sending behavior to Send Automatically and toggle on Allow conversion from this webhook. Define the Goal Last, but not least, we can define a Goal such that people convert when they enter the second segment we created earlier. We’ll also set the Exit condition as “People don’t exit early, they will move through the entire workflow.” Review and start your campaign If you have any outstanding items, click Review items in the top right and complete the steps. Otherswise, click Start Campaign to give your campaign a final review. If you’d like to only verify new people, select the Future additions only option. If you’d like to verify everyone, select the Current people and future addtions option. Click Start Campaign when you’re ready to activate your workflow! Now you can prevent your workspace from sending emails to undeliverable email addresses by proactively and automatically verifying them. --- ## Use Your Own SMTP Server URL: https://docs.customer.io/journeys/use-your-smtp-server/ By default, Customer.io takes care of your email delivery. However, you can send mail through any other SMTP server if you'd rather use your own service. Upsides to doing this You’re in full control. You can send as many emails as you want: emails sent through your custom SMTP server don’t count against your billing plan’s allotted email count. We’ll still handle your open and click tracking. We’ll keep a full copy of your sent emails associated with the recipient. Downsides to doing this Customer.io won’t have a feedback loop for data from your email service provider. Data about delivered, bounced and spammed emails won’t be visible in Customer.io. We have integrations providing delivered, spam and bounce data with Mailgun, Mailjet, Mandrill, Postmark, Sendgrid and Sparkpost Customer.io may be limited in the support we can provide you when deliverability errors occur. For instance, if your email recipients are receiving the error unauthenticated sender, you’ll likely need to work with your SMTP provider to solve the issue, not Customer.io. (Optional) Allowlist our IP addresses If your SMTP server uses an IP allowlist, letting only specific IP addresses connect to your server, you’ll need to add Customer.io IP addresses to your allowlist before you add your custom SMTP server to Customer.io. US RegionEU 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 136.111.237.157 34.10.127.179  This list is subject to change As we grow and scale our infrastructure, the list of IP addresses may change. We will communicate changes to these lists to current Custom SMTP users at least 7 days before the change goes into effect. You can retrieve an up-to-date list of IPs at any time through our /info/ip_addresses endpoint. Configure a custom SMTP server Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Other SMTP and click Continue to set up. Fill out the form with your server address, port, and credentials. You cannot use port 25. Click Finish set up. If you haven’t added a Sending Domain yet, you’ll need to do that before you can send email from Customer.io. When using custom SMTP, you do not need to authenticate your domain in Customer.io. However, you should check your custom SMTP provider’s documentation to see if you still need to add DNS records (such as SPF and DKIM) to your domain to use their services successfully.  Branded link tracking with custom SMTP If you want to use branded custom link tracking in Customer.io (using your domain instead of “customeriomail.com” when generating tracked links), you must verify the domain and add the CNAME record shown in the Domain Settings section of your workspace. The CNAME record will not validate your domain for branded link tracking if your domain has a HSTS policy, but does not currently have SSL coverage. Please see our HTTPS Link Tracking documentation for more information on getting this set up. Additional Notes: Due to a limitation with our cloud provider, you cannot send SMTP messages using port 25. Major SMTP providers normally allow delivery using port 465, 587 or 2525 (like in our screenshot above). If you need alternatives, consult your delivery provider’s documentation for ports that work with their service. Regardless of port used, we always initiate a TLS connection (via the STARTTLS command, when supported by your provider), to ensure the security of your outbound messages. You can add additional SMTP servers to segment deliveries by campaign type. For details check out our documentation on configuring multiple SMTP accounts. If you have any questions about setting this up, get in touch! --- ## Using Multiple SMTP servers URL: https://docs.customer.io/journeys/multiple-smtp-servers/ You can configure multiple custom SMTP servers to have greater control over your deliverability. If your SMTP provider requires you to use separate servers for bulk and transactional emails, you can configure multiple servers in your custom SMTP settings. Getting started First, you need to configure your primary custom SMTP server. By default, all your emails will send from your first SMTP server, but you can change this as you add new servers. You can configure up to six servers. Customizing campaign delivery To customize the delivery settings by campaign type, you need to add your additional servers in the same manner as you added your first custom SMTP server. Once this is done, your new servers default to sending emails for None. This means the new servers will not send any Customer.io generated emails. To change that, set them to one of the following options from the drop-down: Segment Triggered: Sends emails for your Segment-Triggered Campaigns Event Triggered: Sends emails for your Event-Triggered Campaigns Newsletters: Sends emails for your Newsletter Broadcasts Triggered Broadcasts: Sends emails for your API Triggered Broadcasts Transactional Messages: Sends emails for Transactional Messages It’s mandatory for at least one of your custom SMTP servers to stay set on All Emails. This is the server Customer.io will fall back to for any campaign types that don’t have a specific server specified. If you have any questions about configuring multiple SMTP servers that weren’t covered here, get in touch with us! --- ## Use your Mailgun Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-mailgun/ You can use your Mailgun account to send email through Customer.io with no loss in functionality. You can use your Mailgun account to send email through Customer.io with no loss in functionality. To take advantage of Mailgun, you need to do two things: Point Mailgun’s webhooks at Customer.io. Configure Mailgun as a custom SMTP server in Customer.io. Point Mailgun’s webhooks at Customer.io Go to the Mailgun webhooks page. Enter the webhook URL that corresponds to your region for the events you want to forward to Customer.io. US: https://track.customer.io/mailgun/events EU: https://track-eu.customer.io/mailgun/events  Do not send Clicks or Opens to Customer.io! Customer.io already tracks this information. If you send these webhooks, we’ll record each event twice (once natively in Customer.io and again via Mailgun webhook). If you want to track opens and clicks via Mailgun and not Customer.io, you can change tracking settings on an email by email basis. Configure Customer.io custom SMTP settings Before you begin, you need your Mailgun SMTP credentials. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Mailgun config settings Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Mailgun and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your Mailgun credentials. Click Finish set up. Your account is now set up to send email through Mailgun. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your Mailgun server types. --- ## Use your Mailjet Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-mailjet/ You can use your Mailjet account to send email through Customer.io with no loss in functionality. You can use your Mailjet account to send email through Customer.io with no loss in functionality. To take advantage of Mailjet, you need to do two things: Point Mailjet’s webhooks at Customer.io. Configure Mailjet as a custom SMTP server in Customer.io. Point Mailjet’s webhooks at Customer.io Go to the Mailjet event triggers page. Add the webhook URL corresponding to your region for all events except Open and Click. US: https://track.customer.io/mailjet/events EU: https://track-eu.customer.io/mailjet/events Example webhook config  Do not send Click or Open events to Customer.io! Customer.io already tracks this information. If you send these webhooks, we’ll record each event twice (once natively in Customer.io and again via Mailjet webhook). If you want to track opens and clicks via Mailjet and not Customer.io, you can change tracking settings on an email by email basis. Configure custom SMTP settings Before you begin, you need your Mailjet SMTP credentials. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Mailjet and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your Mailjet credentials. Click Finish set up. Your account is now set up to send email through Mailjet. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your Mailjet server types. --- ## Use your Mandrill Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-mandrill/ You can use your Mailchimp Transactional (Mandrill) account to send email through Customer.io with no loss in functionality.  Mandrill is designed for transactional messages only Mailchimp’s acceptable use policy prohibits the sending of bulk emails—emails directed to multiple individuals with the same content—through Mandrill. You can use your Mandrill account to send email through Customer.io with no loss in functionality. To take advantage of Mandrill, you need to do two things: Point Mandrill’s webhooks at Customer.io. Configure Mandrill as a custom SMTP server in Customer.io. Point Mandrill’s webhooks at Customer.io Go to the Mandrill webhooks page and click Add a Webhook. Check the following events: Message Is Sent Message Is Bounced Message Is Marked As Spam Message Is Rejected Message Is Delayed Message Is Soft-Bounced In the Post To URL field, enter the webhook URL corresponding to your region. US: https://track.customer.io/mandrill/events EU: https://track-eu.customer.io/mandrill/events Click Create Webhook. If you want to track opens and clicks via Mandrill and not Customer.io, you can change tracking settings on an email by email basis. Disable CSS inlining If you’re using the drag-and-drop email editor or our own CSS pre-processing, we recommend disabling Mandrill’s CSS inlining feature so it doesn’t interfere with ours. Visit the Mandrill Sending Defaults page and make sure the Inline CSS Styles In HTML Emails option is disabled. You can also disable Mandrill CSS inlining on a per-message basis by adding a custom X-MC-InlineCSS header to your email. Configure custom SMTP settings Before you begin, you need your Mandrill (Mailchimp Transactional) SMTP credentials. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Mandrill and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your Mandrill credentials. Click Finish set up. Your account is now set up to send email through Mandrill. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your Mandrill server types. Ensure that your unsubscribe link works If you use Customer.io unsubscribe links, then everything will continue to work. If you want to use another provider’s unsubscribes, you need to add their unsubscribe link to your emails or layouts. To make sure that it processes, you need to add class="untracked" to the link, like this: <a href="*|UNSUB:http://mywebsite.com/unsub|*" class="untracked">Unsubscribe.</a> If you use Mandrill unsubscribe links, unsubscribes will not be tracked in Customer.io. They will only be tracked in Mandrill. --- ## Use your Postmark Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-postmark/ You can use your Postmark account to send email through Customer.io with no loss in functionality. You can use your Postmark account to send email through Customer.io with no loss in functionality. To take advantage of Postmark, you need to do two things: Point Postmark’s webhooks at Customer.io. Configure Postmark as a custom SMTP server in Customer.io. Point Postmark’s webhooks at Customer.io Log into Postmark. Select your server, the Message Stream you’ll be sending emails from, and then the Webhooks tab. Click Add webhook and enter the webhook URL that corresponds to your region in the Webhook URL field. US: https://track.customer.io/postmark/events EU: https://track-eu.customer.io/postmark/events Select the Delivery, Bounce, Spam, and Subscription Change checkboxes.  Do not send Link Click or Open webhooks to Customer.io! Customer.io already tracks this information. If you send these webhooks, we’ll record each event twice (once natively in Customer.io and again via Postmark webhook). If you want to track opens and clicks via Postmark and not Customer.io, you can change tracking settings on an email by email basis. Configure Customer.io custom SMTP settings Obtaining SMTP credentials from Postmark Before you begin, you’ll need to generate a Postmark SMTP Token. You’ll find it on the Settings tab of the Postmark Message Stream that you want to send from. Go to the Settings tab of your Postmark Message Stream (the same stream that you setup webhooks for in the previous step). Click on Generate an SMTP Token Note the Access Key as you’ll use it as your username and the Secret Key as your password in Customer.io Learn more about Postmark SMTP settings. Configuring Customer.io to use the credentials obtained Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Postmark and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter the Access Key and Secret Key created in the previous step as your Postmark credentials. Click Finish set up. Your account is now set up to send email through Postmark. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams You can configure multiple custom SMTP servers and assign one to each of your Postmark server types accordingly.  Postmark’s Broadcast Stream will include their Unsubscribe link by default If you do configure your Customer.io workspace to send bulk emails through Postmark’s Broadcast Stream, an Unsubscribe link is required. Postmark inserts their own link into your messages by default, however you can opt-out of this by contacting Postmark Support. You can request removal of their link by informing them that you’re using Customer.io’s built-in Unsubscribe link and management, and they’ll be happy to assist from there. --- ## Use your SendGrid Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-sendgrid/ You can use your SendGrid account to send email through Customer.io with no loss in functionality. You can use your SendGrid account to send email through Customer.io with no loss in functionality. To take advantage of SendGrid, you need to do two things: Point SendGrid’s webhooks at Customer.io. Configure SendGrid as a custom SMTP server in Customer.io. Point SendGrid’s event API at Customer.io Now you’ll need to adjust some settings to make sure your SendGrid account (or subuser) sends Customer.io the right data. Go to Sendgrid. If you have a subuser for sends through Customer.io, click your account name at the top left, click Switch User, and select the subuser you created for Customer.io. If you want to create a subuser specifically for Customer.io sends, see the section below. Go to Settings > Mail Settings. Under Webhook Settings, click Event Webhooks. Click Create new webhook. Toggle Enable endpoint on. Enter the Customer.io URL corresponding to your account region in the Post URL field. US: https://track.customer.io/sendgrid/events EU: https://track-eu.customer.io/sendgrid/events Check the Dropped, Delivered, Bounced, and Spam Reports actions. Do not select Opened and Clicked to avoid duplicating data; Customer.io tracks these events automatically. (Optional) Go to Settings > Sender Authentication and click Authenticate Your Domain to set up Domain Keys (DKIM). (Optional) Create a subuser If you send some (but not all) of the mail in your SendGrid account through Customer.io, you’ll want to create a subuser first. With a subuser, you can track all your Customer.io email activity without blending in data from your other SendGrid activity and use separate IPs for separates services. Not all SendGrid plans support the ability to create subusers, so check your plan before you try to create one. If your plan offers subusers, you can find the option to create new subusers on the Settings > Subuser Management page. A SendGrid subuser has a username, email address and password, letting you (or someone on your team) log in to SendGrid as that subuser. You also need an API key associated with this subuser to add SendGrid to Customer.io in later steps. If you don’t set up a subuser and use your primary SendGrid account for Customer.io, all your email sent via SendGrid will show up under one account. Configure custom SMTP settings Before you begin, you need your SendGrid SMTP credentials. You’ll find them under Settings; if you have a subuser, you’ll find them under Settings > Subuser Management. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select SendGrid and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your SendGrid credentials. Click Finish set up. Your account is now set up to send email through SendGrid. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your SendGrid server types. --- ## Use your Sparkpost Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-sparkpost/ You can use your Sparkpost account to send email through Customer.io with no loss in functionality. You can use your Sparkpost account to send email through Customer.io with no loss in functionality. To take advantage of Sparkpost, you need to do two things: Point Sparkpost’s webhooks at Customer.io. Configure Sparkpost as a custom SMTP server in Customer.io. Point Sparkpost’s webhooks at Customer.io Go to Sparkpost’s webhooks page. Enter the webhook URL corresponding to your Customer.io account region for every domain you want to send email from in Customer.io. US: https://track.customer.io/sparkpost/events EU: https://track-eu.customer.io/sparkpost/events Click Select individual events and select the events you want to report to Customer.io. Do not select Open or Click events.  Do not send Click or Open webhooks to Customer.io Customer.io already tracks this information. If you send these webhooks, we’ll record each event twice (once natively in Customer.io and again via Sparkpost webhook). If you want to track opens and clicks via Sparkpost only, you can change tracking settings on an email by email basis. Create a new SMTP delivery key Before you can configure Sparkpost as a Custom SMTP server in Customer.io, you need to generate a Sparkpost API token. Go to the Sparkpost API keys page. Create a new key for use in Customer.io. Save or copy the generated key as you will need it for configuring your SMTP in Customer.io, and can only access it once! Configure custom SMTP settings Before you begin, you need your Sparkpost API key. If you don’t have your API key, you can generate a new key. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Sparkpost and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your Sparkpost credentials. Click Finish set up. Your account is now set up to send email through Sparkpost. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your Sparkpost server types. --- ## Use your Oracle Dyn Account URL: https://docs.customer.io/journeys/triggered-lifecycle-email-with-oracle-dyn/ You can use your Oracle Dyn account to send email through Customer.io with no loss in functionality. You can use your Oracle Dyn account to send email through Customer.io with no loss in functionality. To take advantage of Oracle Dyn, you need to do two things: Point Oracle Dyn’s webhooks at Customer.io. Configure Oracle Dyn as a custom SMTP server in Customer.io. Point Oracle Dyn’s webhooks at Customer.io Go to the Oracle Dyn Integrations page. Add the following webhook URLs for every domain you want to send mail from in Customer.io. If your account is in the EU region, substitute track-eu.customer.io in the URLs below. Bounce Postback URL: https://track.customer.io/oracledyn/events?event=bounced&dyn_cio_email_id=@X-CIO-Email-ID&email=@email&bounce_type=@bouncetype&reason=@bouncerule Spam Complaint Postback URL: https://track.customer.io/oracledyn/events?event=spammed&dyn_cio_email_id=@X-CIO-Email-ID&email=@email In the Custom X-Headers section at the bottom of the page, add X-CIO-Email-ID. Create your SMTP API Credentials If you don’t have your credentials handy, find your account on the Oracle Dyn Users Page page, select your user account, and update your SMTP password. This integration uses port 587. If you need to use a different port, you should use a custom SMTP integration. Save or copy your new password. You will need it to configure Oracle Dyn as your SMTP server in Customer.io, and you can only access it once! Configure custom SMTP settings Before you begin, you need your SMTP password. If you don’t have your SMTP password, you can generate a new one. Go to Settings > Workspace Settings. Click Email, click Custom SMTP Settings, and then click Add Custom SMTP Server. Select Oracle Dyn and click Continue to set up. If you haven’t set up webhooks yet, copy the URL on the Set up webhooks tab and then set up webhooks to report message metrics in Customer.io. Click Add Credentials and enter your Oracle Dyn credentials. Click Finish set up. Your account is now set up to send email through Oracle Dyn. If you have any questions or feedback, contact us and we’ll be happy to help!  Configure multiple servers to use both transactional and broadcast streams If you have a Premium Customer.io plan, you can configure multiple custom SMTP servers and assign one to each of your Oracle Dyn server types. --- ## Choose the right email editor URL: https://docs.customer.io/journeys/email-editors/ We offer several editors for email creation. Check out our newest editor, Design Studio! If that doesn't suit your needs, check out our older editors below.  Check out Design Studio! Design Studio is our newest, most flexible email editor. Use components to create a block-based email from scratch, and set global styles to create a consistent brand across your messages made in Design Studio. No longer do you have to decide between a visual or code-based editor; you can use both! Choose your editor While editing a campaign, broadcast, or transactional message, you can create and edit your emails. When you first click into an email block, you’ll choose to either start from scratch or start from an existing email. Our newest editor: Design Studio In Design Studio, you can: Build emails with our visual, block-based editor. You can also modify the HTML/CSS as you see fit by switching to the code editor! Build emails using reusable blocks. We offer out-of-the-box blocks called standard componentsA pre-built block that helps you build beautiful, engaging messages as quickly as possible in Design Studio.. You can also create your own reusable blocks called custom componentsA custom block of code with content and properties you can reuse across messages made in Design Studio.. Assign global styles that span your emails and in-app messages. Collaborate with teammates by sending test messages, requesting feedback, and more. Check out Welcome to Design Studio to get started! Our classic editors Drag-and-drop Our drag-and-drop editor allows you to quickly build responsive emails without coding. It looks like this: You can quickly add blocks of content, columns, and easily drop in items like images, buttons, social links, and set global styles. When clicking your email to edit it, you can see that it was built using the drag-and-drop editor: To learn more about the drag-and-drop editor, check out these resources: Getting started with drag-and-drop: the basics of how to use columns, images, customize your headers and navigation, and so on. Drag-and-drop editor FAQ: some of the most common questions we’ve received about this editor and upcoming features. Troubleshooting your drag-and-drop emails Rich text This editor is great if you need a little more control over the code compared to the drag-and-drop editor, but don’t need a highly stylized message. You land on the WYSIWYG editor, but you can click HTML to edit code. You can use layouts to structure your emails and create a re-usable header and footer. You can switch to our code-only editor, but once you confirm this action and save the email, you cannot revert the message back to the rich text editor. You can identify emails using the rich text editor in the workflow by this icon: To learn more about the rich text editor, check out these resources: Email layouts overview Customizing your layouts Code The code editor is a great option if you want complete, granular control over your email. Like the rich text editor, you can use layouts to create a global, reusable header and footer. We’ve partnered with Parcel to help you code better emails. Our code editor comes complete with syntax autocomplete, responsive previews, and an Inspect Element mode that helps you find code when you click elements in your preview. You can switch between tabs for writing HTML exclusively and AMP. You can identify emails created using the code editor in the workflow with the icon. To learn more about the code editor, check out these resources: Our email code editor, powered by Parcel How our Layouts work with this editor Customizing your Layouts Disabling CSS pre-processing Create an email from Design Studio For Design Studio, our newest editor, you can create email or copy one from: The Design Studio dashboard Within a campaign, broadcast, or transactional message Create an email from our classic editors From a campaign or API-triggered broadcast: Drag an Email block into your workflow and click Add Content. If the first option you see is Use a Design Studio Email, then click Go back to classic in the top right to access the older editors. Otherwise, learn more about creating emails with Design Studio! Decide how you want to create your email: from scratch or from an existing email. If you want to create from scratch, choose your editor. If you want to create from an existing email, template or layout, then choose the tab that reflects the editor it was made in. In the drag-and-drop tab, choose an existing email from the top or a template at the bottom. In the rich text or code tabs, choose an existing email from the top or a layout at the bottom. Edit the message as you see fit. Change editors and reset message content Sometimes after you create an email in a campaign or broadcast, you want to change the editor you’re using or start over. For instance, if you’re working in our rich text editor, you may realize you want more flexibility with styling and want to switch to Design Studio instead. You can switch to a different editor from the email’s action menu. Keep in mind, this does not migrate your content. When you switch editors and reset your content, you start from scratch. Switch from a classic editor to Design Studio Open your email, and click Actions > Reset message content. Select an email made with any of the editors or start from scratch. If you see the classic editors and want to use Design Studio, click Try it out in the banner. If you see options for Design Studio and want to use a classic editor, click Go back to classic in the banner. When you switch email editors, your content is reset; you start from scratch in the new editor. If you want to return to your existing email, click Cancel to restore your content. Switch from Design Studio to a classic editor Open your email, and click Publish > View published version. Click Actions > Reset message content. Select an email made with any of the editors or start from scratch. If you see the classic editors and want to use Design Studio, click Try it out in the banner. If you see options for Design Studio and want to use a classic editor, click Go back to classic in the banner. When you switch email editors, your content is reset; you start from scratch in the new editor. If you want to return to your existing email, click Cancel to restore your content. --- ## Drag-and-Drop Emails: The Basics URL: https://docs.customer.io/journeys/drag-and-drop/ Drag-and-drop emails consist of content, rows, and settings. **Content** blocks are discrete pieces of email content, images, text, etc. **Rows** provide the structure of your email—they determine how many pieces of content you can fit across the width of your message.  Check out Design Studio! Design Studio is our newest, most flexible email editor. Use components to create a block-based email from scratch, and set global styles to create a consistent brand across your messages made in Design Studio. No longer do you have to decide between a visual or code-based editor; you can use both! Settings The Settings tab contains general settings for your whole message: background color, default typeface, link color, etc. Content blocks in your email inherit these settings. Rows and content The email editor consists of Rows and Content. In general, you’ll add rows to your email, providing a basic structure for your message, and then add content - the images, text, and information you want to send people - to your rows. The Rows tab provides a way to structure your email before you add content. Each row determines the number and size of content blocks you can fit across your message. You can add multiple rows, with different numbers of content blocks, to add visual appeal to your message. The Content tab contains the different kinds of content that you can add to a row—text, title, images, etc. When you click a content block in your email, you’ll see settings specific to the type of content in the row. For example, if you wanted to add a two-column block with text and an image, you would: Click the Rows tab and drag a two-column row into your email. Go to the Content tab to drag text and image blocks into the row. Click your content blocks to add your text and image respectively. Save rows In the drag-and-drop editor, you can save individual rows and reuse them in other drag-and-drop messages—like headers or footers that you want to reuse across all of your messages. If you’re familiar with our rich text and code editors, you know you can create layouts to add global headers and footers. Saved rows are the equivalent in the drag-and-drop editor.  Updating a saved row does not update it everywhere You can make changes to a saved row after you drag it into your email, and then save it again. However, your changes to a saved row are only reflected when you use the row again; they do not affect messages where you’ve already used the row. Select a row and click . Set a Name and Category for your row, like “header” or “footer”. These help you select the row when you want to reuse it. Click Save row. Find a saved row Within Rows, we show 30 rows at a time. You can filter rows by category or search for a row by name. If you have more than 30 rows in a category, you may need to search by keyword to find the row you want to use. Use a saved row If you saved rows of content in the drag and drop editor, you can reuse those rows in other emails. Rows are organized by category. With nothing selected in the editor: Click Rows and select the category containing the row you want to use. Drag the row into your layout. Update a saved row You can update a saved row after you drag it into your message. However, your changes to a saved row are only reflected when you use the row again; they do not affect messages where you’ve already used the row. If you want to update a row in other campaigns and messages, you need to go to the campaign or message you want to update, re-insert the row, and then save the message. To modify a row that you have already saved: Click Rows and select the category containing the row you want to use. Drag the row into your layout. Make changes to the row, and then click . You can either Update the row or save it as a new row. Manage and delete rows In the drag and drop editor, go to Rows, and select Manage saved rows from the drop-down menu to see a list of your saved rows. Click to edit the name and category assigned to a row. Click to delete a row. Deleting a row only prevents you from using the row in the future, it does not remove the row from any messages currently using it. Row settings and responsiveness By default, content blocks stack on mobile devices, going from left-to-right order to top-to-bottom. You can click any row in your message and determine how it behaves in mobile contexts. Do you want to disable collapsing behavior, or hide content on smaller devices? Click each row to reveal responsive settings. By default, content blocks stack from left to right: Standard width Mobile width If you select the row and toggle on Do not stack on mobile, your email will resize without stacking content. You can also set the width of cells to customize the layout of a row. Set background colors By default, the backgrounds of rows and content blocks are transparent. You can change the background at the row or content level. The background of a row stretches to the edge of your audience’s email browser. The content background is contained by the cell itself. Click the row you want to set a background color for. Set the Row background color, the Content background color, or both! Personalize messages with user data You can leverage data associated with your audience to personalize messages for each recipient! When working with text content, click Merge Tags. We’ll list the contexts and data available to your message. Or, if you know exactly which attribute you want to add to your message, you can type it manually. For example, let’s add a first name to a message. Personalize messages with liquid conditions You can use 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}}. comparison operators or logical expressions (statements using &, <, >, etc) to personalize messages too. Check out what version of liquid your messages use to learn which liquid tags and filters are available to you. Click More > Add Liquid in the content menu when working with text.  Do not type complex expressions into a text block Logical and comparison operators will not work in the drag-and-drop editor if you type them directly into the text area. Use the Add Liquid option so that you don’t inadvertantly expose raw liquid statements in your message. Show or hide content using liquid You can use liquid to show or hide entire rows in your email. Select a row in your email and click Add Condition. Enter a Name and Description for the condition. Provide the liquid condition determining who will see the row. For example, this row displays only if a person in your audience has a “state” attribute with a value of “paying”. Customizing headers To maximize space to compose your message, you can collapse email headers. You can see a preview of the From, To, and Subject lines when you collapse headers. For the subject line, you can generate AI-powered suggestions based on your email content and business context. Click next to the subject field to get multiple options that match your brand’s tone and audience. For best results, generate subject lines after you finish your email content. To add a new header, like Reply To or BCC, use the quick-add options. To add a custom header, click Add custom and specify the header you want to add. You can add up to 10 headers. To add preheader text, expand the headers and click Add preheader text. Add images You can add images to your messages using the image content block. When you add an image block to your email, you can upload new images or insert ones you’ve already uploaded. You can also supply a link to an externally hosted image. Change image sizes To customize an image’s size, click the image block and toggle off Auto width. Adjust the scale to your desired percentage. You can also set Full width on mobile. Set a dynamic url for an image Dynamic images render based on data unique to your recipient. They’re a way of personalizing images based on liquid attributes for your customers, events, or objects. You can upload images to our asset library or host them yourself; just be sure to follow best practices for images. When you’re ready, save the image URL as a customer attribute or event property so your liquid (detailed below) renders the personalized image in your campaign. To include an image based on a dynamic url: Drag an Image content block onto your canvas. Browse for an image in your assets library or upload a new one to use for testing. Under Content Properties, toggle Dynamic image on. Enter the appropriate liquid into the Dynamic Url field. In the example below, people have an attribute called profile_image. If this attribute exists for a recipient, the image will display in the email; otherwise, you must add if/else liquid statements to render a fallback. Start by uploading your fallback image to your asset library. Then copy the asset’s link and paste into the else statement. For example: {% if customer.profile_image != blank %}{{customer.profile_image}}{% else %}img-url{% endif %} If the fallback image is stored in your asset library, click the link icon to copy the URL and paste into your liquid condition.  Make sure your fallback image is the same size as your dynamic images The size of your fallback image determines the size of your dynamic images. If a dynamic image is too big or too small, check your fallback image. Save your email. Preview the message to see the image change depending on the person you’ve selected. Set background images To add a background image to your row: Click a row. Click Row Background Image. Enter the Url of your image or click Change image to use an image that you’ve already uploaded. Select the display parameters for the image—whether to stretch, repeat, or center it. Add a navigation menu to your email You can add a menu to the top of your email, simulating something your audience might see on your website. Add a four-column structure. Drag Text content into each, then add your individual text and links into the columns themselves. Adjust the row background, if you’d like the background color to be full-width. Scroll to the bottom of your row settings, and add borders to the right of content blocks 1, 2, and 3. (Click More Options to expose border settings for the sides of each block). Unsubscribe and other special links By default, even when you start from scratch, we insert the default unsubscribe link for you. You can remove this link, but if you do, you must include your own unsubscribe link so people can opt out. You can also add the View in Browser link through the menu of a text block under Special links. This adds a link that lets people view your message in their browser rather than the email client.  Need to track personalized links? Add the data-cio-tag attribute to your links. This lets you track different individualized links in the same category, so you can gather useful metrics about clicks to things like personalized product recommendations, password reset links, customer dashboards, etc. The data-cio-tag takes a string representing the “group” you want to track—<a href="http://mydomain.com?token=123abc" data-cio-tag="YOUR-LINK-GROUP-NAME">CLICK HERE</a> Preview fallback If a recipient has an email client that does not support our drag-and-drop emails, they will see a plaintext fallback. You can preview the fallback from within the drag-and-drop editor. Select Actions > Preview plaintext in the top right. Save your emails We don’t autosave your email as you work on it, because we want to enable you to experiment with your emails, but still be able to discard those changes if you don’t like them! While we don’t autosave your message, we do warn you if you try to leave the editor without saving changes. When you’re ready to save your message, click Save. When you save, we automatically check for errors in your message and warn you about the things you need to fix. Remember that… You cannot import HTML into the drag-and-drop editor. You can, however, add HTML blocks to your message. After you choose to use the drag-and-drop editor for an email, you cannot change back to the rich text or code editors. If you send email using custom SMTP providers, make sure to disable their CSS inlining (if available). The drag-and-drop editor generates markup that is already optimized and doesn’t need further processing. Have feedback? Yes, please! Found a bug? Have a feature request? Something doesn’t work as you expected? Is some functionality missing? Or are you loving the new changes and want to make sure we keep something? Let us know! --- ## Drag-and-Drop Editor FAQ URL: https://docs.customer.io/journeys/drag-and-drop-faq/ This page provides answers to common questions and can help you troubleshoot problems when drafting emails with the drag-and-drop editor. We’ve built and tested this editor as much as we can to pick out scenarios you may want help with, but this definitely isn’t an exhaustive list! If you have a question that isn’t addressed here, please reach out and we’ll do our best to help. Can I edit the HTML of drag-and-drop emails? This editor is built specifically to assemble emails quickly without knowledge of HTML or CSS. You can add blocks of HTML to your email, but there is no way to view all body HTML like there is with the code editor. If you need to control the code of your emails, you should use our code editor. What’s the “{% unsubscribe %}” in my email? This is our default unsubscribe link and clicks are not tracked. You can remove it, but if you do, you have to include your own! If you delete it by accident and need to re-add it, you can copy and paste this into your email wherever you want your untracked unsubscribe link to appear: {% unsubscribe %} Can I disable link tracking in the drag-and-drop editor? You can disable link tracking for links in the drag-and-drop editor by adding the untracked class to links (text, buttons, etc). To disable link tracking on a text link, type untracked in the Class field. To disable link tracking on a button or image with a Url action, click Add New Attribute > Class and type untracked in the field. Which email clients does this editor support? This editor is an integration of Beefree; here is a full list of supported email clients. If a recipient has an email client that does not support our drag-and-drop emails, they will see a plaintext fallback. Does this work with my existing layouts? Instead of layouts, you can use saved rows in the drag and drop editor. The new drag-and-drop editor does not use layouts, but layouts will continue to work just like they always have with our rich text and code editors. Can I save a design for future use? You can use any previous email you’ve designed in the drag-and-drop editor as a starting point for future emails. You can also use saved rows in the drag and drop editor to reuse content across emails. Can I use my own fonts? Outside of the fonts in the editor already, we don’t support adding or using any others. This is because fonts and email clients don’t get along too well, so the ones in our menu are ones we know will behave most consistently in your messages! If you really need a particular font, you can use it in an image, or import it using an HTML block. Example: You would start the message with an HTML block to include this style tag: <style> @import url('https://fonts.googleapis.com/css2?family=Shadows+Into+Light&display=swap'); p { font-family: 'Shadows Into Light'; } </style> Once in place, you can create separate text blocks, and use the imported font. I need a layout that the drag-and-drop editor doesn’t allow! If you’d like a combination of structure and content that the drag-and-drop editor doesn’t support, we suggest using the HTML content block to customize it, and then testing email client support to make sure it looks exactly how you expect! How do I preview my customer and event data while composing a message? If you just want to drop in a single attribute, use “Merge Tags” in the block menu: If you want to write something more complex– an if statement, for example, or anything involving logical or comparison operators (&, >, or <), use our “Add Liquid” option in the text dropdown: Then, if you’d like to preview your email with specific customer and event data, click Preview in the top left of the editor! Head to our liquid documentation to see what’s available to you. Have feedback? Yes, please! Found a bug? Have a feature request? Something doesn’t work as you expected? Is some functionality missing? Let us know! --- ## Troubleshooting Your Drag-and-Drop Emails URL: https://docs.customer.io/journeys/troubleshooting-email-design/ We're continuously checking your message for errors, in your links, headers, and any liquid code you write. Some errors might prevent your message from sending, while others might be completely innocuous! Here's a guide for how to find and see the errors in our drag-and-drop editor, as well as how to fix them. Errors in email content We’ll let you know of any errors in your email content in the buttons at the top of the editor. If there are none, the buttons look like this. If there are problems, though, the button will change to “Review Errors” and animate to get your attention! Click Review Errors to see a pop-up window describing the error(s): Learn more about errors you could encounter while composing an email. Email not updating? If you’ve recently written some liquid and your email does not update when you preview it, check to see if you’ve used any logical or comparison operators such as &, >, or <. These don’t work when typed directly into the editor, and should be added via our Add Liquid option: Errors with links Click Review Links in the top right to see which links have issues and why: Errors in email headers If you have an error in one of your email headers, you’ll see a red exclamation point in the header: Then, when you open it, you’ll be able to see exactly which fields the error pertains to. In this case, it’s the “To” field: If you click Review Errors, you’ll see exactly what the error is. In this case, it’s improper liquid syntax: Fixing errors Unfortunately, there are quite a few ways for an email to go wrong, particularly when a lot of dynamic data is used. We’ve done our best to exhaustively document the different error types for you here, to help you address them each in kind. Have questions or feedback? Yes, please! We want to improve the drag-and-drop editor’s error detection and help you be more aware of any issues with your messages, as well as how to fix those issues: Is a specific error confusing? Would you prefer it if we showed you errors in a different way? Do you expect them to see them at a different time or in a different area of the editor? What kind of errors do you find yourself fixing most often? Do they frustrate you? Does something work exactly as you do expect? Tell us about that, too! Don’t hesitate to get in touch! --- ## Email code editor URL: https://docs.customer.io/journeys/email-code-editor/ If you're comfortable writing your own HTML, our code editor can help you draft better emails with syntax autocomplete, responsive previews, and developer tools to help you debug your message.  Check out Design Studio! Design Studio is our newest, most flexible email editor. Use components to create a block-based email from scratch, and set global styles to create a consistent brand across your messages made in Design Studio. No longer do you have to decide between a visual or code-based editor; you can use both! How it works When you create an email, you can pick the Code option to write your own HTML. Our code editor is powered by Parcel and contains features that can help you build messages that support your entire audience. There’s a lot going on in the code editor, so you can show and hide the pieces of the editor as you need them. No matter what you hide, you’ll always see your code and the corresponding preview. Sample Data: shows a representative person and their data, so you know which attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and event data are available to your message in 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}}.. The Envelope contains your message’s To, From, Subject, and other fields determining how you want to handle your message. The Preview shows your rendered HTML or AMP components. Developer tools are features that can help you proof and test your email’s links, images, and accessibility. The Editor is where you write your email. It contains a number of helpful features, like code auto-complete and you can hover over tags to see Can I Email’s assessment of email client support for different elements. You can switch between tabs for writing HTML exclusively and AMP. Controlling the preview By default, the preview shows what your message will look like given the space available in your browser. You can use the controls in the upper-right of the preview to see what your message will look like in other sizes, color schemes, or for people with visual impairments. Change email size and test responsiveness Click to preview your message at different heights, widths, and make sure that your email is responsive on devices of all sizes. Toggle light or dark mode Click to test your message in light mode, dark mode, and with common visual impairments to make sure that it will work for everybody in your audience. When you test dark mode, you’ll see a few options. Light mode and Dark mode affect prefers-color-scheme: dark/light settings. Forced dark mode Forced dark mode emulates color inversion of certain email clients. Some email clients—including Outlook and Gmail, will find areas of your email with dark text and light background and flip them to match the user’s Dark Mode preference. We don’t have full control over these cases, so it’s always a good idea to test your images and colors. You have two preview settings for forced dark mode: Forced dark (Outlook) and Forced dark (Google). Outlook Forced dark (Outlook) shows what your email will look like in these locations: Outlook webmail all versions Outlook progressive web app Outlook on Mac or Android It does not represent what you’ll see here: Outlook Windows desktop App Outlook iOS app Google Forced dark (Google) shows what your email will look like across a number of email apps on Android, including Gmail, Samsung, Yahoo, AOL, K-9, and GMX. It does not represent what you’ll see here: Webmail version of Gmail (this has no dark mode) Gmail iOS app Test your message for people with visual impairments You can simulate a variety of visual impairments to help you design inclusive emails and support your entire audience. Click to enable or disable visual impairment settings. You can only apply one filter at a time. Developer tools The Developer tools panel, in the bottom-right of the editor—contains features that can help you work more efficiently and debug your messages. Inspect element mode Inspect the preview to highlight its corresponding HTML in the editor. Click to enable Inspect Element mode. Hover over any element in the preview to view its properties. Then click it to go to that section of your code. You can also enable or disable this mode with keyboard shortcuts: Mac: CMD SHIFT C Windows: CTRL SHIFT C Focus mode Focus Mode aligns the preview with the code you’re working on so you can preview changes to your message in real-time. Click to enable Focus Mode. As you navigate to different sections of your code, the preview jumps to the matching location and outlines the relevant element. You can also enable or disable this mode with keyboard shortcuts: Mac: CMD SHIFT M Windows: CTRL SHIFT M Expanded table view Tables are one of the main ways you’ll organize content in an email. Expanded table view highlights table/cell borders in the Preview to help you identify table-based layout issues. Click to toggle expanded table view. Problem checker Writing code is hard! The problem checker analyzes your code and highlights bugs or mistakes. You’ll find it in the Problems tab in the bottom right corner. Click any problem to go to the affected code. Link validation Link validation checks that links in your message work properly and go to the right pages. Click Links in the bottom right corner to open the link validator and click Validate to run a check. We’ll attempt to connect to each link in your code and tell you if the connection is secure and successful. If the button in the top-right of the checker says In Sync, the validation results match the code. If you make code changes, you can click Refresh to run the validator again. Image validation Our image validator checks that all your images are secure, optimized, and load properly. Click Images in the bottom-right to open the image validator. We’ll attempt to load the images in your code. If the button in the top-right says In Sync, then the validation results match the code. If you make code changes, you can Refresh to run the validator check again. Accessibility checker The code editor uses Deque University’s Accessibility rules so you can easily evaluate your email for accessibility issues and best practices. Go to the Accessibility tab to make sure that your message is accessible to all readers. Depending on the size of your email, the checker may take a moment to complete. The checker returns a list sorted by severity (Critical, Serious, Moderate, Mild). Click an issue to jump to the relevant line of code. Many of the issues include a link on the second line with more information about the issue. --- ## Introduction to Email Layouts URL: https://docs.customer.io/journeys/3-layouts-and-customerio/ When using our Rich Text and Code editors to design your email templates, there are two parts to emails: the content and the layout. The **layout** is everything except the body of your email. Your content, or message, fits *into* the layout. What are layouts?  Layouts are only available in our rich text or code editors. You cannot use layouts with Design Studio or our drag and drop editor; instead, start from a template. Or learn more about the reusable content features available in Design Studio (components) and the drag and drop editor (saved rows, snippets). When using our Rich Text and Code editors to design your email templates, there are two parts to emails: the content and the layout. The layout is everything except the body of your email. Your content, or message, fits into the layout. Here’s an example of a layout on its own in the Email Layouts area of Customer.io: In this case, the image header, the white wrapper, and the text at the bottom is the layout. The {{content}} tag in the middle indicates where future content will go. That content might change, but the layout will always stay the same. If you use this layout in an email, the content in the editor replaces the {{content}} tag. Here’s the above example with content: Add a layout  Layouts are only available in our rich text or code editors. You cannot use layouts with Design Studio or our drag and drop editor; instead, start from a template. Or learn more about the reusable content features available in Design Studio (components) and the drag and drop editor (saved rows, snippets). Using a layout Rich text editor When you’re composing an email in the rich text editor, click Layout & Preview: From Preview mode, click Change Layout: Choose an layout to preview it in your message. Then click Apply Layout. Code editor When you’re composing an email in the code editor, click to open the email envelope: Then click next to Layout to choose one. The selection renders in the preview on the right. Create a layout If you need a new layout, go to Content > Email Layouts. Until you start creating your own layouts, we use a default “plaintext” layout with no formatting. Click Create Layout to get going. You’ll see this: We offer a set of responsive starters for you to get started with, or you can start from scratch and paste in your own HTML if you’ve got it. Just remember that HTML and CSS work a little differently for emails than they do on the web. If you need help customizing the starters, we have a guide for that! Whatever option you choose, the one thing you must have in your layout is this: {{content}} That’s where Customer.io will insert your email’s content— the words you write— in the future when you use that layout. Plaintext layouts If you’d like a plaintext layout, all you need in the editor is {{content}} and nothing else: Best practices Here are some best practices and tips for how to create and manage layouts in Customer.io: Layouts are for design, not content: Generally, we recommend that your content go in an email’s body, rather than in the layout. This gives you much more control over what you send to your customers, and prevents anyone getting incorrect content. Try not to use Liquid in layouts: we can’t validate it the way we do when we send emails, so you might end up sending missing attributes to your customers. Build re-usable layouts, ones that you can re-use across campaigns, rather than building them on a per-campaign basis. You need an unsubscribe link; you can handle this yourself, or Customer.io can do it for you. If you want us to handle unsubscribes, just use this piece of code in your layout: <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe Text</a> --- ## Customizing Email Layouts URL: https://docs.customer.io/journeys/customizing-layout-starters/ Customize our starter layouts to quickly produce reusable email layouts that fit your brand and style.  Layouts are only available in our rich text or code editors. You cannot use layouts with Design Studio or our drag and drop editor; instead, start from a template. Or learn more about the reusable content features available in Design Studio (components) and the drag and drop editor (saved rows, snippets). Our Layout Starters are built using a framework called Foundation for Emails. This framework ensures that our starter email layouts are generally supported across clients, while still letting you to customize the look and feel of your emails. Choosing a Layout Starter Under Create content, click More > Layouts to see starter layouts. Choose the starter that works best for you, set a name for your layout, and then you can customize the HTML and CSS to fit your needs. Find your way with code comments Because email code can be overwhelming sometimes, we’ve added comments to help you find the various areas you might want to customize. You can remove the comments if you’d like. Below, we’ll show you which comments to look for to easily customize things like your logo, background, and even set columns in your layout. In HTML, comments look like this: <!-- This is an HTML comment: it doesn't affect the code! --> And CSS comments, like this: /* ...and this is a CSS comment. It doesn't affect the code either, but it helps us annotate it! */  Do not remove Foundation CSS Removing the link to the foundation CSS will break your layout! That piece of code looks like this: <link href="https://cdnjs.cloudflare.com/ajax/libs/foundation-emails/2.2.1/foundation-emails.min.css" rel="stylesheet"> Adding your company information Our layouts include spaces for you to add your company’s copyright information and address. They look like this: &copy;{{ "now" | date:"%Y" }} [[YOUR COMPANY NAME HERE]] [[YOUR ADDRESS HERE]] Adding a logo The logo is defined in this block of code below. Just change the URL in src="" to your logo image, and that’s it! <!-- To add your logo image, modify the src here. --> <img src="https://s3.amazonaws.com/fast.customer.io/email-templates/logo.png" align="center" class="float-center"> Adding links and changing menus Some starters have a set of links. For example, the Menu Layout starter has a set of links at the top. You’ll obviously want to add your own. To add your own links, search for the following section. <!-- make sure to add correct titles and links for all your menu items! --> <th class="menu-item float-center"><a href="#">Account</a></th> <th class="menu-item float-center"><a href="#">Shop</a></th> <th class="menu-item float-center"><a href="#">Invite</a></th> <th class="menu-item float-center"><a href="#">My Cart</a></th> This is where you can add or remove the links themselves and customize the text. For example, I could add the following links to update the menu. <th class="menu-item float-center"><a href="https://example.com/products/new-releases">New Releases</a></th> <th class="menu-item float-center"><a href="https://example.com/products/anvils">Anvils</a></th> <th class="menu-item float-center"><a href="https://example.com/products/rollerskates">Rollerskates</a></th> <th class="menu-item float-center"><a href="https://example.com/products/bird-seed">Bird Seed</a></th> <th class="menu-item float-center"><a href="https://example.com/products/cement">Cement</a></th> Change the typeface By default, we use the Helvetica Neue font. If you want to change it, no problem! Find this comment and the associated CSS styles: /* Replace all instances of "Helvetica Neue" with font of your choice */ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; The fonts you use might depend on the email clients you want to support. Some fonts are better supported than others. Safe Fonts The safest typefaces to use are Arial, Verdana, Georgia, Times New Roman, and Courier: these fonts are universally supported. In the example above, we use Arial as a fallback if Helvetica doesn’t display. Use a Web font Web fonts, acquired through services like Google Fonts or MyFonts, can help you get creative with your font use! Web fonts aren’t 100% supported, but for the most part, web fonts will work in: iOS Mail Apple Mail Android (the mail client, not the Gmail app) Outlook 2000 and the Outlook.com app Thunderbird If you’re comfortable with that level of support, you can go to Google Fonts (or another font provider) and choose a font. When you use a font, Google will give you a URL like this: <link href="https://fonts.googleapis.com/css?family=Modak" rel="stylesheet"> Add it to the top of your Layouts code, under Foundation. Then, find instances of your font families and replace them with your new font declaration. Find this comment: /* Replace all instances of "Helvetica Neue" with font of your choice */ Replace font declarations below the comment with your new font: font-family: "Modak", Arial, sans-serif; Webfonts can be tricky; make sure that you set a fallback to a safe font. You can see that we’re using Arial as a fallback again, but you can choose whatever safe font you like! If you click Preview, you can see your new font choice, along with any other changes you’ve made. Here’s an example in the Announcement Starter where I’ve added a logo also: Adding buttons The following code adds a button and works in all email clients. You’ll notice hat our button is in a table: that’s one of the difficulties in ensuring that your message behaves the way you expect across all clients. <table class="button"> <tr> <td> <table> <tr> <td><a href="#">Button</a></td> </tr> </table> </td> </tr> </table> You can add this same code to your email’s content when composing an email. Customizing a button You can use Foundation’s Button docs as reference to customize buttons. To change a button’s size, add one of the tiny, small, or large classes. Here’s a large button: To change a button’s color, add one of the secondary, success, warning, or alert classes, like this: If you want to define your own color, you need to return to the Layout editor and add your own class. In this example, I’ve used acme-primary <table class="button acme-primary"> <tr> <td> <table> <tr> <td><a href="#">Success Button</a></td> </tr> </table> </td> </tr> </table> Then, in your Layout, add this code in the Foundation Overrides area of the styles, setting the color that you want to use for the button. table.button.acme-primary table td { background: #YOURCOLOR; border: 0px solid #YOURCOLOR; } table.button.acme-primary table a { border: 0px solid #YOURCOLOR; } Menu stacking On small screens, the navigation in our Menu Starter stacks. Here’s an example from an iPhone: If you want to prevent menus from stacking and keep links layed out horizontally on small screens, find the menu in the code and remove the small-vertical class. <table class="menu main-menu small-vertical"><tr><td><table><tr> <!-- make sure to add correct titles and links for all your menu items! --> <th class="menu-item float-center"><a href="#">Account</a></th> <th class="menu-item float-center"><a href="#">Shop</a></th> <th class="menu-item float-center"><a href="#">Invite</a></th> <th class="menu-item float-center"><a href="#">My Cart</a></th> </tr></table></td></tr></table> Adding columns You may want your emails to have multiple columns, or a grid. We generally recommend that you do this in your email’s content, rather than in the layout itself, because you’ll likely want the content within your grid to be dynamic. Dynamic content should reside in your message composer! For example, here we’re use Foundation’s Grid syntax to write our HTML. This means that column code uses the small-12 and large-6 classes to set the size of the column. The grid is divided by 12, so a two-column grid is made up of two large-6 columns: <th class="small-12 large-6 columns first"> <table> <tr> <th>One</th> </tr> </table> </th> <th class="small-12 large-6 columns last"> <table> <tr> <th>Three</th> </tr> </table> </th> Columns in the Layout You can make more complex grid-based customizations with some careful code wrangling. This is the code you’ll need to replace: <th class="small-12 large-12 columns first"><table><tr><th> <table class="wrapper" align="center"><tr><td class="wrapper-inner"> <!-- We'll replace this content tag with whatever you write in your email --> {{content}} </td></tr></table> </th> For example, if you want a two-column grid with a static sidebar, you could change your layout code as follows. Remember that the content in the left bar here will be editable in the Layout only; you cannot edit it in your emails. <th class="small-12 large-6 columns first"><table><tr><th> <table class="wrapper" align="center"><tr><td class="wrapper-inner"> Static sidebar content! </td></tr></table> </th> <th class="small-12 large-6 columns last"><table><tr><th> <table class="wrapper" align="center"><tr><td class="wrapper-inner"> <!-- We'll replace this content tag with whatever you write in your email --> {{content}} </td></tr></table> </th> --- ## Archiving Old Layouts URL: https://docs.customer.io/journeys/archiving-layouts/ If you find yourself with a Layout that you no longer need, but you _do_ want to keep the campaigns and newsletters associated with it (and their data), you can stay organized by archiving it! We don’t let you delete Layouts that you’re using in active campaigns or newsletters. But what if you don’t want to use that Layout anymore going forward? Simple: archive it! How to archive Archiving a Layout is simple. When you hover over that Layout in the Layouts menu, you’ll see a button to archive. The option will also be available on the individual Layout’s page. …and how to unarchive If you’d like to use the Layout in emails once more, head over to the ‘Archived’ tag, find the Layout you’d like to use again, and Unarchive it either from the Layouts listing, or the individual page: Remember that: Archived Layouts are not available for selection in future emails You can easily unarchive a Layout if you make a mistake You can still edit your archived Layouts You cannot archive your default Layout --- ## HTML and CSS Email vs. Web URL: https://docs.customer.io/journeys/1-html-and-css/ Quite often, our customers code their own email templates or receive them from a developer, and we'll get questions asking why a given email looks different between what's been coded, what's shown in Customer.io, and what's sent to a particular person. There are a couple of reasons for this: HTML and CSS work differently in emails and the web. Emails and how they’re laid out (Layouts) both work a little differently in Customer.io In this doc, we’ll try to explain reason #1, show how those differences manifest in emails, and hopefully give some good advice for how to move forward. (Here’s more info on reason #2.) Why does this happen? Coding for the web is code for browsers. There’s an accepted standard. We use semantic HTML and CSS. HTML like header, footer and paragraph tags add meaning to the content inside, and external CSS gives style and structure (things like display, float, or font-family). Emails, however, are a whole different kettle of fish. They’re opened and read in a huge variety of clients with no one standard between them. And therein lies the problem: Email client inconsistencies Desktop, web, and mobile email clients all use different engines to render an email. (E.g., Apple Mail, Outlook for Mac, and Android Mail use WebKit. Outlook 2003 uses IE, while Outlook 2013 uses Word.) Web clients will use the browser’s engine. This variety means that your emails will likely look different across browsers, because… separate CSS files are a no-go. All code has to go in the email. any CSS that isn’t inlined is usually stripped. no CSS shorthand! clients might add their own CSS. For example, Gmail will set all <td> fonts to font-family: Arial, sans-serif. They might also do funny things like strip out lines of code that begin with periods. your images are likely blocked by default, and a user may or may not see them. forms are inconsistent, as are videos (but gifs are mostly supported!) “responsive” emails are difficult and support for what “responsive” means can change across clients. CSS properties like display: none; aren’t supported everywhere, and neither are rounded corners. font support beyond the basic isn’t great, either As a result, an email that looks one way in the code editor might look different in Customer.io, might look different in Alice’s email client, and might look different in Bob’s email client. Some examples I made a simple table email which looks like this in the browser: In the Customer.io Preview, it looks like this: Meanwhile, here it is sent as a test to couple of mail clients, starting with my Apple Mail desktop client. Gmail in Chrome: Yahoo! Mail in Firefox: and Outlook 03 in Windows 7: You can see how this might be a headache! What you should do Unfortunately, some of this is unavoidable. Inconsistencies like the above will occur in rendering; different processing happens at different times! However, all is not lost. Once you understand the above, you’re well-placed to understand Customer.io and Layouts (both in and out of the app), and make your emails beautiful! Step 1: Understand Customer.io email How email works in Customer.io is pretty simple: ICYMI, we’ve got some email basics for you in this doc— where to write your copy, basic Liquid implementation, and testing. Step 2: Understand Customer.io Layouts Different services call these different things— Layouts, Templates, etc. In Customer.io, we decouple your email layout (how it looks) from its content (the words that live in it). Layouts live in one area of the app, while your email content lives in the Composer. We’ve written a comprehensive explanation of Layouts here - you can learn how to structure your HTML and CSS within Customer.io, and where the code lives! Step 3: Customize your emails! There’s a couple of ways you can do this. You can either start with something pre-built, which a lot of folks do, or from scratch. How to adapt a template This process is pretty straightforward once Layouts are understood. Here’s a couple of initial guides we’ve written with email layouts from popular frameworks: Foundation - Basic MailChimp - Two-Column Once you see how these are done, it should be easier for you to adapt your own! If there are guides you’d like to see, let us know! Code your own Feeling confident? Awesome! You can start from scratch and code your own email from the ground up. When coding, remember: Tables are your friend! Use these for your layout rather than semantic HTML. Inline CSS: Because browser-based email applications like Gmail, strip out <head> and <body> tags by default, you should always use inline CSS. We try to do this for you with Premailer. But you can also: write CSS inline as you go, use a web-based CSS inliner such as Foundation’s Inliner Don’t rely too much on images, because of blocking If you need to, you can target specific clients. For example, Outlook: <!--[if mso 12]>Only Outlook 2007 will see this.<![endif]--> Test, test, test! We can’t emphasize this enough. Test your email code before sending! At Customer.io, we recommend Litmus. Simple =/= boring! Basic doesn’t have to mean boring. You can still do cool stuff! It’s just different, and a little more difficult. Until email code catches up, there will be differences between web and email— but with a bit of testing, your emails can still be as beautiful as you want them to be. Want to read more, or need more help? Here’s a few great resources on HTML, CSS, and how they differ for web vs. email: Lee Munroe’s excellent article on how to build HTML emails Campaign Monitor’s breakdown of the CSS support for the top 10 most popular mobile, web and desktop email clients More CSS support The Tyranny of Tables: Why Web and Email Design are So Different --- ## CSS pre-processing URL: https://docs.customer.io/journeys/css-pre-processing/ By default, we pre-process CSS for emails created in our rich text or code editors, never our drag and drop editor. If you're using a custom email template which has already been pre-processed, add the template to our code editor then disable CSS pre-processing to avoid layout issues. This most frequently happens with responsive email templates on mobile devices. What’s CSS pre-processing? CSS pre-processing converts your CSS styles to inline attributes in your emails. This can make your email more compatible with email clients. Why disable CSS pre-processing? You may want to disable CSS pre-processing if your HTML has already been processed. For instance, if you copy/pasted a custom email template with styles already inlined, repeating CSS pre-processing may cause issues with your layout. This most frequently happens with responsive email templates on mobile devices. Disabling CSS pre-processing means that your CSS will stay exactly as specified in your template, avoiding these problems. How we pre-process CSS How we pre-process CSS depends on: The type of email editor you’re using The processor tied to the version of liquid available in your email Email editor CSS pre-processing How to change Processor Design Studio Disabled by default Transformer: CSS inlining Juice Drag-and-drop Disabled always N/A N/A Rich text Enabled always N/A Check liquid version Code Enabled by default Workspace setting and/or email setting Check liquid version Note, we never pre-process CSS for emails made in the drag-and-drop editor; we do not convert CSS or external stylesheets to inline styles. Liquid version determines processor The way we pre-process CSS and external stylesheets depends on the version of liquid used in your email. You’re either using our latest liquid or legacy liquid. To check, hover over the timestamp at the top of the email editor. Liquid version Processor Converts CSS to inline styles Converts external stylesheets Latest Juice yes no Legacy Premailer yes yes Design Studio emails only use our latest liquid, so they always use the same processor.  Emails using our latest liquid, processed by Juice, do NOT download and process external stylesheets. Even when the transformer for CSS inlining is turned on for Design Studio, we do not add inline styles based on external styles. Same with emails made in the rich text or code editors—when CSS pre-processing is enabled and they use the latest liquid, we do not convert external stylesheets. We leave the stylesheet tag intact for email clients that can process them. Code editor Workspace-level setting Within Workspace Settings > Email, click the Default Email Settings tab to find “CSS Pre-processing with the Code Editor.” By default, the setting is enabled across your workspace. It only influences emails created in the code editor, not those made in the rich text or drag and drop editors. Send a test message to yourself to see how your email will look with pre-processing enabled. Disable pre-processing If you disable the workspace-level setting, we will not apply pre-processing to any new emails created in the code editor. We will continue to pre-process CSS in any emails created while the workspace-level setting was enabled. To change pre-processing on existing messages, change the email-level setting. Send a test message to yourself to see how your email will look with pre-processing disabled. Email-level setting Within an email created in the code editor, you’ll see a checkbox for CSS pre-processing. By default, this is set to your workspace setting. The email-level setting takes precedence over the workspace-level toggle if, for instance, CSS pre-processing is disabled in your workspace but enabled for a message. This is only available for emails created in the code editor, not those made in the rich text or drag and drop editors. Send a test message to yourself to see how your email will look with pre-processing enabled. Disable pre-processing To disable CSS pre-processing on an email, uncheck the CSS pre-processing box in the header. Send a test message to yourself to see how your email will look with pre-processing disabled. --- ## AMP for email URL: https://docs.customer.io/journeys/amp-for-email/ Engage your audience with dynamic content and reduce the amount of times you have to link your customers out. [AMP](https://amp.dev/about/email) lets your customers complete surveys, RSVP to events, respond to comments, and more from within your emails. You can craft AMP components directly in the code editor of Design Studio or in the advanced code editor. Send test emails to ensure those with the ability to view dynamic content, and those without, will see what you expect. You also have the option to send AMP content through our App API. Requirements for sending and receiving AMP content To send an AMP message, you need to send from a domain that: Passes SPF Passes DKIM Has a DMARC that passes DKIM (optional, but recommended) Has HTTPS Link Tracking enabled in Customer.io Supports HTTPS delivery per Google’s requirements Is registered with to send AMP; this sets you up to send AMP emails to Gmail, Yahoo, and Mail.ru users. Keep in mind, you can complete all of the above, but that doesn’t mean all of your customers will be able to view AMP content. These are the ESPs that currently support AMP for email. To view AMP content in an email, the recipient must also enable dynamic content in their settings. Consult ESPs, like Gmail, for exact guidelines. Register to send AMP To send AMP emails, you need to fill out a sender registration form and make sure you follow policies for providers that support AMP—Google, Yahoo, and Mail.ru. See Register to send AMP for more information. Add AMP content to email Once your domain passes, you can configure AMP within the code editor of your Customer.io emails or via API. To create an AMP version of an email that uses our drag-n-drop or rich text editors, you will need to utilize our App API. Create AMP content in workflows Use the code editor of Design Studio or the advanced code editor to create AMP within your workspace.  Enable the experimental feature AMP for Email if you use the advanced code editor To create AMP within the advanced code editor, you must have both of these experimental features enabled in Account Settings: Advanced Code Editor and AMP for Email. You do not need to enable experimental features to use AMP in Design Studio. The AMP tab in the advanced code editor You can preview your AMP components on the right of the editor as well as send test emails to your account. AMP uses its own language as well as HTML. Most HTML tags are supported with AMP but some, like images, have exceptions. Inserting an image in our code editor will format it properly with <amp-img>. If you are new to AMP, check out the playground and templates at Amp.dev to save yourself time! Otherwise, this is the minimum template you’ll need to get started: <!doctype html> <html amp4email data-css-strict> <head> <meta charset="utf-8"> <script async src="https://cdn.ampproject.org/v0.js"></script> <style amp4email-boilerplate> body { visibility:hidden } </style> </head> <body> <all message content> </body> </html> Before sending an email with AMP components: Validate your links. Customer.io validates tracked links, so check that any untracked links (those that do not utilize your domain) are also valid and secure. Create an HTML fallback. People who allow dynamic content will see your AMP version and everyone else will see your fallback content. Select the HTML tab beside the AMP tab to get started. The HTML and AMP editors are separate, meaning, the content you make in one tab does not generate content in the other tab. You’ll find this email, like any other, within your message library. You’ll have the option to copy it to other campaigns, broadcasts, and transactional emails, as well. At this time, AMP does not support Customer.io’s Layouts, but you can craft the body or styling of the message as you see fit.  Need to track personalized links? Add the data-cio-tag attribute to your links. This lets you track different individualized links in the same category, so you can gather useful metrics about clicks to things like personalized product recommendations, password reset links, customer dashboards, etc. The data-cio-tag takes a string representing the “group” you want to track—<a href="http://mydomain.com?token=123abc" data-cio-tag="YOUR-LINK-GROUP-NAME">CLICK HERE</a> Create AMP content via API To create and update AMP content via API: Create the email in your workspace using any editor. This will be the fallback for anyone who cannot view dynamic content in their inbox. Create the AMP version of your message. Use tools like Amp.dev’s playground to try out components and templates and ensure they’re rendering properly. Validate your links. Customer.io validates tracked links, so check that any untracked links (those that do not utilize your domain) are also valid and secure. Send the request using the body_amp field. You can update an email through this newsletter endpoint, this campaign endpoint, or this transactional message endpoint. { "body_amp": "<!doctype html> <html amp4email data-css-strict> <head> <meta charset=\"utf-8\"> <script async src=\"https://cdn.ampproject.org/v0.js\"></script> <script async custom-element=\"amp-carousel\" src=\"https://cdn.ampproject.org/v0/amp-carousel-0.1.js\"></script> <style amp4email-boilerplate> body { visibility:hidden } </style> <style amp-custom> h1 { margin: 1rem } </style> </head> <body> <h1> New Year, New AMP {{ customer.email }} </h1> <amp-carousel id=\"carousel-with-preview\" width=\"800\" height=\"400\" layout=\"responsive\" type=\"slides\" on=\"slideChange:AMP.setState({currentCat: event.index})\"> <amp-img layout=\"fill\" src=\"https://amp.dev/static/img/tutorials/firstemail/photo_by_caleb_woods.jpg\" alt=\"photo courtesy of Unsplash\"></amp-img> <amp-img layout=\"fill\" src=\"https://amp.dev/static/img/tutorials/firstemail/photo_by_craig_mclaclan.jpg\" alt=\"photo courtesy of Unsplash\"></amp-img> <amp-img layout=\"fill\" src=\"https://amp.dev/static/img/tutorials/firstemail/photo_by_lightscape.jpg\" alt=\"photo courtesy of Unsplash\"></amp-img> <amp-img layout=\"fill\" src=\"https://amp.dev/static/img/tutorials/firstemail/photo_by_nick_karvounis.jpg\" alt=\"photo courtesy of Unsplash\"></amp-img> </amp-carousel> </body> </html>" } Send the message like any other - start your campaign, broadcast your newsletter, send your transactional email, etc.  AMP content created via API will not appear in your workspace. The AMP tab of the code editor only displays the content saved through the UI, not the API. Send test emails To send test emails with AMP, you must first allowlist the from address in your email settings. Test emails will display AMP content added via the code editor or the API. Report on interactions with AMP Currently, there are no native metrics specifically tracking engagement with AMP content. Your metrics will, however, reflect clicks on AMP content if click tracking is not disabled. --- ## Adding a view in browser link URL: https://docs.customer.io/journeys/view-in-browser/ Add a link to your emails so people can view a web version of the message in their browser. View in browser links only work in emails—they aren't supported in other message types like push notifications, SMS, or WhatsApp. “View in browser” links let your audience view a web-hosted version of your email in their browser. If you have an image-heavy email, worry about display issues in specific email clients, or simply want to give your audience the option to view your email outside of their inbox, you can add a link that generates a web version of the email personalized for each recipient.  Email only — and public “View in browser” links only work in emails. They are not supported in other message types, such as push notifications, in-app messages, SMS, or WhatsApp. The {% view_in_browser_url %} Liquid tag only generates a URL in the context of an email delivery. These links are also public—anyone with the URL can see the full email message. Don’t include sensitive information like bank account details, personal health data, or credit card numbers in emails that use “view in browser” links.  Transactional messages and protected content “View in browser” links will not work with Transactional messages when: Protect sensitive data is enabled. Your transactional API request includes "disable_message_retention": true. Learn more about the Transactional API and protecting sensitive data. How do I add a view in browser link into a custom layout? To add the default Customer.io view in browser link (similar to the unsubscribe one we talked about here), you can use the following HTML code in your layout: <a href="{% view_in_browser_url %}">View this email online</a> The resulting link will look something like: http://e.customeriomail.com/deliveries/... These links are not trackable. Your email must include this {% view_in_browser %} Liquid tag for Customer.io to generate the hosted HTML page. Without this tag, we won’t generate the hosted page and your link won’t work. How do I brand my link? By default, “View in browser” links use the e.customeriomail.com domain. You can brand these links with your own domain by setting up custom link tracking. If you set up HTTPS link tracking, your “View in browser” links will also use your custom HTTPS domain. --- ## Resources for templates, code, and best practices URL: https://docs.customer.io/journeys/4-email-design-resources/ This page contains resources to help you develop emails to support your use cases and audience. Adapt-a-template guides These guides are meant to show you the process of adapting a basic template from another service into a Customer.io Layout. Foundation - Basic Mailchimp - Two-Column Layout creation and templates Pre-built templates Mailchimp’s Blueprints Litmus community Foundation for Emails Inkbrush, an email layout builder A really simple responsive HTML email template Frameworks to build your own HTML Email Boilerplate MJML.io, a markup language that lets you code your own responsive emails emailframe.work, another coding framework for responsive emails Cerberus, a collection of patterns Code Campaign Monitor’s Ultimate Guide to CSS support MailChimp’s CSS support guide Making your emails accessible Emails and webfonts: the three main approaches you can take Longer reads, but full of great advice Mastering Responsive Email Design Across The Most Popular Clients A cheat sheet for mobile email design The Tyranny of Tables: Why Web and Email Design are So Different --- ## Adapting Foundation's Basic template URL: https://docs.customer.io/journeys/5-adapt-foundation-basic/ [Foundation for Emails](https://get.foundation/emails/) is a framework; a code bundle that gives you sets of pre-built components and makes it easier to build responsive emails. That way, you don't have to write all your code from scratch. They've got [a set of pre-built templates](https://get.foundation/emails/email-templates.html) that some of our customers like to use, the *Basic* one being one of them. This template looks something like this (depending on your client): To adapt it into a Customer.io layout, remember that you have to split it into two parts: Layout - what is consistent across emails? how will it look? what is its structure? Content - what do you want to edit on a per-email basis? For this, I’ve decided that everything in the yellow box, I want to edit on a per-email basis: So let’s do it! The Content Look at the HTML for the template, and pull out the part that governs the content you’ve identified, and only that content: <h1>Hi, Susan Calvin</h1> <p class="lead">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magni, iste, amet consequatur a veniam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut optio nulla et, fugiat. Maiores accusantium nostrum asperiores provident, quam modi ex inventore dolores id aspernatur architecto odio minima perferendis, explicabo. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Minima quos quasi itaque beatae natus fugit provident delectus, magnam laudantium odio corrupti sit quam. Optio aut ut repudiandae velit distinctio asperiores?</p> <table class="callout"> <tr> <th class="callout-inner primary"> <p>Callout: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit repellendus natus, sint ea optio dignissimos asperiores inventore a molestiae dolorum placeat repellat excepturi mollitia ducimus unde doloremque ad, alias eos!</p> </th> </tr> </table> Save it for later! The Layout Meanwhile, everything else in the code for the Basic layout will then go into the Email Layouts area. There, choose to create a new layout from scratch: Here’s the basic skeleton code: <html> <head> <style type="text/css"></style> </head> <body> {{content}} <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe</a> </body> </html> What you want to do first is grab the Foundation layout’s CSS code (there’s a lot of it!), and paste it between the <style></style> tags: Second (and this is the slightly tricky part), copy and paste the code between the two <body></body> tags from Foundation’s template into Customer.io, with two caveats: 1. Make sure that you keep the Customer.io {{ content }} tag where your email body will go (“Hi, Susan Calvin!”). 2. Keep an unsubscribe link! I’ve kept the Customer.io default, which is <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe</a> Then, just save your new layout! Back to the content! Remember the content code you saved? It goes in the Composer, when you create your email:  Note that the above is while in Code mode! This means that you can edit it on a per-email basis. It’s recommended that you stay in Code Mode while doing so. Then, just make sure that your layout is selected in Layout & Preview: And we’re done! The final code This is what I have in my ‘Email Layouts’, in case you’d like to copy and paste it directly: <html> <head> <style type="text/css"> .wrapper { width: 100%; } #outlook a { padding: 0; } body { width: 100% !important; min-width: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; Margin: 0; padding: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } #backgroundTable { margin: 0; Margin: 0; padding: 0; width: 100% !important; line-height: 100% !important; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; clear: both; display: block; } center { width: 100%; min-width: 580px; } a img { border: none; } p { margin: 0 0 0 10px; Margin: 0 0 0 10px; } table { border-spacing: 0; border-collapse: collapse; } td { word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; border-collapse: collapse !important; } table, tr, td { padding: 0; vertical-align: top; text-align: left; } @media only screen { html { min-height: 100%; background: #f3f3f3; } } table.body { background: #f3f3f3; height: 100%; width: 100%; } table.container { background: #fefefe; width: 580px; margin: 0 auto; Margin: 0 auto; text-align: inherit; } table.row { padding: 0; width: 100%; position: relative; } table.spacer { width: 100%; } table.spacer td { mso-line-height-rule: exactly; } table.container table.row { display: table; } td.columns, td.column, th.columns, th.column { margin: 0 auto; Margin: 0 auto; padding-left: 16px; padding-bottom: 16px; } td.columns .column, td.columns .columns, td.column .column, td.column .columns, th.columns .column, th.columns .columns, th.column .column, th.column .columns { padding-left: 0 !important; padding-right: 0 !important; } td.columns .column center, td.columns .columns center, td.column .column center, td.column .columns center, th.columns .column center, th.columns .columns center, th.column .column center, th.column .columns center { min-width: none !important; } td.columns.last, td.column.last, th.columns.last, th.column.last { padding-right: 16px; } td.columns table:not(.button), td.column table:not(.button), th.columns table:not(.button), th.column table:not(.button) { width: 100%; } td.large-1, th.large-1 { width: 32.33333px; padding-left: 8px; padding-right: 8px; } td.large-1.first, th.large-1.first { padding-left: 16px; } td.large-1.last, th.large-1.last { padding-right: 16px; } .collapse > tbody > tr > td.large-1, .collapse > tbody > tr > th.large-1 { padding-right: 0; padding-left: 0; width: 48.33333px; } .collapse td.large-1.first, .collapse th.large-1.first, .collapse td.large-1.last, .collapse th.large-1.last { width: 56.33333px; } td.large-1 center, th.large-1 center { min-width: 0.33333px; } .body .columns td.large-1, .body .column td.large-1, .body .columns th.large-1, .body .column th.large-1 { width: 8.33333%; } td.large-2, th.large-2 { width: 80.66667px; padding-left: 8px; padding-right: 8px; } td.large-2.first, th.large-2.first { padding-left: 16px; } td.large-2.last, th.large-2.last { padding-right: 16px; } .collapse > tbody > tr > td.large-2, .collapse > tbody > tr > th.large-2 { padding-right: 0; padding-left: 0; width: 96.66667px; } .collapse td.large-2.first, .collapse th.large-2.first, .collapse td.large-2.last, .collapse th.large-2.last { width: 104.66667px; } td.large-2 center, th.large-2 center { min-width: 48.66667px; } .body .columns td.large-2, .body .column td.large-2, .body .columns th.large-2, .body .column th.large-2 { width: 16.66667%; } td.large-3, th.large-3 { width: 129px; padding-left: 8px; padding-right: 8px; } td.large-3.first, th.large-3.first { padding-left: 16px; } td.large-3.last, th.large-3.last { padding-right: 16px; } .collapse > tbody > tr > td.large-3, .collapse > tbody > tr > th.large-3 { padding-right: 0; padding-left: 0; width: 145px; } .collapse td.large-3.first, .collapse th.large-3.first, .collapse td.large-3.last, .collapse th.large-3.last { width: 153px; } td.large-3 center, th.large-3 center { min-width: 97px; } .body .columns td.large-3, .body .column td.large-3, .body .columns th.large-3, .body .column th.large-3 { width: 25%; } td.large-4, th.large-4 { width: 177.33333px; padding-left: 8px; padding-right: 8px; } td.large-4.first, th.large-4.first { padding-left: 16px; } td.large-4.last, th.large-4.last { padding-right: 16px; } .collapse > tbody > tr > td.large-4, .collapse > tbody > tr > th.large-4 { padding-right: 0; padding-left: 0; width: 193.33333px; } .collapse td.large-4.first, .collapse th.large-4.first, .collapse td.large-4.last, .collapse th.large-4.last { width: 201.33333px; } td.large-4 center, th.large-4 center { min-width: 145.33333px; } .body .columns td.large-4, .body .column td.large-4, .body .columns th.large-4, .body .column th.large-4 { width: 33.33333%; } td.large-5, th.large-5 { width: 225.66667px; padding-left: 8px; padding-right: 8px; } td.large-5.first, th.large-5.first { padding-left: 16px; } td.large-5.last, th.large-5.last { padding-right: 16px; } .collapse > tbody > tr > td.large-5, .collapse > tbody > tr > th.large-5 { padding-right: 0; padding-left: 0; width: 241.66667px; } .collapse td.large-5.first, .collapse th.large-5.first, .collapse td.large-5.last, .collapse th.large-5.last { width: 249.66667px; } td.large-5 center, th.large-5 center { min-width: 193.66667px; } .body .columns td.large-5, .body .column td.large-5, .body .columns th.large-5, .body .column th.large-5 { width: 41.66667%; } td.large-6, th.large-6 { width: 274px; padding-left: 8px; padding-right: 8px; } td.large-6.first, th.large-6.first { padding-left: 16px; } td.large-6.last, th.large-6.last { padding-right: 16px; } .collapse > tbody > tr > td.large-6, .collapse > tbody > tr > th.large-6 { padding-right: 0; padding-left: 0; width: 290px; } .collapse td.large-6.first, .collapse th.large-6.first, .collapse td.large-6.last, .collapse th.large-6.last { width: 298px; } td.large-6 center, th.large-6 center { min-width: 242px; } .body .columns td.large-6, .body .column td.large-6, .body .columns th.large-6, .body .column th.large-6 { width: 50%; } td.large-7, th.large-7 { width: 322.33333px; padding-left: 8px; padding-right: 8px; } td.large-7.first, th.large-7.first { padding-left: 16px; } td.large-7.last, th.large-7.last { padding-right: 16px; } .collapse > tbody > tr > td.large-7, .collapse > tbody > tr > th.large-7 { padding-right: 0; padding-left: 0; width: 338.33333px; } .collapse td.large-7.first, .collapse th.large-7.first, .collapse td.large-7.last, .collapse th.large-7.last { width: 346.33333px; } td.large-7 center, th.large-7 center { min-width: 290.33333px; } .body .columns td.large-7, .body .column td.large-7, .body .columns th.large-7, .body .column th.large-7 { width: 58.33333%; } td.large-8, th.large-8 { width: 370.66667px; padding-left: 8px; padding-right: 8px; } td.large-8.first, th.large-8.first { padding-left: 16px; } td.large-8.last, th.large-8.last { padding-right: 16px; } .collapse > tbody > tr > td.large-8, .collapse > tbody > tr > th.large-8 { padding-right: 0; padding-left: 0; width: 386.66667px; } .collapse td.large-8.first, .collapse th.large-8.first, .collapse td.large-8.last, .collapse th.large-8.last { width: 394.66667px; } td.large-8 center, th.large-8 center { min-width: 338.66667px; } .body .columns td.large-8, .body .column td.large-8, .body .columns th.large-8, .body .column th.large-8 { width: 66.66667%; } td.large-9, th.large-9 { width: 419px; padding-left: 8px; padding-right: 8px; } td.large-9.first, th.large-9.first { padding-left: 16px; } td.large-9.last, th.large-9.last { padding-right: 16px; } .collapse > tbody > tr > td.large-9, .collapse > tbody > tr > th.large-9 { padding-right: 0; padding-left: 0; width: 435px; } .collapse td.large-9.first, .collapse th.large-9.first, .collapse td.large-9.last, .collapse th.large-9.last { width: 443px; } td.large-9 center, th.large-9 center { min-width: 387px; } .body .columns td.large-9, .body .column td.large-9, .body .columns th.large-9, .body .column th.large-9 { width: 75%; } td.large-10, th.large-10 { width: 467.33333px; padding-left: 8px; padding-right: 8px; } td.large-10.first, th.large-10.first { padding-left: 16px; } td.large-10.last, th.large-10.last { padding-right: 16px; } .collapse > tbody > tr > td.large-10, .collapse > tbody > tr > th.large-10 { padding-right: 0; padding-left: 0; width: 483.33333px; } .collapse td.large-10.first, .collapse th.large-10.first, .collapse td.large-10.last, .collapse th.large-10.last { width: 491.33333px; } td.large-10 center, th.large-10 center { min-width: 435.33333px; } .body .columns td.large-10, .body .column td.large-10, .body .columns th.large-10, .body .column th.large-10 { width: 83.33333%; } td.large-11, th.large-11 { width: 515.66667px; padding-left: 8px; padding-right: 8px; } td.large-11.first, th.large-11.first { padding-left: 16px; } td.large-11.last, th.large-11.last { padding-right: 16px; } .collapse > tbody > tr > td.large-11, .collapse > tbody > tr > th.large-11 { padding-right: 0; padding-left: 0; width: 531.66667px; } .collapse td.large-11.first, .collapse th.large-11.first, .collapse td.large-11.last, .collapse th.large-11.last { width: 539.66667px; } td.large-11 center, th.large-11 center { min-width: 483.66667px; } .body .columns td.large-11, .body .column td.large-11, .body .columns th.large-11, .body .column th.large-11 { width: 91.66667%; } td.large-12, th.large-12 { width: 564px; padding-left: 8px; padding-right: 8px; } td.large-12.first, th.large-12.first { padding-left: 16px; } td.large-12.last, th.large-12.last { padding-right: 16px; } .collapse > tbody > tr > td.large-12, .collapse > tbody > tr > th.large-12 { padding-right: 0; padding-left: 0; width: 580px; } .collapse td.large-12.first, .collapse th.large-12.first, .collapse td.large-12.last, .collapse th.large-12.last { width: 588px; } td.large-12 center, th.large-12 center { min-width: 532px; } .body .columns td.large-12, .body .column td.large-12, .body .columns th.large-12, .body .column th.large-12 { width: 100%; } td.large-offset-1, td.large-offset-1.first, td.large-offset-1.last, th.large-offset-1, th.large-offset-1.first, th.large-offset-1.last { padding-left: 64.33333px; } td.large-offset-2, td.large-offset-2.first, td.large-offset-2.last, th.large-offset-2, th.large-offset-2.first, th.large-offset-2.last { padding-left: 112.66667px; } td.large-offset-3, td.large-offset-3.first, td.large-offset-3.last, th.large-offset-3, th.large-offset-3.first, th.large-offset-3.last { padding-left: 161px; } td.large-offset-4, td.large-offset-4.first, td.large-offset-4.last, th.large-offset-4, th.large-offset-4.first, th.large-offset-4.last { padding-left: 209.33333px; } td.large-offset-5, td.large-offset-5.first, td.large-offset-5.last, th.large-offset-5, th.large-offset-5.first, th.large-offset-5.last { padding-left: 257.66667px; } td.large-offset-6, td.large-offset-6.first, td.large-offset-6.last, th.large-offset-6, th.large-offset-6.first, th.large-offset-6.last { padding-left: 306px; } td.large-offset-7, td.large-offset-7.first, td.large-offset-7.last, th.large-offset-7, th.large-offset-7.first, th.large-offset-7.last { padding-left: 354.33333px; } td.large-offset-8, td.large-offset-8.first, td.large-offset-8.last, th.large-offset-8, th.large-offset-8.first, th.large-offset-8.last { padding-left: 402.66667px; } td.large-offset-9, td.large-offset-9.first, td.large-offset-9.last, th.large-offset-9, th.large-offset-9.first, th.large-offset-9.last { padding-left: 451px; } td.large-offset-10, td.large-offset-10.first, td.large-offset-10.last, th.large-offset-10, th.large-offset-10.first, th.large-offset-10.last { padding-left: 499.33333px; } td.large-offset-11, td.large-offset-11.first, td.large-offset-11.last, th.large-offset-11, th.large-offset-11.first, th.large-offset-11.last { padding-left: 547.66667px; } td.expander, th.expander { visibility: hidden; width: 0; padding: 0 !important; } table.container.radius { border-radius: 0; border-collapse: separate; } .block-grid { width: 100%; max-width: 580px; } .block-grid td { display: inline-block; padding: 8px; } .up-2 td { width: 274px !important; } .up-3 td { width: 177px !important; } .up-4 td { width: 129px !important; } .up-5 td { width: 100px !important; } .up-6 td { width: 80px !important; } .up-7 td { width: 66px !important; } .up-8 td { width: 56px !important; } table.text-center, th.text-center, td.text-center, h1.text-center, h2.text-center, h3.text-center, h4.text-center, h5.text-center, h6.text-center, p.text-center, span.text-center { text-align: center; } table.text-left, th.text-left, td.text-left, h1.text-left, h2.text-left, h3.text-left, h4.text-left, h5.text-left, h6.text-left, p.text-left, span.text-left { text-align: left; } table.text-right, th.text-right, td.text-right, h1.text-right, h2.text-right, h3.text-right, h4.text-right, h5.text-right, h6.text-right, p.text-right, span.text-right { text-align: right; } span.text-center { display: block; width: 100%; text-align: center; } @media only screen and (max-width: 596px) { .small-float-center { margin: 0 auto !important; float: none !important; text-align: center !important; } .small-text-center { text-align: center !important; } .small-text-left { text-align: left !important; } .small-text-right { text-align: right !important; } } img.float-left { float: left; text-align: left; } img.float-right { float: right; text-align: right; } img.float-center, img.text-center { margin: 0 auto; Margin: 0 auto; float: none; text-align: center; } table.float-center, td.float-center, th.float-center { margin: 0 auto; Margin: 0 auto; float: none; text-align: center; } .hide-for-large { display: none !important; mso-hide: all; overflow: hidden; max-height: 0; font-size: 0; width: 0; line-height: 0; } @media only screen and (max-width: 596px) { .hide-for-large { display: block !important; width: auto !important; overflow: visible !important; max-height: none !important; font-size: inherit !important; line-height: inherit !important; } } table.body table.container .hide-for-large * { mso-hide: all; } @media only screen and (max-width: 596px) { table.body table.container .hide-for-large, table.body table.container .row.hide-for-large { display: table !important; width: 100% !important; } } @media only screen and (max-width: 596px) { table.body table.container .callout-inner.hide-for-large { display: table-cell !important; width: 100% !important; } } @media only screen and (max-width: 596px) { table.body table.container .show-for-large { display: none !important; width: 0; mso-hide: all; overflow: hidden; } } body, table.body, h1, h2, h3, h4, h5, h6, p, td, th, a { color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-weight: normal; padding: 0; margin: 0; Margin: 0; text-align: left; line-height: 1.3; } h1, h2, h3, h4, h5, h6 { color: inherit; word-wrap: normal; font-family: Helvetica, Arial, sans-serif; font-weight: normal; margin-bottom: 10px; Margin-bottom: 10px; } h1 { font-size: 34px; } h2 { font-size: 30px; } h3 { font-size: 28px; } h4 { font-size: 24px; } h5 { font-size: 20px; } h6 { font-size: 18px; } body, table.body, p, td, th { font-size: 16px; line-height: 1.3; } p { margin-bottom: 10px; Margin-bottom: 10px; } p.lead { font-size: 20px; line-height: 1.6; } p.subheader { margin-top: 4px; margin-bottom: 8px; Margin-top: 4px; Margin-bottom: 8px; font-weight: normal; line-height: 1.4; color: #8a8a8a; } small { font-size: 80%; color: #cacaca; } a { color: #2199e8; text-decoration: none; } a:hover { color: #147dc2; } a:active { color: #147dc2; } a:visited { color: #2199e8; } h1 a, h1 a:visited, h2 a, h2 a:visited, h3 a, h3 a:visited, h4 a, h4 a:visited, h5 a, h5 a:visited, h6 a, h6 a:visited { color: #2199e8; } pre { background: #f3f3f3; margin: 30px 0; Margin: 30px 0; } pre code { color: #cacaca; } pre code span.callout { color: #8a8a8a; font-weight: bold; } pre code span.callout-strong { color: #ff6908; font-weight: bold; } table.hr { width: 100%; } table.hr th { height: 0; max-width: 580px; border-top: 0; border-right: 0; border-bottom: 1px solid #0a0a0a; border-left: 0; margin: 20px auto; Margin: 20px auto; clear: both; } .stat { font-size: 40px; line-height: 1; } p + .stat { margin-top: -16px; Margin-top: -16px; } span.preheader { display: none !important; visibility: hidden; mso-hide: all !important; font-size: 1px; color: #f3f3f3; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; } table.button { width: auto; margin: 0 0 16px 0; Margin: 0 0 16px 0; } table.button table td { text-align: left; color: #fefefe; background: #2199e8; border: 2px solid #2199e8; } table.button table td a { font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; color: #fefefe; text-decoration: none; display: inline-block; padding: 8px 16px 8px 16px; border: 0 solid #2199e8; border-radius: 3px; } table.button.radius table td { border-radius: 3px; border: none; } table.button.rounded table td { border-radius: 500px; border: none; } table.button:hover table tr td a, table.button:active table tr td a, table.button table tr td a:visited, table.button.tiny:hover table tr td a, table.button.tiny:active table tr td a, table.button.tiny table tr td a:visited, table.button.small:hover table tr td a, table.button.small:active table tr td a, table.button.small table tr td a:visited, table.button.large:hover table tr td a, table.button.large:active table tr td a, table.button.large table tr td a:visited { color: #fefefe; } table.button.tiny table td, table.button.tiny table a { padding: 4px 8px 4px 8px; } table.button.tiny table a { font-size: 10px; font-weight: normal; } table.button.small table td, table.button.small table a { padding: 5px 10px 5px 10px; font-size: 12px; } table.button.large table a { padding: 10px 20px 10px 20px; font-size: 20px; } table.button.expand, table.button.expanded { width: 100% !important; } table.button.expand table, table.button.expanded table { width: 100%; } table.button.expand table a, table.button.expanded table a { text-align: center; width: 100%; padding-left: 0; padding-right: 0; } table.button.expand center, table.button.expanded center { min-width: 0; } table.button:hover table td, table.button:visited table td, table.button:active table td { background: #147dc2; color: #fefefe; } table.button:hover table a, table.button:visited table a, table.button:active table a { border: 0 solid #147dc2; } table.button.secondary table td { background: #777777; color: #fefefe; border: 0px solid #777777; } table.button.secondary table a { color: #fefefe; border: 0 solid #777777; } table.button.secondary:hover table td { background: #919191; color: #fefefe; } table.button.secondary:hover table a { border: 0 solid #919191; } table.button.secondary:hover table td a { color: #fefefe; } table.button.secondary:active table td a { color: #fefefe; } table.button.secondary table td a:visited { color: #fefefe; } table.button.success table td { background: #3adb76; border: 0px solid #3adb76; } table.button.success table a { border: 0 solid #3adb76; } table.button.success:hover table td { background: #23bf5d; } table.button.success:hover table a { border: 0 solid #23bf5d; } table.button.alert table td { background: #ec5840; border: 0px solid #ec5840; } table.button.alert table a { border: 0 solid #ec5840; } table.button.alert:hover table td { background: #e23317; } table.button.alert:hover table a { border: 0 solid #e23317; } table.button.warning table td { background: #ffae00; border: 0px solid #ffae00; } table.button.warning table a { border: 0px solid #ffae00; } table.button.warning:hover table td { background: #cc8b00; } table.button.warning:hover table a { border: 0px solid #cc8b00; } table.callout { margin-bottom: 16px; Margin-bottom: 16px; } th.callout-inner { width: 100%; border: 1px solid #cbcbcb; padding: 10px; background: #fefefe; } th.callout-inner.primary { background: #def0fc; border: 1px solid #444444; color: #0a0a0a; } th.callout-inner.secondary { background: #ebebeb; border: 1px solid #444444; color: #0a0a0a; } th.callout-inner.success { background: #e1faea; border: 1px solid #1b9448; color: #fefefe; } th.callout-inner.warning { background: #fff3d9; border: 1px solid #996800; color: #fefefe; } th.callout-inner.alert { background: #fce6e2; border: 1px solid #b42912; color: #fefefe; } .thumbnail { border: solid 4px #fefefe; box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2); display: inline-block; line-height: 0; max-width: 100%; transition: box-shadow 200ms ease-out; border-radius: 3px; margin-bottom: 16px; } .thumbnail:hover, .thumbnail:focus { box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); } table.menu { width: 580px; } table.menu td.menu-item, table.menu th.menu-item { padding: 10px; padding-right: 10px; } table.menu td.menu-item a, table.menu th.menu-item a { color: #2199e8; } table.menu.vertical td.menu-item, table.menu.vertical th.menu-item { padding: 10px; padding-right: 0; display: block; } table.menu.vertical td.menu-item a, table.menu.vertical th.menu-item a { width: 100%; } table.menu.vertical td.menu-item table.menu.vertical td.menu-item, table.menu.vertical td.menu-item table.menu.vertical th.menu-item, table.menu.vertical th.menu-item table.menu.vertical td.menu-item, table.menu.vertical th.menu-item table.menu.vertical th.menu-item { padding-left: 10px; } table.menu.text-center a { text-align: center; } .menu[align="center"] { width: auto !important; } body.outlook p { display: inline !important; } @media only screen and (max-width: 596px) { table.body img { width: auto; height: auto; } table.body center { min-width: 0 !important; } table.body .container { width: 95% !important; } table.body .columns, table.body .column { height: auto !important; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; padding-left: 16px !important; padding-right: 16px !important; } table.body .columns .column, table.body .columns .columns, table.body .column .column, table.body .column .columns { padding-left: 0 !important; padding-right: 0 !important; } table.body .collapse .columns, table.body .collapse .column { padding-left: 0 !important; padding-right: 0 !important; } td.small-1, th.small-1 { display: inline-block !important; width: 8.33333% !important; } td.small-2, th.small-2 { display: inline-block !important; width: 16.66667% !important; } td.small-3, th.small-3 { display: inline-block !important; width: 25% !important; } td.small-4, th.small-4 { display: inline-block !important; width: 33.33333% !important; } td.small-5, th.small-5 { display: inline-block !important; width: 41.66667% !important; } td.small-6, th.small-6 { display: inline-block !important; width: 50% !important; } td.small-7, th.small-7 { display: inline-block !important; width: 58.33333% !important; } td.small-8, th.small-8 { display: inline-block !important; width: 66.66667% !important; } td.small-9, th.small-9 { display: inline-block !important; width: 75% !important; } td.small-10, th.small-10 { display: inline-block !important; width: 83.33333% !important; } td.small-11, th.small-11 { display: inline-block !important; width: 91.66667% !important; } td.small-12, th.small-12 { display: inline-block !important; width: 100% !important; } .columns td.small-12, .column td.small-12, .columns th.small-12, .column th.small-12 { display: block !important; width: 100% !important; } table.body td.small-offset-1, table.body th.small-offset-1 { margin-left: 8.33333% !important; Margin-left: 8.33333% !important; } table.body td.small-offset-2, table.body th.small-offset-2 { margin-left: 16.66667% !important; Margin-left: 16.66667% !important; } table.body td.small-offset-3, table.body th.small-offset-3 { margin-left: 25% !important; Margin-left: 25% !important; } table.body td.small-offset-4, table.body th.small-offset-4 { margin-left: 33.33333% !important; Margin-left: 33.33333% !important; } table.body td.small-offset-5, table.body th.small-offset-5 { margin-left: 41.66667% !important; Margin-left: 41.66667% !important; } table.body td.small-offset-6, table.body th.small-offset-6 { margin-left: 50% !important; Margin-left: 50% !important; } table.body td.small-offset-7, table.body th.small-offset-7 { margin-left: 58.33333% !important; Margin-left: 58.33333% !important; } table.body td.small-offset-8, table.body th.small-offset-8 { margin-left: 66.66667% !important; Margin-left: 66.66667% !important; } table.body td.small-offset-9, table.body th.small-offset-9 { margin-left: 75% !important; Margin-left: 75% !important; } table.body td.small-offset-10, table.body th.small-offset-10 { margin-left: 83.33333% !important; Margin-left: 83.33333% !important; } table.body td.small-offset-11, table.body th.small-offset-11 { margin-left: 91.66667% !important; Margin-left: 91.66667% !important; } table.body table.columns td.expander, table.body table.columns th.expander { display: none !important; } table.body .right-text-pad, table.body .text-pad-right { padding-left: 10px !important; } table.body .left-text-pad, table.body .text-pad-left { padding-right: 10px !important; } table.menu { width: 100% !important; } table.menu td, table.menu th { width: auto !important; display: inline-block !important; } table.menu.vertical td, table.menu.vertical th, table.menu.small-vertical td, table.menu.small-vertical th { display: block !important; } table.menu[align="center"] { width: auto !important; } table.button.small-expand, table.button.small-expanded { width: 100% !important; } table.button.small-expand table, table.button.small-expanded table { width: 100%; } table.button.small-expand table a, table.button.small-expanded table a { text-align: center !important; width: 100% !important; padding-left: 0 !important; padding-right: 0 !important; } table.button.small-expand center, table.button.small-expanded center { min-width: 0; } } </style> <style> .header { background: #8a8a8a; } .header .columns { padding-bottom: 0; } .header p { color: #fff; margin-bottom: 0; } .header .wrapper-inner { padding: 20px; /*controls the height of the header*/ } .header .container { background: #8a8a8a; } .wrapper.secondary { background: #f3f3f3; } </style> </head> <body> <table class="body" data-made-with-foundation=""> <tr> <td class="float-center" align="center" valign="top"> <center data-parsed=""> <!-- move the above styles into your custom stylesheet --> <table bgcolor="#8a8a8a" align="center" class="wrapper header float-center"> <tr> <td class="wrapper-inner"> <table align="center" class="container"> <tbody> <tr> <td> <table class="row collapse"> <tbody> <tr> <th class="small-6 large-6 columns first" valign="middle"> <table> <tr> <th> <img src="http://placehold.it/200x50/09ACAB"> </th> </tr> </table> </th> <th class="small-6 large-6 columns last" valign="middle"> <table> <tr> <th> <p class="text-right">BASIC</p> </th> </tr> </table> </th> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </table> <table align="center" class="container float-center"> <tbody> <tr> <td> <table class="spacer"> <tbody> <tr> <td height="16px" style="font-size:16px;line-height:16px;">&#xA0;</td> </tr> </tbody> </table> <table class="row"> <tbody> <tr> <th class="small-12 large-12 columns first last"> <table> <tr> <th> {{ content }} </th> <th class="expander"></th> </tr> </table> </th> </tr> </tbody> </table> <table class="wrapper secondary" align="center"> <tr> <td class="wrapper-inner"> <table class="spacer"> <tbody> <tr> <td height="16px" style="font-size:16px;line-height:16px;">&#xA0;</td> </tr> </tbody> </table> <table class="row"> <tbody> <tr> <th class="small-12 large-6 columns first"> <table> <tr> <th> <h5>Connect With Us:</h5> <table class="menu vertical"> <tr> <td> <table> <tr> <th style="text-align: left;" class="menu-item float-center"><a href="#">Twitter</a></th> <th style="text-align: left;" class="menu-item float-center"><a href="#">Facebook</a></th> <th style="text-align: left;" class="menu-item float-center"><a href="#">Google +</a></th> </tr> </table> </td> </tr> </table> </th> </tr> </table> </th> <th class="small-12 large-6 columns last"> <table> <tr> <th> <h5>Contact Info:</h5> <p>Phone: 123-456-7890</p> <p>Email: <a href="mailto:you@example.com">you@example.com</a></p> </th> </tr> </table> </th> </tr> </tbody> </table> </td> </tr> </table> </td> </tr> </tbody> </table> </center> </td> </tr> </table> <!-- You can use the Customer.io unsubscribe link in all of your emails. We are able to track the unsubscribe to the specific email they unsubscribed from. You also have the option to include an unsubscribe link in your email header. Newsletters will automatically avoid sending to unsubscribed users, and in campaigns you have the option of whether to send to unsubscribed users or not. To learn more, or if you want to handle unsubscribes yourself, please refer to the docs: https://docs.customer.io/journeys/unsubscribes/ --> <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe</a> </body> </html> --- ## Adapting MailChimp's Two-Column template URL: https://docs.customer.io/journeys/6-adapt-mc-twocol/ If you use MailChimp, you’ll be familiar with their pre-built templates, and this is one of the more straightforward ones! It looks something like this when you start, (depending on your client): To adapt it into a Customer.io layout, remember that you have to split it into two parts: Layout - what is consistent across emails? how will it look? what is its structure? Content - what do you want to edit on a per-email basis? Split up the template I’ve decided that everything in the yellow box, I want to edit on a per-email basis: So let’s do it! Pull out HTML that governs the content I’ve found it’s easiest to do this with two tabs in a code editor. The first has the HTML of the template, and the other is blank. Look at the HTML for the template, and pull out the part that governs the content you’ve identified, and only that content. In this MailChimp layout, that’s the two table rows for templateBody and templateColumns: The code for them is located between lines 667-787. Here’s a gif of me pulling that content out: If you’d like the raw code, you can find it here. Once you’ve cut this code from the HTML template, paste it into your blank code document to save it for later. The Customer.io Layout Meanwhile, everything else in the code for the Basic layout will then go into the Email Layouts area. There, choose to create a new layout from scratch: Here’s the basic skeleton code: <html> <head> <style type="text/css"> </style> </head> <body> {{content}} <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe</a> </body> </html> Between style tags, get the MailChimp style out (it’s lines 19-587 for me). Put it between the <style></style> tags at the top of the Customer.io layout: Second (and this is the slightly tricky part), get the structure. Remember when I pulled the content out of my email above? Everything that’s left between the two <body></body> tags is what will go in your Customer.io Layout: Copy and paste that code between the two <body></body> tags from the MailChimp template into Customer.io, with two caveats: 1. Make sure that you keep the Customer.io {{ content }} tag where your email body will go. 2. Keep an unsubscribe link! I’ve kept the Customer.io default, which is <a href="{% unsubscribe_url %}" class="untracked">Unsubscribe</a> Then, just save your new layout! You can now use it in your emails. So let’s do that! Back to the content! Remember the content code you saved, the two table rows from lines 667-787? It goes in the Composer when you create your email: Note that I'm in Code mode! This means that you can edit it on a per-email basis. You should stay in Code Mode while doing so; this prevents layout breakages induced by the rich-text editor. Then, just make sure that your layout is selected in Layout & Preview: And we’re done! Grab the final code to paste into Customer.io In case you’d like to copy and paste the code we worked through directly, you can grab the final code for what I have in my Email Layout from Github and here’s the HTML for the Email Content. A note on MailChimp merge tags Adjust any MailChimp-specific tags (*|MC:SUBJECT|*, for example) to work with Customer.io. --- ## How do I add an avatar/logo to my emails? URL: https://docs.customer.io/journeys/adding-sender-image/ By default, that image people see next to your email in their inbox will be the first character of your company’s name. Occasionally we get asked how to add a logo so that it will appear next to your email messages in the end-user’s inbox instead. Unfortunately, this is not something you (or we) control exclusively as part of the email sending process but rather it is determined by the recipient’s inbox provider or email client. Avatars shown in Gmail When sending from G-Suite, Gmail will use the sender’s profile image as you would expect. When sending from non-Gmail sources, you used to be able to signal which image to use by uploading it to a Google+ account but those days are long gone. These days, Gmail will use your BIMI configuration to determine which image to show. BIMI stands for “Brand Indicators for Message Identification.” It is an emerging email specification that enables the use of brand-controlled logos within supporting email clients. You can read more about what BIMI is and why you should care in this article by Litmus as well as in this BIMI Q&A article from Email on Acid. Avatars for other email clients BIMI is also used by Yahoo! and we believe more inbox providers are sure to follow but many other email clients (Thunderbird, Airmail, and Postbox, for example) use Gravatar to display the sender image. To take full control of this avatar in the email clients that support it, create a Gravatar account, verify your email address, and choose your desired image.  It may take a little while for your new image to take effect. --- ## Set Custom Email Headers URL: https://docs.customer.io/journeys/custom-mail-headers/ Email headers specify specific sending and return options for an email. When sending via SMTP, you can use custom headers to do a great many things: customize messages, tag them, track them, or control specific behaviors. For example: Set the X-Mailgun-Tag header if you’re using Mailgun’s “Topic” subscription functionality Set your own List-Unsubscribe header for specific emails (if you use our unsubscribe link, we add this header automatically) Stop emails from being shown as a thread by Gmail with the X-Entity-Ref-ID header Use X-Auto-Response-Suppress:OOF to suppress auto-replies from Exchange servers Adding, editing, and removing custom headers You’ll find the custom headers option in the email composer. Add them by clicking the Add Custom link to the right of the Headers: Here’s an example of an added item: You cannot CC people on your emails at this time. We do have options for Reply-to and BCC, though. Errors There are a few guidelines: You can add up to four custom headers at a time, in addition to Reply-to, BCC, and Subject. Your header’s name can’t be blank, but the value can be. There are some headers you can’t use (see “Denylisted headers” below) You can use Liquid here! If there are errors, you’ll see them in the ‘Review Errors’ modal. When there’s a problem with your headers, our ‘Review Errors’ button animates: And we show you an indicator on the relevant header. Then, you can review your errors in the resulting modal: It looks like this: If you’re concerned about how to deal with a specific error here, you should be able to find it in our our Composer Errors documentation. Denylisted headers There are some headers that we need to keep control of at Customer.io: Message data such as Subject or To MIME headers which dictate email formatting, as well as Return Path information (Received or Return-Path): these are generated by our servers at time of delivery Mail client information tags, such as X-Mailer: Customer.io needs to set X-Mailer and X-Report-Abuse-To, to allow receiving services to notify us of bad Authentication-Results The full list: Message data MIME Return Path Information Client-specific Message-ID Mime-Version Received Mail-System-Version Date Content-ID Return-Path Mailer From Content-Base Authentication-Results Originating-Client Subject Content-Alias Received-SPF X-Mailer To Content-Identifier Auto-Submitted X-Report-Abuse-To Reply-To Content-Length VBR-Info CC Lines Content-Type Content-Disposition Content-Transfer-Encoding Encoding Provider-specific headers X-SMTPAPI (SendGrid): If you send an X-SMTPAPI header, we reserve the use of the email_id unique argument to support message tracking and reporting, the remainder of your header will be passed through to Sendgrid unchanged. Feedback? This is a fairly advanced feature, and we want to make it as useful as possible, and help you better track and customize your emails. If you have any questions or feedback on this feature, please let us know! --- ## Set custom preheader/preview text URL: https://docs.customer.io/journeys/custom-preheader-text/ An email's preheader text (more accurately referred to as "preview text") is the small block of text shown in an end-user's email inbox, next to or underneath the subject line. It's often used as a summary or an incentive to open and read the rest of the email. Normally, this preview is pulled from the first text in your email. This is OK if your content starts right away, and is short enough for a summary. However, if that isn’t the case, you might have text like ‘Is this email not displaying correctly?’ taking up valuable inbox real estate that could be used to incentivise opens instead! In Customer.io, you can control exactly what text will appear in this preview for an email— to avoid situations like the one mentioned above. How to specify your preview text Simple! In the composer, click on ‘Add preheader text’ next to the subject: Save, and you’re good to go! You can also use Liquid here— customer attributes, event data (for event triggered campaigns), and so on. Preview text length We estimate that most email clients (iOS, GMail, etc.) automatically show up to 140 characters of preheader text but that number could be much lower and is, unfortunately, not within your (or our) control. How to hide automatically pulled in preheader text When adding custom preheader text in Customer.io, by default, the first line of your email content is automatically pulled in if the preheader text is shorter than 140 characters. We do this because we heard feedback that short preheader text (lots of blank space) can appear “spammy” in the inbox. However, if you wish to push back the first line of your email content so that it is not autmatically shown as the preheader text, here is a helpful article from Litmus that will help you do that. The key snippet from that article: You can create white space after your desired preview text so that email clients don’t pull other distracting text or characters into the envelope content. All you need to do is add a chain of zero-width non-joiners (‌) and non-breaking spaces ( ) after the preview text that you want displayed. The repetition of “‌ ” then fills any remaining preview text space. So, to get started, you can add instances of &zwnj; and &nbsp; directly to the “preheader” input field in the message editor in Customer.io. The part that’s hard to determine (and requires lots of testing) is how many instances of those to provide. So for example, this is how much it took to push the body copy out of a preview in Apple Mail: Does this work?? &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp; With that included in your preheader text, you’ll get this result: …and without all that, you’ll get the standard expected result: So, this means you can definitely push the body content out of your preview, but it’s going to take a large amount of spacing and testing to make sure it’s out. You’ll want to send yourself test copies in multiple email clients to ensure that you’re getting as close to a consistent result as possible. Client support Most email clients will display some kind of preview text. There’s variation in placement and length, but for most modern clients, you’re good to go. The below services do not support preview text: Desktop Mobile Webmail Lotus Notes 8.5 Blackberry GMX Outlook 2003 Web.de Outlook 2007 Freenet.de Outlook 2010 If you want a more comprehensive guide to preview text support, check out this additional article from Litmus. Gmail support When searching in Gmail, the results may pull from the email’s plain text version instead of your custom preview text. This can lead to results that include lines of asterisks, and it might look like your preview text is broken or redacted. This is an issue with Gmail currently, not preheader text in Customer.io. --- ## What does the Fake BCC option do? URL: https://docs.customer.io/journeys/fake-bcc/ *Fake Bcc* is an option available when you're adding a Blind Carbon Copy (BCC) address to your campaigns. It's checked by default. When checked, rather than sending a true BCC we'll send you a separate copy of the email (with a slightly modified subject line containing the user’s email address). This allows you to be copied on emails and open them and click links without worrying about affecting the user’s history. “Fake Bcc” is an option available when you’re adding a Bcc address to your campaigns. It’s checked by default. If this option is checked, rather than sending a true Bcc, we’ll send you a separate copy of the email (with a slightly modified subject line containing the user’s email address). This allows you to be copied on emails and open them and click links without worrying about affecting the user’s history. If you want to send a true Bcc, make sure this option is unchecked. If you’ve Bcc’d your CRM and you’re not receiving the emails, double check that “Fake Bcc” is unchecked. In the case of Hubspot, we’ll send a true BCC email; however, they will be marked as opened. There is no workaround for this. --- ## Create multiple from addresses URL: https://docs.customer.io/journeys/multiple-from-addresses/ You may have messages that should come from different email addresses depending on your use case—onboarding emails from customer success managers, newsletters from a general company address, etc. You can add multiple from addresses to fit your needs. Add multiple from addresses To add a new from address: Go to Settings > Workspace Settings and select Email. Find the sending domain you want to add the new address to and click Add from address. Enter the sender name and email address. After you add a new from address and the domain is verified, you can pick your new address using the drop-down next to the From label when creating an email. Add dynamic from addresses You may want to set a dynamic from address so your emails come from a person’s dedicated account manager, or someone that your audience expects to receive emails from. For example, if you assign an account_manager to your people, you can set a dynamic from address to use the account manager email address using liquid like {{customer.account_manager}}. To set a dynamic from address: Go to Settings > Workspace Settings and select Email. Scroll to the bottom of the page and locate Dynamic From Addresses. Click Add from address. Enter the display name and email address. Use liquid for the email address, determining the attribute or event property representing the from address when you select that sender (like {{customer.account_manager}}). You may also want to use a liquid if statement to set a fallback, in case people in your workspace don’t have the attribute representing your dynamic from address. {% if customer.account_manager != blank %}{{ customer.account_manager }}{% else %}accounts@example.com{% endif %}  Want to learn more? Check out these tips and tricks on using liquid with email addresses. Check where from addresses are used If you’d like to see where a specific from address is being used, search for it in your Message Library. This is a dedicated page in your workspace where you can locate specific templates by searching on keywords, as well as locate all message actions and broadcasts that are using a specific from address. You can also filter the results based on whether the message is actively sending or drafted. --- ## Welcome Email Copy URL: https://docs.customer.io/journeys/welcome-email-copy/ Got email writer's block? Here are copy templates for 5 different types of *Welcome emails* that can help get you started. The templates on this page refer to an imaginary productivity app called Prioritizer. General product / service welcome This is a basic welcome message from a business, app, or website that is sent after people sign up for an account. [greeting] Hi Bob!Welcome to Prioritizer! Thanks so much for joining us. You’re on your way to super-productivity and beyond! [who we are; our mission/ what we help you do; how it works] Prioritizer is a task management app that helps you focus on the important things in life by only allowing you to add 3 items a day. Set and track daily, weekly, and monthly priorities — and get the stuff that matters done. [what to do next that will set you up for success] Our number one tip to get the most out of Prioritizer is to download our browser extension and give it a whirl. [how it helps] It’ll make sticking to your priorities super simple and just a click away.[CTA](Download the extension) [open communication channel for questions, conversations, and help] Have any questions? Just shoot us an email! We’re always here to help. Cheerfully yours, The Prioritizer Team Grab your editable google doc template here. Newsletter subscription welcome: Whether the goal is to nurture leads, build an audience, engage existing customers, or promote sales, roll out a great welcome to your brand. [greeting]Hi Linda! Thanks so much for signing up for the Prioritizer newsletter! [set up expectations/make personal connection] You’re joining an amazing community of folks who love nerding out about productivity. [set up expectations re: frequency + type of content] You’re joining an amazing community of folks who love nerding out about productivity. Here’s what to expect: every Tuesday you’ll get an email with a collection of our best content with actionable advice and food for thought to help you get more done. [who we are / why company exists] Oh, by the way, let’s introduce ourselves before we get going. Prioritizer is a task management app that helps you focus on the important things in life by only allowing you to add 3 items a day. Our goal with the newsletter and our content is to create and share content that will help you be more effective with your time! [best content / freebies to build trust + affinity] As you wait for the next issue, check out some of our most popular posts. They’re a great place to get started. (links) [openness to conversation] We’d love to chat. Just hit reply to this email or any of our newsletters to get in touch with feedback, questions, or ideas for us! Have an awesome day! Louise, Prioritizer Marketing Manager [secondary call to action can go here, or a trust-winning reminder how easy it it is to unsubscribe] p.s. Want to check out our Prioritizer app? Head here to sign up for a free trial. Grab your editable google doc template here. Personal outreach welcome You may want to automatically send people a personal welcome message from someone in the company a few days after they sign up. This type of personal outreach invites communication and can be a helpful opportunity for new users to bring up issues. [greeting] Hi Gene! I’m Louise Belcher, CEO of Prioritizer. I’d like to personally thank you for signing up. Welcome aboard our journey towards smarter task management and happier productivity! [explain mission / common goal; personally connect with the reader] We started Prioritizer because we’ve always had trouble keeping a realistic to-do list that made sure important priorities got done. So much of your day escapes you because you end up doing reactive work that feels more urgent. [lead into what you’d like the reader to do next] Our mission is to help people keep on track with valuable goals. So I wanted to make sure you get the most out of your trial. [CTA] Check out our 5 top tips for success with Prioritizer. [open door to support and feedback] I’d love to hear whether you think Prioritizer helps fulfill your big goals or what we can do to improve. If you have any questions about getting started, I’m happy to help. Just reply to this email! Let’s do great things together! Louise Belcher [postscript call to action - great spot to get a little more salesy, offer an incentive, ask a specific question to elicit a response, or express extra personality] p.s. I love reading about productivity but hate wasting time finding quality stuff. What’s your favorite source of good reads? Grab your editable google doc template here.. Free sample welcome Companies, especially many SaaS businesses, offer free content, such as e-books, guides, and other resources in exchange for an email address. This is often your first point of contact with someone who’s just expressed interest in your product area. It’s just as crucial to make a memorable first impression in this case and set any expectations for marketing emails to follow. [greeting & gratitude] Hi Bob, Thanks so much for your interest in our 50 Most Successful Productivity Lifehacks book! [access to resource] Here’s your download link. [what’s next?] You’ll also start receiving weekly emails with thoughtfully human-curated content and our best blog posts full of actionable advice and food for thought to help you get more done. If that’s not your jam, no worries - just click the unsubscribe link. [explain who you are, make a short pitch to give context for your call to action] At Prioritizer, we’re only interested in lifehacks that make it easier to focus on accomplishing great goals — so much so that we made an app for it! We help you keep a realistic to-do list of priorities. [CTA] Check out our 30-day free trial!(Start being more productive) Always here if you have any questions, The Prioritizer Team [postscript - a nice spot to ask for a visibility boost for your content] p.s. Think this guide is helpful? Just click here to share with friends and colleagues. Grab your editable google doc template here. Learn more Read our full blog post on these 5 welcome email types for more context and examples Use the Princess Bride Copywriting Formula for a great welcome email message Personalize your welcome campaign by role --- ## Gmail Promotions URL: https://docs.customer.io/journeys/gmail-promotions/ Google has added annotation support for emails that appear in the Promotions tab of the Gmail app. This means that your promotional emails have more opportunities to make an impression beyond the subject and preheader. You can highlight incentives, share key information from your message, and stand out in the crowded inbox. Introduction Google has added annotation support for emails that appear in the Promotions tab of the Gmail app. This means that your promotional emails have more opportunities to make an impression beyond the subject and preheader. You can highlight incentives, share key information from your message, and stand out in the crowded inbox. 1. Choose your Annotations Supported annotations include images, your logo, promo codes, expiration dates on deals, and more. Choose the https links and copy that you want to use in your promotion. Check out Google’s best practices for more guidance. Logo URL Your logo should be a square- or circle-shaped image and be served over https instead of http. Product Image URL You’ll be able to add a single product image. Any image size will work and will be cropped automatically. GIF & WEBP images are not supported and will be filtered out. Sample image above is 538x138. Discount Offer Describe your discount, this will be shown as a badge (eg “25% off” or “free shipping”), shown in green above. Start Date The date your promotion begins. This must be a date on or before today. Expiration Date The date your promotion will expire. This must be a future date and will be used to calculate the countdown on the top right of the card. 2. Build Your Promotions Snippet After you’ve determined the promotion type, duration, and images to use, construct the snippet for your email using the template below. Copy this snippet into another document and replace the logo, description, subject line, discount code, start date, end date, and image with your own promotion details. <!--Google promotions via Schema.org microdata--> <div itemscope itemtype="http://schema.org/Organization"> <meta itemprop="logo" content="[---HTTPS URL FOR LOGO---]" /> </div> <div itemscope itemtype="http://schema.org/EmailMessage"> <meta itemprop="subjectLine" content="[---ALTERNATE SUBJECT LINE---]" /> </div> <div itemscope itemtype="http://schema.org/DiscountOffer"> <meta itemprop="description" content="[---DISCOUNT DESCRIPTION---]" /> <meta itemprop="discountCode" content="[---DISCOUNT CODE---]" /> <meta itemprop="availabilityStarts" content="[---START DATE FORMAT YYYY-MM-DDTHH:MM:SS-05:00---]" /> <meta itemprop="availabilityEnds" content="[---END DATE FORMAT YYYY-MM-DDTHH:MM:SS-05:00---]" /> </div> <div itemscope itemtype="http://schema.org/PromotionCard"> <meta itemprop="image" content="[---HTTPS URL FOR PROMO IMAGE---]" /> </div> Here’s the code of the example card shown in the introduction above. <div itemscope itemtype="http://schema.org/Organization"> <meta itemprop="logo" content="https://customer.io/wp-content/uploads/2019/02/thumbnail-circular.ico" /> </div> <div itemscope itemtype="http://schema.org/EmailMessage"> <meta itemprop="subjectLine" content="[Important] Upgrade to an annual subscription" /> </div> <div itemscope itemtype="http://schema.org/DiscountOffer"> <meta itemprop="description" content="20% off" /> <meta itemprop="discountCode" content="ANNUAL20" /> <meta itemprop="availabilityStarts" content="2019-12-01T08:00:00-05:00" /> <meta itemprop="availabilityEnds" content="2019-12-30T23:59:59-05:00" /> </div> <div itemscope itemtype="http://schema.org/PromotionCard"> <meta itemprop="image" content="https://customer.io/wp-content/uploads/2019/02/1200x630-1-1.png" /> </div> 3. Test Your Snippet Once you have your snippet ready, test it with Google using their preview tool to confirm that it’s showing the right information. 4. Add the Snippet to Your Email Once your promotions card looks like the way you want it in the Google preview tool, you can add it directly to any email as HTML, using your preferred editor. Add Promotions using the Drag-and-Drop Editor Within the Drag-and-Drop editor, click and drag a new HTML content block to the top of your email. Replace the default contents in the HTML code block with your snippet from above. It won’t affect the design or layout of your email. Add Promotions using the using HTML Editor If you’re using the HTML editor, all you need to do is copy your snippet into the of your email and save. If there’s no tag, then just paste the code at the top, above the rest of the content. Add your email content as you would normally below the snippet. Add Promotions using the Rich-Text Editor For the Rich-Text editor, toggle to HTML view in your editor and copy your snippet into the of your email. If there’s no tag, then just paste the code at the top, above the rest of the content. Toggle HTML off and you should be able to edit the rest of your email as normal. 5. Send a Test Once your email is ready, send a test to yourself using the Actions dropdown on the top right of the editor. If you don’t see the test email, make sure you’re using the Gmail mobile app and on a personal non-G-Suite account. Google recommends some additional troubleshooting tips here. 6. Send Your Email! Once you’ve successfully tested the email, you’re ready to send it to your customers! --- ## Send data from Customer.io to Salesforce, Highrise or another CRM URL: https://docs.customer.io/journeys/bcc-salesforce-or-your-crm/ If you use a CRM or sales software like Salesforce, Highrise, Capsule CRM, SugarCRM, Zoho, Pipedrive and many others, you can often send a BCC to update a contact or lead with new information. With Customer.io, you can set up a BCC on individual emails to send those details to your CRM as needed. For each email you want to BCC to your CRM, head into the Composer: Add your CRM’s dropbox address in the BCC field Uncheck the “Fake BCC option” What does “Fake BCC” do? If this option is checked, instead of sending a true BCC, we’ll send you a separate copy of the email (with a modified subject line containing the user’s email address). This allows you to be copied on emails, open them and click links without worrying about affecting the user’s history. If you want to send a true BCC, make sure this option is unchecked. If there are any CRMs not supported by this method, please let us know. --- ## Code editor: send Trustpilot reviews URL: https://docs.customer.io/journeys/bcc-trustpilot/ Send Trustpilot reviews as emails using our code editor. If you want to send Trustpilot reviews through Customer.io, you can either integrate with Zapier or follow the steps below. In this guide, you BCC Trustpilot in an email and include certain metadata, no formal integration required.  Use the code editor You must create the email with our code editor to ensure the metadata is not stripped from the email. Obtain your unique BCC address from Trustpilot. In Customer.io, drag an email block onto your campaign or API-triggered broadcast. Or select Email for a newsletter or transactional message. Click Add Content. Under Start from scratch, choose Code, or under Start from existing email, click Code and choose an email made with the code editor. Add the BCC address to your email’s envelope. If you send the email content in a Transactional API call, your developer needs to add the email address to thebcc (or fake_bcc) field. Add this tag <script type="application/json+trustpilot"> to pass along the metadata Trustpilot needs. Add the data you want to send, formatted as JSON. For instance, you could send customer data using liquid like {{customer.latest_booking}}, where latest_booking is a property you store in Customer.io. If you pass the email content in a Transactional API call, you need static values; liquid will not get evaluated. After Trustpilot receives the message (on BCC), they send the review request to your customer. --- ## Get started URL: https://docs.customer.io/journeys/push-getting-started/ This page can help you understand what you need to do before you send your first push notification. You may need to integrate your app with Customer.io, add your push certificates to your workspace, or register devices. Before you begin Setting up push notifications requires development work in your mobile app, access to your push provider, and then you’ll need to compose messages in Customer.io. This may mean that you’ll need to coordinate work across a few different people in your organization to get up and running! Your path in the setup process might change depending on whether you’re a developer integrating with Customer.io or a marketer who wants to send messages. Use the chart below to better understand what you, or other members of your team, must to do before you can send push notifications to your app’s users. Some of the squares below link directly to relevant documentation!  If you’re just getting started, use our SDKs! Our SDKs can greatly simplify your development process. Even if you want to add some custom-developed features, our SDKs provide a foundation for your app’s integration with Customer.io. flowchart LR a{Where do I start?} a--->|I'm a marketer| f a-->|I'm a developer| b{Integrate your mobile app} subgraph Developers b-->d[Use our SDKs] b-->e[Integrate directly] end d-->f[Authorize push provider] subgraph Developers or Marketers f end e-->f f-.->|no images or links|c[Send simple push] f-.->|add images and links|g[Send rich push] subgraph Marketers c g end click f "/push-developer-guide/#generate-certificates" _self click d "/sdk/" _self click e "/push-developer-guide/" _self click c "/send-push/" _self click g "/push-custom-payloads/" _self Plan your implementation Before you get started with push notifications, you should map out the things you want your push notifications to do so that you can develop all the right features in your app. We encourage you to use our SDKs, which can help you quickly take advantage of Customer.io in your app and are being actively developed to incorporate new features. A simple push notification, which most integrations support by default, contains a title and a body. However, you may want to send your audience images, link them to pages in your app (commonly known as “deep links”), or group notifications for different purposes in your app. All of these features may take additional development, and require you to use custom payloads. Set up your mobile app Before you can send push notifications, you need to make sure that your app is set up to work with Customer.io. In general, we recommend that you use our SDKs to simplify your development process and provide a standard avenue for us to support your app and use cases. Whether you use our SDKs or write your own integration, your app must do the following things. Identify people and register device tokens for people. In general, this means that your app requires people to log in, or otherwise make themselves known to you; a device token cannot be “anonymous” in Customer.io. Set up your app to receive push notifications. (Recommended) Set up your app to report push metrics back to Customer.io. Our SDKs have functions to do this somewhat automatically. (Recommended) Send events back to Customer.io representing activity you want to track or trigger campaigns. (Recommended) Set up your app to interpret rich push notifications—messages that include images, links, sounds, etc. Pick your push provider(s) Before you can send a push notification, you need to authorize Customer.io to send notifications through your push notification service(s). There are two push notification services, and they allow third parties (like us!) to send notifications to apps installed on iOS and/or Android-based devices. Depending on your app development path, you may use one or both services. As a part of the setup process, you’ll provide Customer.io with your push provider’s certificate. We’ll show you how to get your push certificate and upload it to Customer.io in our push developer guide. If you don’t know which service(s) you use, or you don’t have access to your services, you may need to talk to a developer on your team to gain access to your push service credentials. Service Supports Required in Customer.io Apple Push Notification service .p8 certificate, App ID, App key, Bundle ID Firebase Cloud Messaging .JSON key You can send push notifications to iOS devices using either Apple’s Push Notification service (APNs) or Google’s Firebase Cloud Messaging (FCM) service. To authorize your Customer.io workspace to send notifications through APNs, you’ll need to upload your APNs .p8 certificate and provide your credentials or upload your .JSON to iOS devices over Apple’s Push Notification service, you’ll need to upload your APNs .p8 certificate and enter your Apple Developer Key ID, Team ID, and Bundle ID. Find your device token for testing As you get started with push notifications, you’ll likely want to test your implementation by sending notifications to yourself or a dedicated test device. To find the device token of your test user: Go to People, click your test user, and go to the Devices tab. Hover over a device and click to copy the token to a push notification. --- ## Integrate your app URL: https://docs.customer.io/journeys/push-developer-guide/ Before you can send push notifications, you need to set up your workspace to send push notifications through your push provider and configure your app to receive push notifications. You can do this with or without our SDKs providing flexibility in how you send and receive push notifications. When you integrate with us, you have some choices depending on the push platform you use (APNs, FCM) and whether or not you want to use our SDKs—though we strongly recommend that you use our SDKs if possible to simplify your integration process. But, before you can send and receive push notifications with Customer.io, you must: Generate and add certificates for your push service, Apple Push Notification service (APNs) and/or Firebase Cloud Messaging (FCM). Configure push in your app. You can do this with our SDKs or integrate directly with our API. Our SDKs provide a ready-made interface to configure push. If you want to integrate Customer.io without using our SDKs, you’ll need to write your own code to: Identify people and set their device token Send events representing your users’ actions back to Customer.io Send yourself a push notification to test your implementation. Generate and add push certificates Before you can send a push notification, you need to generate certificates from Apple’s push notification service (APNs) and/or Google’s Firebase Cloud Messaging service (FCM) and add them to your workspace. This authorizes Customer.io to send notifications to your app through your push provider(s). Firebase Cloud Messaging (FCM) setup To send notifications through Firebase Cloud Messaging, you’ll need to upload a JSON server key that authorizes Customer.io to send notifications. If you already have your key, skip ahead to step 4 below. The JSON server key must match the Sender ID that you use in your Firebase project. Log into the Firebase Console for your project. Click in the sidebar and go to Project settings. Go to Service Accounts and click Generate New Private Key. Confirm your choice and download the credential file. In Customer.io, go to your workspace’s Settings > Workspace Settings and click Settings next to Push. For Android, click Enable, and then click Choose file… to select the JSON server key you generated in previous steps. When you’re done, your workspace is setup to send notifications through Firebase to Android devices. Send to iOS via Firebase After you’ve set up Firebase for Android, you can enable Firebase as your iOS push-provider. In Customer.io, go to Settings > Workspace Settings and click Settings next to Push. For iOS, click Enable, and select the Firebase Cloud Messaging (FCM) option. Apple Push Notification service (APNs) setup You can send push notifications to iOS devices using the Apple Push Notification service (APNs). To authorize your Customer.io workspace to send notifications to iOS devices over Apple’s Push Notification service, you’ll need to upload your APNs .p8 certificate and enter your Apple Developer Key ID, Team ID, and Bundle ID. If you already have your p8 certificate, you can skip ahead to step 5 below. Log into your Apple Developer account and go to Certificates, Identifiers & Profiles > Keys. Click the blue button to create a new key. Click Apple Push Notifications service (APNs) and enter a name for the key. Click Continue and then Register to create the key. Download your keys and put it somewhere you’ll remember. You can only download your key once! In Customer.io, go to your workspace’s Settings > Workspace Settings and click Settings next to Push. Click Enable under iOS, and select the Apple Push Notification service (APNs) option. Click Choose file… and upload your .p8 certificate. Enter your Key ID, Team ID, and Bundle ID. You can find these in your Apple Developer Account. (Optional) Enable the Send all push notifications to sandbox option. Your iOS certificate may have both sandbox and production environments; this option sends push notifications to both environments.  We recommend creating a separate workspace for your sandbox environment. Click Save Changes Rotating your APNs credentials You provide credentials for your developer account and app bundle. If you need to move your app to a new developer account or bundle, you’ll need to generate a new .p8 certificate and update your credentials in Customer.io. If you’ve already rotated your credentials and push notifications are failing, stop and restart any campaigns, broadcasts, and/or transactional messages that send push notifications. Stopping and restarting establishes new connections to APNs using your updated credentials so push notifications will send properly. Stop any campaigns or active messages (Newsletters, Broadcasts, or Transactional messages) that send push notifications. If you don’t, push notifications will fail when you update your credentials. You can search for push notifications and trace those to relevant campaigns on the Content > Message Library page. Generate your new .p8 certificate in your Apple Developer Account. In Customer.io, go to Settings > Workspace Settings > Push > iOS. Click Edit authorization and set your new credentials. Click Save Changes. Restart the campaigns, broadcasts, and/or transactional messages you. They’ll begin sending using your new credentials. Configure push without our SDKs In general, we suggest that you integrate using our SDKs, which provide ready-made methods to interpret messages, identify devices, and send client-side events. However, if you want to write your own integration with Customer.io, we’ve provided some code samples below to help you get started with push notifications. You’ll need to familiarize yourself with your integration platform to understand the range of configuration options that you have available. If your mobile app is already set up to receive push notifications, you won’t need to make many changes to work with Customer.io. But, you should be aware that we send two custom parameters in the payload of push notification: CIO-Delivery-ID & CIO-Delivery-Token. These are used for sending Open events back to us. iOS App Setup The following shows an example to help you register for remote notifications in iOS. Open the project editor, and go to the Signing & Capabilities tab. Click Capability and and select Push Notifications. Update the didFinishLaunchingWithOptions callback in the AppDelegate.swift to request the device token. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Configure user interactions self.configureUserInteractions() // Register with APNs: this might change based on iOS versions you want to support UIApplication.shared.registerForRemoteNotifications() return true } Add the following methods to handle the behavior when the token request succeeds or fails. // Handle remote notification registration. func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data){ self.enableRemoteNotificationFeatures() // Convert token to string let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)}) // Forward the token to your provider, using a custom method. self.forwardTokenToServer(token: deviceTokenString) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { // The token is not currently available. print("Remote notification support is unavailable due to error: \(error.localizedDescription)") self.disableRemoteNotificationFeatures() } Handle the notification. // Push notification received func application(_ application: UIApplication, didReceiveRemoteNotification data: [AnyHashable : Any]) { // Print notification payload data print("Push notification received: \(data)") // Customer.io push notifications include data regarding the push // message in the data part of the payload which can be used to send // feedback into our system. var deliveryID: String = ""; if let value = data["CIO-Delivery-ID"] { deliveryID = String(describing: value) } var deliveryToken: String = ""; if let value = data["CIO-Delivery-Token"] { deliveryToken = String(describing: value) } } Android App Setup The following shows an example of how to register with Firebase in Android. The easiest way to integrate Firebase Cloud Messaging into your Android app is to use the Firebase Assistant Tool in Android Studio. It walks you through the required steps. Add the Firebase SDK as a dependency into your build.gradle file. compile 'com.google.firebase:firebase-messaging:12.0.1' Create a new service that extends the FirebaseInstanceIdService class. Within this class you need a onTokenRefresh function in which you can use FirebaseInstanceId.getInstance().getToken() to retrieve the device message token. Note that this function is only called when the device token changes, so if you need access to it at other activities you may want to save it in a variable for later use. public class FirebaseIdentifierService extends FirebaseInstanceIdService { public FirebaseIdentifierService() { } @Override public void onTokenRefresh() { // Get updated InstanceID token. String refreshedToken = FirebaseInstanceId.getInstance().getToken(); Log.d("firebase", "Refreshed token: " + refreshedToken); } } Update the AndroidManifest.xml to add the service. <service android:name=".FirebaseIdentifierService" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/> </intent-filter> </service> Now that you can generate tokens, you can handle incoming messages. Create a new service, extending FirebaseMessagingService, to handle messages. public class NotifierService extends FirebaseMessagingService { private static final String TAG = "messaging"; public NotifierService() { } @Override public void onMessageReceived(RemoteMessage remoteMessage) { // Check if message contains a data payload. // You can have data only notifications. if (remoteMessage.getData().size() > 0) { Log.d(TAG, "Message data payload: " + remoteMessage.getData()); // Customer.io push notifications include data regarding the push // message in the data part of the payload which can be used to send // feedback into our system. String deliveryId = remoteMessage.getData().get("CIO-Delivery-ID"); String deliveryToken = remoteMessage.getData().get("CIO-Delivery-Token"); } // Check if message contains a notification payload. if (remoteMessage.getNotification() != null) { handleNotification(remoteMessage.getNotification()); } } private void handleNotification(RemoteMessage.Notification notification) { Log.d(TAG, "Message Notification Body: " + notification.getBody()); } } Update the AndroidManifest.xml one more time to add the new service. <service android:name=".NotifierService" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT"/> </intent-filter> </service> Identify Device Tokens Before you can send a push notification, you need to identify the person—and their device—using your app. Typically, you should send this information every time someone launches your app on their client device. We recommend that you make calls using one of our client libraries from your backend system rather than directly from your app. If you can’t do this, and you need to make calls from within your app, you’ll want to make sure that they run asynchronously. To add a device to the customer profile, call the following endpoints in either our Pipelines API (recommended) or our Track API. If your account is in the EU region, make sure you use the appropriate -eu URLs. Pipelines API (Recommended) Pipelines API (Recommended) Method: POST URL: https://cdp.customer.io/v1/track JSON Payload: { "type": "track", "event": "Device Created or Updated", "userId": "42", "properties": { "device": { "token": "string", "type": "ios" } }, "timestamp": "2021-07-14T19:10:25.000Z" } Classic Track API Classic Track API Method: PUT URL: https://track.customer.io/api/v1/customers/{customer_id}/devices JSON Payload: { "device": { "id": "messaging token", "platform": "ios/android", "last_used": UNIX timestamp when the device was used } }  We store up to 25 devices If you add a 26th device, we’ll remove the device with the oldest last_used timestamp. You can remove a device from a person’s profile as well. A person can have up to 25 devices, but you should prune device tokens when your push provider invalidates them. Pipelines API (Recommended) Pipelines API (Recommended) Method: POST URL: https://cdp.customer.io/v1/track JSON Payload: { "type": "track", "event": "Device Deleted", "userId": "42", "properties": { "device": { "token": "string", "type": "ios" } }, "timestamp": "2021-07-14T19:10:25.000Z" } Classic Track API Classic Track API Method: DELETE URL: https://track.customer.io/api/v1/customers/{customer_id}/devices/{token} Push payloads sent from Customer.io We have a simple user interface for sending push notifications, including images and links. However, if you want to use our UI, you should either: Integrate with our SDK(s) Write your own integration, expecting to handle payloads in the following format. If your app is already set up to receive push notifications in different formats, you can send a custom push payload instead. APNs (iOS) APNs (iOS) Your Image URL and Deep link or web URL go inside a CIO.push object. Custom Data resides in the payload but outside the CIO and aps objects. { "CIO": { "push": { "link": "string", "image": "string" } }, "aps": { "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" }, "sound": "default" }, //custom keys go here "customKey": "a custom key" } FCM (iOS) FCM (iOS) The CIO.push object contains the link and image fields from notification. Custom Data resides in the payload but outside the CIO and aps objects. { "message": { "apns": { "payload": { "CIO": { "push": { "link": "string", "image": "string" } }, "aps": { "alert": { "title": "string", "body": "string" }, "sound": "default" }, // custom keys go in the payload but outside the CIO and aps objects "customKey1": "custom keys", "customKey2": "another custom key" } } } } FCM (Android) basic push FCM (Android) basic push For a basic push, the push title and body reside in the message.notification. If you add an image, link, or custom data, we move the entire payload moves under the message.data object. { "message": { "notification": { "title": "string", //(optional) The title of the notification "body": "string", //The message you want to send } } } FCM (Android) rich push FCM (Android) rich push For a basic push, the push title and body reside in the message.notification object—similar to Firebase’s standard notification payload. If you add an image or link, the entire payload moves under the message.data object. { "message": { "data": { "title": "string", //(optional) The title of the notification "body": "string", //The message you want to send "image": "string", //https URL to an image you want to include in the notification "link": "string", //Deep link in the format remote-habits://deep?message=hello&message2=world // other "custom keys" are sent inside this object } } } Send events for key customer actions You can send events representing your audience’s activities in your app. When you send an event, you’ll set the name of the event: this is how you’ll find your event when creating segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. or campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria.. You can also send “screen view” events, representing the screens that your audience visits in your app. See screen view events for more information. Pipelines (Recommended) Pipelines (Recommended) The userId in the request can be either an email address or an ID, associating the event payload with that person. But you can also send anonymous events. Method: POST URL: https://cdp.customer.io/v1/track JSON Payload: { "event": "set favorites", "userId": "42", "properties": { "fav-food": "pizza" }, "timestamp": "2021-07-14T19:10:25.000Z" } Classic Track API Classic Track API The identifier in the request can be either an email address or an ID, associating the event payload with that person. But you can also send anonymous events. Method: POST URL: https://track.customer.io/api/v1/customers/{identifier}/events JSON Payload: { "name": "set favorites", "timestamp": 1638393518, "data": { "fav-food": "pizza" } }  We track push metrics using a different endpoint To report push opened, converted, and delivered metrics for a specific push notification, see push opened events. Screen view events Screen events track the screens that people visit in your app. You can use screen view data not only to track the parts of your app that people find most engaging, but also to create segments or tailor campaigns to people who use specific parts of your app. If you use our Pipelines API, which we recommend, you can send screen view events with a screen call. In our classic Track API, you can send screen view events with a type of screen. When you send a screen view event, the name should be the name of the screen a person viewed. You can also send additional properties in the event—these are things you might want to reference using 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}}. in your campaigns and messages. Pipelines (Recommended) Pipelines (Recommended) The userId in the request can be either an email address or an ID, associating the event payload with that person. But you can also send anonymous events. Method: POST URL: https://cdp.customer.io/v1/screen JSON Payload: { "userId": "97980cfea0067", "name": "mlb-scores", "properties": { "fav-team": "giants" } } Classic Track API Classic Track API The identifier in the request can be either an email address or an ID, associating the event payload with that person. But you can also send anonymous events. Method: POST URL: https://track.customer.io/api/v1/customers/{identifier}/events JSON Payload: { "name": "mlb-scores", "type": "screen", "timestamp": 1638393518, "data": { "fav-team": "giants" } } Track push opened metrics You can report three metrics from the client side to help track the performance of your push notifications: delivered, opened, and converted. If you use our iOS or Android SDK, we can automatically track delivered and opened metrics for you. Otherwise, you’ll need to send events to Customer.io to report metrics from your messages. When we deliver a push notification, we include CIO-Delivery-ID and CIO-Delivery-Token properties. You must include these properties in your delivered, opened, or converted events to tell us which message corresponds to the metric you’re reporting. Pipelines (Recommended) Pipelines (Recommended) Method: POST URL: https://cdp.customer.io/v1/track JSON Payload: { "event": "opened", "userId": "42", "properties": { "delivery_id": "CIO-Delivery-ID from the notification", "device_id": "CIO-Delivery-Token from the notification" }, "timestamp": "2021-07-14T19:10:25.000Z" } Classic Track API Classic Track API Our classic Track API uses a different endpoint for push metrics than other types of events. Method: POST URL: https://track.customer.io/api/v1/push/events JSON Payload: { "delivery_id": "CIO-Delivery-ID from the notification", "event": "opened", "device_id": "CIO-Delivery-Token from the notification", "timestamp": UNIX timestamp when the event occurred } Send yourself a test notification Verify that you’ve configured everything correctly by sending yourself a test push notification. There are a couple of ways to make sure that your initial configuration is working. You can send a single test message from the settings, or (if you have an iOS app) use a separate sandbox environment for your tests. You can send yourself a test push from the Push Notification Settings page after you’ve uploaded your certificate(s). Enter your device token. and you’ll hopefully get a test push to that device with the following content: “If you’re reading this, it means your integration with Customer.io is working!” and you’re ready to start adding push notifications to your campaigns. You can find your device token on the People page; click the person you want to send a test message to and go to the Devices tab. --- ## Migrate from another provider URL: https://docs.customer.io/journeys/push-migration/ When you move to Customer.io from another push provider, you'll need to import your audience and help them upgrade to a version of your app that uses Customer.io. How it works While you can export your audience from your old provider, push notification delivery is still dependent on the SDK(s) in your app. You can’t send a push from Customer.io to someone using a version of your app that doesn’t use Customer.io! So, when you move to Customer.io from another push provider, you’ll need to import your audience and help them upgrade to a version of your app that uses Customer.io before you can send them push notifications. This means that you won’t be able to finish your migration until a significant portion of your audience has upgraded to a version with Customer.io. You’ll likely need an interim period when you maintain audiences in both push providers while you push your audience to update your app. The steps below are a general guide, but we’ve provided more specific instructions to export and import data from some specific providers as well. 1. Integrate with Customer.io You won’t be able to send push notifications to people through Customer.io until they upgrade to a version of your app integrated with Customer.io! You’ll need to remove your old provider’s SDK, add ours, and test your app before you release it to your audience. This is probably the lengthiest and most involved part of the process to migrate from another provider, so you’ll want to start with your integration. See our mobile SDKs for more about integrating with Customer.io. 2. Export your audience (not their device tokens) When you’re ready to start a migration campaign, you’ll export your audience from your old provider and import them into Customer.io. You won’t export device tokens because you’ll register tokens for people when you identify them with the Customer.io SDK; this will be how you count members of your audience who’ve updated your app! You may need to reformat some of your audience information to fit Customer.io before you import it. For example, OneSignal typically exports attributes and column titles with capital letters. If you maintain attributes in a different format (like snake_case), you’ll need to reformat your exported data to match the way you want to store it in Customer.io. You can also add people using our API. So, if you export people from your service programmatically, you can easily re-shape your exported data to fit Customer.io and then import it via our APIs. 3. Import your audience into Customer.io Depending on how you exported people, you can import them again through a CSV or our API. You’ll need to make sure that it fits Customer.io—see our API or CSV import documentation for more information. But, in general, you’ll need to make sure that your CSV contains a value mapped to id or email to identify the person. All the other values will represent attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for people—which you’ll use to segment them, personalize messages with 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}}., and more. 4. Create a segment to monitor your audience’s progress In Customer.io, you can create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. to compare your mobile audience in Customer.io to the number of people you exported from (or store in) your old provider. So, as the Customer.io segment approaches the number of exported people, you can safely sunset your old provider. Luckily, we already have a segment that might work for you: the Have a mobile device segment checks for people who have a device. As you register devices from the version of your app integrated with Customer.io, your segment will grow. If you already have device users in Customer.io, and you just want to segment the people you migrated from your former platform, you may want to create a new segment that a Device exists for people you added from your previous platform. You’ll want to set a new attribute to make this easier—like from_<old_platform>. 5. Encourage your audience to update your app You should send notifications to your audience through your old provider encouraging them to update your app. As people update to a version of your app containing the Customer.io SDK, you’ll see the Customer.io segment you created in earlier steps start to fill up. You’ll be able to send these people—your Customer.io app users—notifications through Customer.io. As the segment you created in the previous step approaches the number of people you exported from your old provider, you can safely sunset your old provider. Export your audience from OneSignal To migrate your audience from OneSignal to Customer.io, you’ll need to export your audience from OneSignal and import them into Customer.io. You don’t want to export their device tokens because you’ll register tokens with Customer.io when you identify them with the Customer.io SDK; this will be how you count members of your audience who’ve updated your app! In OneSignal, go to Audience > Subscriptions. Select a segment if your mobile users are confined to a specific segment. Otherwise, don’t select a segment to export all your users. Make sure the columns that you want to export are present. Leave out device information like the Push Token column. Click Export and OneSignal will send you an email with your CSV. Now you’re ready to import your audience to Customer.io! Export your audience from Braze Braze doesn’t let you export all of your users directly. You’ll need to create a segment in Braze and export that segment. Braze exports are limited to 500,000 users at a time, so you may need to generate and export multiple segments. After you’ve made your Braze segment, you can go to Segments in Braze and click the settings dropdown and select the CSV Export User Data option. Now you’re ready to import your audience to Customer.io! Import your audience from another provider You might need to reformat your exported data to fit Customer.io. For example, Braze exports attributes and column titles with capital letters; if you maintain attributes in a different format (like snake_case), you’ll need to reformat your exported data to match the way you want to store it in Customer.io. In Customer.io, go to People, click Add People and select Import a CSV. Set up your Import: How do you want to identify people? If your id in Customer.io doesn’t match the External User ID from OneSignal, you should use email. If you’re importing from OneSignal and use the same value for IDs in Customer.io that you use in OneSignal, you should map the external_user_id column to id in Customer.io when you import you audience. Set Do you want to add new people? to Yes. Set Do you want to update existing people? to Yes. Set What should we do with empty values? to Ignore them. Map fields from your CSV to attributes in Customer.io. You’ll need to map at least one identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—an ID or email. Otherwise, you may want to map columns in your import to specific attributes if you use different formats for values in OneSignal and Customer.io (e.g. you use firstName in OneSignal and first_name in Customer.io). Review your import for errors and warnings. Click Complete import. The import process takes approximately one minute per 20-30 thousand rows. You can leave the page, and we’ll send you an email when your import is finished. You can also check the status of your import on the Imports page (under Configure data, click More). --- ## Registering device tokens URL: https://docs.customer.io/journeys/device-tokens/ Device (push) tokens are how we identify devices in your audience. This page provides information about how to register tokens, associate them with people in your workspace, and what to do when they expire or are otherwise no longer useful. What is a device token? Device tokens—sometimes known as push tokens in other products or push services—are unique, anonymous identifiers for the app-device combination that are issued by push notification services: they’re basically device addresses, so you can send push notifications to your app’s users. If a person doesn’t have a device token, they can’t receive push notifications and their device won’t appear in Customer.io. In Customer.io, we reflect device tokens as devices (or device_id in our APIs and SDKs). The key thing is that a device token by itself is anonymous. You have to identify a user to associate the device token with a person in Customer.io, so you can send them messages. The device lifecycle In general, the token lifecycle begins when your app registers for a token. That’s when push services like Firebase Cloud Messaging (FCM) and Apple’s Push Notification service (APNs) issue tokens to a device. sequenceDiagram participant a as App User participant b as Push Provider participant c as Customer.io a->>a: User opens app note over a, c: User is anonymous and cannot receive push a->>b: Fetch device token a->>c: User logs in (identify) c->>a: Associate token with user (automatic) note over a, c: User is identified and eligible for push c->>b: Send push b->>a: Send push a->>c: User logs out (clearIdentify) c->>c: Disassociate token note over a, c: User is anonymous again, token is invalid When a device gets a token, it’s anonymous until you identify the user—like when they log into your app or give their email address. The token will be valid during the user’s session in your app, but it can become invalid for any number of reasons. The most obvious reason is when a user logs out of your app. But, beyond that, push services can invalidate tokens for any reason on their end. So, whenever a person opens your app, you should make sure that you register for a device token to ensure that your user has a valid token. Invalid tokens are not reusable. Once a token is invalid, it won’t become valid again. Customer.io does some work to prune invalid device tokens to help you maintain a tidy workspace. For example, when a person logs out of your app and you run our SDKs’ clearIdentify() method, we’ll remove their device token. Or if you send a push notification to a token that’s no longer valid, we’ll know that the token is bad and remove it from the person’s profile in Customer.io. flowchart LR z(Person opens app)-->b subgraph y [Person is anonymous] b(Register for token) end subgraph x [Person is identified/eligible for push] b-->|person logs in or otherwise identifies themselves|d(Person becomes eligible for push) d-.->|Person closes app|f(Person is still eligible for push) end d-->|person logs out|e(Person not eligible for push) When should I register for a device token? You should register for a device token whenever someone opens your app‚ even if a person already has a device token associated with them or you haven’t yet asked them for permission to receive push notifications. If someone already has a token, push services (FCM or APNs) can invalidate device tokens for any number of reasons at any time. Registering for a device token gets a new token if a current token doesn’t exist or is invalid. Otherwise, it ensures that the current token is valid. You should also call the registration function after a person grants authorization to receive push notifications. Remember, even if you register for a token, you still need to identify someone before you can send them notifications. Identifying a person associates a token with a person, so you can send messages to that device. A person can have multiple devices, and each of a person’s devices can receive messages. Android device tokens Android 13 and Later requires you to ask for permission to send push notifications. Your app’s users are assigned a token when the user grants permission to receive notifications. Android 12 and Earlier assume that users permit to receive push notifications by default. You can request a push token by default. iOS device tokens You should register for a device token at app startup, but iOS users must grant permission to receive push notifications. If you use our iOS SDK, you’ll need to add our registration method. If you use our other iOS-supporting SDKs, they’ll do this for you. Beginning in iOS 12, you can request provisional authorization and send provisional push notifications—messages that don’t have an alert or play sounds. Our native iOS SDK supports provisional push notifications, but our other iOS-supporting SDKs do not support provisional push. You still need to identify people Remember that device tokens are anonymous on their own. Customer.io doesn’t support anonymous push notifications, so you can’t simply target any individual device token in your workspace. You still have to identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. someone to associate a token with a person, so you can send them messages. This ensures that you send messages to the right people in your audience. Can I target a specific device? You can target specific devices that belong to people in your workspace with: Transactional Push notifications, because these are one-to-one messages; they’re intended to respond to an event from an individual device with a notification that the user implicitly expects. The Custom device token option when you send an event-triggered campaign. This can be helpful if you store the current device token in your event data—so you can send a message directly to the device that triggered an event.  You must register devices to send push notifications While you can use the custom device token option to target a user by event property or profile attribute, the device you target must have been registered by our SDK to send your notification. If you add the device token as a profile attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. or event property without registering the device token from your app, we won’t be able to send your notification. When you send a push notification as a part of non-event-triggered campaigns, you can target your audience’s Last Active Device, which is likely the device that you want to target anyway. If you set up a campaign and don’t use the Last Active setting, we’ll send your push notification to all of your audience’s push-eligible devices. What to do when someone logs out of your app When a person logs out of your app, you should use our SDKs’ clearIdentify method to disassociate the device token from the user in the current session. This ensures that you respect the privacy of your users and don’t inadvertently send push notifications to the wrong user (if the device has multiple users). What happens to old/invalid device tokens? Device tokens are ephemeral: they become invalid when a person logs out of your app, and a push service can change them at any time. Beginning January 31, 2023, when a device token becomes unregistered, it is invalid and will never become valid again; if a person logs into your app again, they’ll get a new device token. To prevent invalid tokens from stacking up on people in your workspace, we automatically remove devices when they’re invalidated by your push services. If, when you send a push notification, your push service (FCM or APNs) responds with an unregistered message, we’ll automatically remove the token from your workspace. flowchart LR a[send push]-->b{Is the push sent?} b-->|yes|c[display push] b-.->|no, device token is unregistered|d[delete device] class d mermaid-error classDef mermaid-error fill: #ffedf0,color: #69002c, stroke:#69002c Device _last_status Devices have a status (represented by _last_status in the API), that we update automatically when you identify someone or send them a push notification. This status represents the state of the last push notification sent to the push notification service, and can be one of unused, sent, bounced, or suppressed. The unused status means that you either haven’t sent a push notification to a device, or the pushes you’ve sent to the device failed. When you send a push notification to a device, we update the status of that device as reported by the push notification service (FCM or APNs). sent: The push was accepted by the push notification service. bounced: The last push notification bounced. This means that the user uninstalled your app, or the device was unable to receive your notification because it device was off, no longer exists, was in airplane mode, etc. suppressed: The last two push notifications bounced. We will stop attempting to send push notifications to this device. If the last status of the device is bounced or suppressed, and you identify the user with the same device token, we’ll clear this status, and it will return to “unused” (or, in the API, an empty string). You should refresh device tokens and identify people when they open your app or log in to help refresh this status. flowchart LR a[person opens app or logs in] --> |"identify call"|h{has device been sent a successful push?} h -->|yes|d[last status is sent] h -->|no|i[last status is unused] d --> e[send push] i --> e e --> k{did push provider accept the push?} k --> |yes|b[last status is sent] k --x |no|c{is this the first failed push?} c --> |yes|l[last status is bounced] c --> |no|m[last status is suppressed]  Do not segment your audience using last status yet Segmenting on devices today selects a person, not a device, and a person can have multiple devices. So, if you segment based on device values like last status, your segment may inadvertently include or exclude people (and, by extension, their devices). We’re working on a solution to help you target devices specifically. But, until then, you probably don’t want to use last status in segments. How do I know if someone is opted into push notifications? Our SDKs record whether or not a device is opted-into notifications with the push_enabled attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. This attribute is set to true if a customer has enabled push notifications for your app, and false if they disabled notifications. The push_enabled attribute does not actually determine whether or not a person can receive push notifications. It simply records their status the last time they opened your app. A person’s opt-in status is set at the operating system level and logged by your push provider (APN or FCM)—and all of that happens outside your app. While you can use this attribute to get a general idea of which devices and people are opted into push notifications, it may not give you a perfectly accurate picture of your app audience in some situations for a few reasons: It’s a lagging indicator of opt-in status: people set their opt-in status outside of your app, and the app must be open to record changes to this attribute. If you use our Subscription CenterCustomer.io’s subscription center feature provides a way for customers to subscribe to, or unsubscribe from, specific topics. This helps you manage your audience’s preferences and make sure that they only get the messages they’re interested in., the push_enabled attribute does not account for your audience’s subscription preferences. People must have a version of your app with our SDK installed to record this attribute. The third issue above is the trickiest, especially if you’re coming to Customer.io from another push provider. When you import your push audience, these devices may be opted into push notifications, but they won’t have a push_enabled attribute until your audience updates to a version of your app that includes the Customer.io SDK. This can make it challenging to get a sense of your total audience size. Push preferences are set outside your app Your audience changes their push preferences outside your application. The push_enabled attribute simply records this setting when your audience opens your app. If a person changes their push preference, and your app isn’t open, the SDK cannot record the change until the next time they open your app. flowchart LR a(person changes push preference) a-->b{Is the app open?} b-->|yes|c(Update push_enabled in Customer.io) b-.->|no|d((Wait until person opens app)) d-.->e(person opens app)-.->c Some users are opted-out of push notifications by default In many cases, people who first open your app must opt into push notifications before they can receive messages from you. The default opt-in status is determined by each device’s operating system: iOS: People are opted out by default. They must opt in to receive push notifications. Android 13 or newer: People are opted out by default. They must opt in to receive push notifications. Android 12 or older: People are opted in by default. They must opt out to stop receiving push notifications. When your audience first opens your app, you can expose a system prompt to opt people into notifications, but their opt-in status is recorded by the operating system. Your audience can opt-into or out of notifications at any time, and this all happens outside of your app. For versions of your app using our SDK, we’ll record this status as the push_enabled attribute the next time your audience opens your app. People can be unsubscribed when push_enabled is true Imagine you have a customer who has opted into push notifications on their device. Their push_enabled attribute is set to true. Then, later, they get an email from you and decide to opt out of all notifications. This sets their unsubscribed attribute to true. This attribute prevents them from receiving email, push, SMS, and WhatsApp messages—despite the person having explicitly opted into push notifications. You can override this setting and send to unsubscribed people, but you should be careful to observe your audience’s subscription preferences where possible. This person may have devices where the push_enabled attribute is true (and accurately reflects their push opt-in status), but they won’t receive push notifications because they’ve unsubscribed from all of your messages. Where push_enabled is a lagging indicator of push preferences, unsubscribed is an immediate and deterministic preference. A person with unsubscribed set to true will not receive push notifications, regardless of their push_enabled attribute. Transactional push notifications are an exception to this rule. Unsubscribed people are still eligible for transactional push notifications. Using push_enabled as segment criteria You may not want to use the push_enabled attribute as criteria for a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., especially if you’re migrating from another push provider. The push_enabled attribute is a lagging indicator of push preferences, and it may not reflect the real push audience. If you look at a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. of people with push_enabled set to true, your segment may: Include people whose unsubscribed attribute or subscription preferences exclude them from push notifications. Exclude people with versions of your app that don’t have the Customer.io SDK. Not reflect your audience’s most recent preferences. In general, you should probably use more actionable criteria to target your audience. You likely don’t want to send a message to everybody who is opted-into push notifications. You’d rather send targeted messages to people who: Need to update your app (i.e. their app version is below a certain number). Haven’t engaged with a specific screen or feature in your app. Have items in their cart and haven’t completed their purchase. --- ## Send push notifications URL: https://docs.customer.io/journeys/send-push/ You can send basic push notifications—with a title and a body—as a part of a campaign or broadcast. Create a Push Notification This process covers a basic push notification. If you set an image or a deep link, your app should either use our SDKs or handle payloads in the format we send. If you want to send more than images and links—like badges, sounds, etc—you can send a message using a Custom Push Payload. In your campaign, click and drag a Push Notification into your campaign from the sidebar. Click the push notification in your workflow to change its send behavior, add action conditions, etc. When you’re done, click to add content. If you’ve setup both Android and iOS, you’ll see an option to choose which platform you’d like to send your push notification to (if you only want to send to one of iOS or Android). You may want to override the Subscription Preference: when people unsubscribe from your emails, it automatically prevents them from receiving push notifications as well. You can set up push notifications to ignore your audience’s subscription status—but you should make sure that you respect your audience’s topic preferences if you use our Subscription CenterCustomer.io’s subscription center feature provides a way for customers to subscribe to, or unsubscribe from, specific topics. This helps you manage your audience’s preferences and make sure that they only get the messages they’re interested in. feature. Choose which device(s) will receive your message in the To field. By default, you send push notifications to all of a person’s devices. For more information, see chose which devices receive your message below. (Optional) Set a Title—the text that appears above your message. If you don’t specify a title, Android devices display your app name, and iOS devices omit the title all together. Enter your Message text. We don’t have a strict size limit, but we suggest you keep messages as short as possible. Use the preview to see how your message wraps and how much real estate it consumes on the screen. (Optional) Set an Image URL and/or Deep Link or Web URL. The image shows along with your message, and the link sends your audience somewhere when they tap your notification. Both fields assume that you use our SDKs or handle our payload structure. For iOS, determine if you want to play the Default sound or send silently. You cannot provide a custom sound value today. Our SDKs only support the default sound. (Optional) Add Custom data to your payload. Custom data is any JSON-formatted data your app is set up to handle independently of our push payload and SDKs. Note: due to a Firebase limitation we only support sending string key value pairs Click Save to finish adding a push notification to your campaign. Preview push notifications When you compose your notification, you can select different mobile contexts to see what your push might look like on different devices. Here’s the iOS lock screen: We’ve chosen an “Alert” style for our display, but kept it fairly generic due to the sheer amount of devices that might be out there. To truly see what it looks like on a specific device, you should send a test to that device. Choose which device(s) receive your message When composing a push notification, the To field determines which of your audience’s device(s) receive the notification. By default, you send push notifications to all of a person’s devices. You can limit the push to specific device(s) by selecting: Last used device: Sends the message to the device that your customer most recently used with your app. By default, our SDKs set last_used when you invoke the identify method. If you integrate directly with our API, you can provide a last_used timestamp when updating devices our API. Customize: Sends the message to a specific device token. Generally, when you use this option, you want to use a liquid variable—like if you store a person’s preferred device as an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., you might use this option and send to {{customer.preferred_device}}. Play sound on push receipt (iOS only) When you send a push notification to iOS devices, you can opt to send the Default sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the phone to vibrate. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered! Message Priority Push notifications have a concept of priority, which typically affects the balance between message delivery speed and battery optimization on recipient devices. In most cases, these settings only affect Android messages because we already send iOS messages at the highest priority (10)—whether you select Normal or High priority. Learn more about iOS message priority But for Android messages, Normal priority means that messages might be delayed due to device-specific battery optimizations, and devices in standby mode might not wake up long enough to send delivery metrics back to Customer.io, so you might not see messages as delivered until your users open your app. Setting a High priority ensures that we deliver messages as soon as possible to Android devices, and the message will wake the device to properly report delivery metrics.  See Firebase’s documentation on message priority Before you set high priority, see Firebase’s documentation on message priority to learn more about when you should send high-priority messages. You can set separate priority settings for each platform by clicking Show advanced options. You might do this if you wanted to lower the priority of iOS messages, which, again, are already delivered at the highest priority by default. Personalize messages with liquid As with all other Customer.io messages, you can personalize messages with relevant customer data using 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}}.. If you’ve got an event or API triggered broadcast campaign, you can also use event and broadcast data! For example, if you trigger your message using a purchase event, you might notify your audience how much they spent with a message like this: Thanks for purchasing! You've spent {{event.total}}! Standard push notification payload Push notification Title and Body are standard for most messages. However, if you send a notification with an image or a link, you’ll either need to use our SDK or handle our notification payload(s). Below are our standard notification payloads. The payload changes slightly depending on whether you use Firebase Cloud Messaging (FCM, for both iOS and Android devices) or Apple’s Push Notification service (APNs, for iOS devices only). APNs (iOS) APNs (iOS) Your Image URL and Deep link or web URL go inside a CIO.push object. Custom Data resides in the payload but outside the CIO and aps objects. { "CIO": { "push": { "link": "string", "image": "string" } }, "aps": { "mutable-content": 1, "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" } }, //custom keys go here "customKey": "a custom key" } FCM (iOS) FCM (iOS) The CIO.push object contains the link and image fields from notification. Custom Data resides in the payload but outside the CIO and aps objects. { "message": { "apns": { "payload": { "CIO": { "push": { "link": "string", "image": "string" } }, "aps": { "mutable-content": 1, "alert": { "title": "string", "body": "string" } }, // custom keys go in the payload but outside the CIO and aps objects "customKey1": "custom keys", "customKey2": "another custom key" } } } } FCM (Android) FCM (Android) When you send a push notification containing an image or a link, we place the entire payload in the data object.  Simple push payloads use the notification object If you send a simple notification—a notification without an image, link, or custom keys, your notification’s title and body will go in the message.notification object. This is the default location for a push notification title and body for FCM-based pushes. { "message": { //rich push "data": { "title": "string", //(optional) The title of the notification "body": "string", //The message you want to send "image": "string", //https URL to an image you want to include in the notification "link": "string", //Deep link in the format remote-habits://deep?message=hello&message2=world // other "custom keys" are sent inside this object } } } Test your push notifications  Use a test/development workspace When testing push notifications, you should test in a separate workspace from your production/customer-facing environment, ensuring that you never send your audience a message in error. For more information, see our testing recommendations To send a test push, you’ll need the device tokens of your test devices. Go to the People page and select your test “people” to find their devices—either in the Devices tab or the Devices section of the Overview. Hover over a token and click to copy it. Test your push content To see your push notification’s content on a single device, click the “Send test” button in the top right-hand corner of the composer. You’ll see this modal: Paste your device token into the “Send test…” box in the Push composer and send a test to it. You may see a “Device type” option if you have both Android and iOS enabled, and you’ll need to tell us which type your test device is. If you’ve chosen to send to sandbox in your Push settings, that will be enabled here. To disable it, you’ll need to return to your settings and uncheck that box: Once you’ve sent a test, you’ll be able to quickly re-use that token, by checking the ‘Last tested token’ box. --- ## Custom push payloads URL: https://docs.customer.io/journeys/push-custom-payloads/ You can send highly customized push notifications using a Custom Payload. If you've integrated with our SDK, custom payloads let you send images and link customers to pages in your app when they tap your message. Getting started with custom payloads Creating a custom payload for your push notification grants access to a greater range of push features, like images and deep links—provided that your app is set up to support those features. To enter a custom payload, click Custom Payload when composing your push notification. You can then write or paste your custom code into the editor. There are different tabs for Android and iOS so that you can send custom code for each platform separately but, if you are using Firebase Cloud Messaging, you can just use the Android tab to send custom code that will be used for both Android and iOS. Before you begin Figure out what you want your message to do: do you want to send people to a deep link in your app or show them an image? Before you compose your message, make sure that your app supports that functionality. You may have to develop your app to handle some custom payload features. If your payload includes links (like the link key, supported by our SDKs), you may need to know the format of deep links—links to pages within your app. Talk to your development team if you need help understanding the link scheme within your app. You should also understand which push provider(s) you use. If you use Firebase Cloud Messaging, you may be able to construct a single payload supporting both Android and iOS devices. If you use Apple’s Push Notification service (APNs), you’ll need to enter different payloads for both iOS and Android devices. When you have your payload, you’ll need to ensure that: Your JSON code is valid. The code you use follows the guidelines for the specific provider: Apple Push Notification Service (APNs - iOS notifications) Firebase Cloud Messaging (FCM - Android and iOS notifications) Basic FCM payload Basic FCM payload Here’s the most basic example of a custom payload for sending to Android and iOS devices via Firebase Cloud Messaging: { "message": { "notification": { "body": "YOUR MESSAGE BODY HERE", "title": "YOUR MESSAGE TITLE HERE", "image": "YOUR IMAGE URL HERE" } } } Basic APNs payload Basic APNs payload Here’s the most basic example of a custom payload for sending to iOS devices via Apple Push Notification Service: { "aps": { "alert": { "body": "YOUR MESSAGE BODY HERE", "title": "YOUR MESSAGE TITLE HERE" } } } Personalize push notifications You can use 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}}. in your custom payload. While you can technically use liquid in any string field, you probably want to limit yourself to the body, title, and subtitle (on iOS) fields. Here’s a basic Android (and iOS via FCM) example using customer and event data: { "message": { "notification": { "body": "Hi {{customer.first_name}}, your {{event.event_title}} has been booked!", "title": "All set!" } } } This results in a push notification that looks like this on iOS: Custom push payload reference While you can send push notifications to both iOS and Android devices using FCM, if you want to provide a custom payload, you must provide separate payloads for your iOS and Android audiences. If your custom payload does not match the shape below, or you provide an incorrect data type for a key, your notification may fail.  Check your iOS notification setup your iOS push payload changes based on whether you send notifications through Google’s Firebase cloud messaging platform (FCM) or the Apple Push Notification service (APNs). Make sure you reference the correct reference for your push provider. FCM custom push payload Your custom payload’s shape and keys depend on whether you use Customer.io’s SDKs or developed your own custom integration with our API. If you send a message with our SDKs, you can place the entire message within the message.data object or use the notification; our SDK handles both payload structures flexibly. This works for both Android and iOS platforms. The message.data object can also contain custom data that your app to interpret. Custom data isn’t supported natively; it takes additional development. Talk to your app’s developer(s) to understand the custom data that you can pass to your app. If you developed a custom integration, you can set a global notification—for both Android and iOS—in the message.notification object. You can pass additional options for the android push in the message.android.notification object. You’ll find example payloads below depending on whether you’ve integrated with our SDK or wrote your own custom integration. SDK integration SDK integration Our SDK natively supports the keys below. However, if you’ve extended the SDK or done additional app development, your payload may contain other fields. { "message": { "data": { "title": "string", //(optional) The title of the notification "body": "string", //The message you want to send "image": "string", //https URL to an image you want to include in the notification "link": "string", //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. Custom integrated app Custom integrated app { "message": { "notification": { "title": "string", //(optional) The title of the notification "body": "string", //(optional) The message you want to send "image": "string" //https URL to an image you want to include in the notification }, "data": { //Use if you've integrated with our SDK. //Optional key-value pairs that your app interprets. //We don't validate the keys or values you provide. }, "android": { "notification": { //(Optional) Contains options specific to an android notification. "icon": "string", "sound": "string", "tag": "string", "color": "#rrggbb", "click_action": "string", "body_loc_key": "string", "body_loc_args": "stringified,array", "title_loc_key": "string", "title_loc_args": "stringified,array" } } } } message object Required The parent object for Android custom push payloads. android object Contains custom push options for your notification. data object Contains key-value pairs that your app interprets. notification object Contains the push body and title. iOS custom push payload (FCM)  This section is for iOS over Firebase only! If you send push notifications to iOS using the Apple Push Notification service (APNs), you should see our APNs payload reference instead. Before you send a push notification, make sure you understand how you’re integrated with Customer.io. If you use one of our SDKs, make sure you set mutable_content to 1. This ensures that your push notifications show images and report “delivered” metrics. SDK integration SDK integration { "message": { "apns": { "payload": { "CIO": { "push": { "link": "string", //Deep link in the format remote-habits://deep?message=hello&message2=world "image": "string" //https URL to an image you want to include in the notification } }, "aps": { // mutable_content must be set to 1 to support images // and "delivered" metrics from the Customer.io SDK "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, // additional custom data "additionalProperties": "custom keys" } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Custom-integration Custom-integration When you send a custom notification, you can set a default message.notification with a body. The iOS custom payload portions of your notification appear in the apns object. headers is an object representing HTTP request headers defined in Apple Push Notification Service. payload is an object containing both the aps dictionary and any custom payload options you want to set, as defined by Apple’s payload reference. Including a title and body in the apns.payload.alert overrides the title and body set in the message.notification object. fcm_options is an object containing options for features provided by the FCM SDK for iOS. The following shows the basic shape of an iOS custom push payload for FCM. You’ll find a deeper explanation of the iOS payload options that FCM supports in Google’s documentation. { "message": { "notification": { // default notification "title": "string", "body": "string" }, "apns": { "headers": { // headers defined in Apple Push Notification Service. "apns-priority": 10 }, "payload": { "aps": { // iOS message options go here "mutable_content": true, "sound": "default" } } } } } message object Required The base object for all Firebase payloads. apns object Required Defines a push notification for iOS devices. headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required iOS custom push payload (APNs)  This section is only for apps that use Apple’s Push Notification service (APNs)! You can also send push notifications to iOS devices using Google’s Firebase Cloud Messaging (FCM) service. Make sure you use the correct payload for the messaging provider you set up in your Workspace Settings. Whether you use our SDK or developed your own integration, your push notification will normally include a aps.alert object with a body, reflecting the message you want to send. You can pass additional options for the push in the aps object (outside of the alert). Where the alert represents the message, these settings tell the device how to handle the message—whether to play a sound or add the message to a thread of notifications, etc. If you use our SDK, you’ll include additional options—like deep links or images—in the CIO.push object. You should also set mutable_content to 1. This ensures that your push notifications show images and report “delivered” metrics. Otherwise, you can pass additional, custom data outside the aps object. This custom data is something you’ve extended our SDK, or done your own development, to support. Talk to your app’s developer(s) to understand the custom data that you can pass to your app. { "aps": { "alert": { //can be string or object //alert object keys "body": "string", "title": "string", "subtitle": "string", "launch-image": "string", // localization arguments "title-loc-key": "string", "title-loc-args": ["array", "of", "strings"], "subtitle-loc-key": "string", "subtitle-loc-args": ["array", "of", "strings"], "loc-key": "string", "loc-args": ["array", "of", "strings"] }, // message options "badge": 0, // number "sound": "default", "thread-id": "string", "category": "string", "content-available": 0, // number, 0 (default) or 1 // mutable_content must be set to 1 to support images // and "delivered" metrics from the Customer.io SDK "mutable-content": 1, // number, 0 (default) or 1 "target-content-id": "string", }, // options supported by Customer.io's SDK "CIO": { "push": { "link": "string", // deep links in the format remote-habits://deep?message=hello&message2=world "image": "string" // https URL to an image, including the file extension } }, // additional custom data "additionalProperties": "custom keys" } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. Got feedback? We’re hoping to get your feedback on how you’re using custom payloads, if there’s anything you’re struggling with, or if there’s anything you’re really enjoying about it and want to make sure we keep. Let us know! --- ## Test push notifications URL: https://docs.customer.io/journeys/push-qa-testing/ When it comes to testing your push configuration and messages for quality assurance (QA) purposes, we recommend that you keep separate development/test and production workspaces—*especially* if your workspaces don't share configuration credentials. For example, if you have an iOS app, you may have a sandbox environment set up to test your push notifications before you start your campaigns. This often has the same certification credentials as your production environment. We still recommend setting up a separate test workspace with the “Send to sandbox” option turned on: To send any type of test message, you’ll need a device token. Here’s how to find one. Looking for A/B testing for push notifications? We've got you covered. Find your device token Go to People, click your test user, and go to the Devices tab to find your device token. Hover over a device and click to copy the token to a push notification. Testing your configuration Once you have a device token, if you want to test if your push setup is working, we recommend that you send this message from the configuration page: Enter your platform type and the token you’d like to test with (see how to find those here), and you’ll hopefully get a test push to that device with the following content: “If you’re reading this, it means your integration with Customer.io is working!” and you’re set to start adding push notifications to your campaigns. Send a single push notification If you want to see how your push notification looks on a single device, you can send a test from within the push notification composer: This is the modal you should see: 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. --- ## Push metrics and message statuses URL: https://docs.customer.io/journeys/push-notifications-tracking/ A push notification can have several different statuses as it goes through the process of being sent. When we attempt to deliver these, you may have told us to try reaching them on one or multiple devices. How it works While we track the status of each individual message as it goes through Customer.io, push notifications can be trickier to track than other message channels for two main reasons: A person can have multiple devices, and you can send a message to all of a person’s devices. When you send to All Devices, we aggregate metrics across all of a person’s devices because we (and you!) monitor engagement at a personal level, not a device level. Push notifications go through an intermediary—the push provider (APNS or FCM). We report statuses between us and your push provider; devices using our SDK report back delivered and opened metrics back to Customer.io—but there can be a slight delay in reporting these metrics. And Customers who have an earlier version of your app that doesn’t have our SDK installed may not report these statuses back at all. flowchart LR a(trigger push notification) a-->b{Is push finished sending to all devices} b-.->|No|c(Push Attempted) b-->|Yes|i{Is at least one send successful?} i-->|Yes|d(Push Sent) i-.->|No, liquid failure|j(Push Failed) i-.->|No, bounced tokens|k(Push Bounced) d-->e{Is device online?} e-->|Yes|z(Push Delivered) z-->g{Does person tap push?}-->|yes|h(Push Opened) e-.->|No|x(Wait for device to come online)-.->z Mobile metrics In general, you’re concerned with a few metrics for push notifications: Sent: We’ve sent a message to the push provider. Delivered: The device reported that it received the message. Opened: The user tapped the notification; if the notification includes a deep link, this takes the user into the app. Converted: The user achieved a goal and you want to report that back to Customer.io. You can measure success of push notifications against the number of Sent or Delivered messages, but there’s an important distinction between the two: Sent is reported by the push provider (APNS or FCM) and Delivered is reported by the device. When you send a push notification, it’s much more likely that it’ll report Sent right away, but it may not report Delivered (or other metrics on the device) until the user opens the app. When you measure the success of messages, just remember that your Sent is immediately trustworthy, and the Delivered count may change over time as people open your app. What push metrics do we capture from devices and when? Devices report message deliveries, opens, and conversions. But unlike other channels, push notification reporting depends on the status of your app on the recipient’s device. If your app is in the foreground, we’ll typically report metrics (if you show push notifications when your app is in the foreground). If your app is in the background, we might be able to report metrics based on the device’s operating system and your SDK implementation. See Reporting metrics while apps are in the background for more information. If the app is closed (sometimes called the killed or cold state), we can’t report metrics until the user opens your app. Android and iOS have strict rules about what we can do when apps are in the background. This can cause a delay between when you send a message and when we can report metrics relating to deliveries, opens, and so on. In general, this is an issue for deliveries, which happen independently of your app’s state on a recipient’s device—as opposed to an Opened or Converted events. For example, if your push notifications include deep links, your link will open the app and report metrics. Your notification is only likely to be opened in the background if it doesn’t include a deep link into your app. Event Reported in Foreground Reported in Background Reported in Closed Sent N/A N/A N/A Delivered ✅ ✅* ❌ Opened ✅ ✅** ❌ Converted ✅ N/A ❌ *Background reporting is dependent on the device operating system and user settings. **Opening a message while the app is in the background typically applies to messages that don’t contain deep links. If a message has a deep link into your app, it’ll bring the app to the foreground and we’ll report metrics. Reporting metrics when apps are in the background Android and iOS both have strict, but slightly different rules for what our SDKs are allowed to do when apps are in the background. For iOS, the SDK has 30 seconds after receiving a notification to report metrics back to Customer.io. Typically that means that we’ll report a delivery as long as your app is opened. For Android, apps in the background will report metrics back to Customer.io if: The device isn’t in Doze mode. The device and/or your app isn’t in Deep Sleep mode. The user hasn’t otherwise optimized battery usage in a way that blocks background activity. Why are messages sent but not delivered? We mark a message sent when we send it to the push provider (APNS or FCM). But we can’t verify delivery until a device reports a delivered event back to Customer.io. Because the SDK reports delivery, a message might show sent but not yet show delivered when: The app is closed. The recipient’s device is offline. The recipient doesn’t have a version of your app containing the Customer.io SDK. In many cases, the push notification probably made it to your recipient’s device, but we won’t know about it because the app can’t report back delivered metrics until it’s opened or put in a state that allows it to report back metrics (like foregrounding the app). flowchart LR a{Is unsubscribed true?} a-.....->|yes|c(push not sent) a-->|no|b(push sent) b(push sent) b-->d{Is person opted-into notifications?} d-...->|no|h(push not delivered) d-->|yes|f{Does the app include the Customer.io SDK?} f-..->|no|i(push delivered but delivery not reported to Customer.io) f-->|yes|k{Is device online and able to report?} k-.->|no|l(Wait for app to come online) l-.->k k-->|yes|j(push delivered and marked delivered in Customer.io) Differences in delivered metrics for Android and iOS Android and iOS handle deliveries differently, which means that you’ll see a significant difference in delivered metrics between the two platforms. iOS devices only report delivery when a user has opted into notifications and a push notification appears to the user (whether in the notification tray, lock screen, etc). But Android reports the delivery of notifications regardless of opt-in status. This means that a message can be delivered to a device, suppressed because a user hasn’t opted into notifications, and the device will still report back that the message was delivered. Because Android reports delivery regardless of opt-in status, you’ll likely see higher delivered metrics for Android devices than iOS devices. We’re working on ways to reconcile the difference between the two platforms in the future, so that we can give you a better understanding of your audience’s engagement. Push reporting when migrating from another provider If you migrated a push audience from another provider, your audience may have versions of your app that don’t include our SDK. These devices can still receive notifications (assuming you use the same certificates in Customer.io and your previous service provider), but they won’t report back delivered metrics to Customer.io unless you gather this data using a third party like Segment or Rudderstack and send these events to Customer.io. Push notification statuses You can look up the status of each push notification by clicking on the message in the Deliveries page. This can help you understand whether a message was sent, delivered, opened, or converted. Note that statuses are affected by the All Devices and Last Used Device settings. See Push notification status when targeting all devices for more information. Attempted: We’re in the process of sending a message to the push notification service. This status means that your push notification has been created and we’ve tried to send it via the appropriate service—Apple’s Push Notification Service (APNS) or Firebase Cloud Messaging (FCM)—which then would send to your customer’s device. If there are errors with our attempt, we’ll try to send it again up to ten times. Sent: We’ve successfully sent your message to the push notification service. This status does not indicate that a message has been successfully delivered. It only means that the push payload was valid and we were able to pass it to the push notification service for valid push tokens and filter out suppressed devices. If you’ve sent your message to all of a person’s devices, this indicates that we’ve successfully sent your message to the push notification service for all of a person’s devices and filtered out suppressed devices. Note that the total number of “Sends” in your campaign metrics includes people whose messages bounced and were suppressed. We do this to help calculate your total audience size when you send a message: sends minus suppressed and bounced messages equals total recipients. Delivered: The device reported back that it received the message. There are a number of things that can affect or delay us reporting message delivery. See What push metrics do we capture from devices and when? for more information. Opened: The user tapped the notification. If you’ve sent your message to all of a person’s devices, we report a message as Opened when a person taps on a push notification sent to any of their devices. This metric is reported back to Customer.io when a person opens your app (or already has the app open). Failed: Our attempts to send a push notification were unsuccessful. If you tried to send a notification to all of a person’s devices, then this status occurs when none of the attempts to send a push notification to the end-user’s devices are successful. You’ll be able to see the failure reasons on the Delivery page. If we’re able to send a push notification to at least one of a person’s devices, we’ll report the message as Sent. Converted: The user achieved a goal and your app reported that back to Customer.io. This is treated the same as emails; when a user matches your campaign’s conversion criteria, the message nearest the conversion is marked as Converted.  Opens and clicks are the same things in push notifications Unlike other channels, where people have to open a message before they can click a link, people can read a notification right when it’s delivered—they don’t have to open it. So, in push notifications, we track Opened in the same way that most other messages track Clicked. Push notification status when targeting all devices For other message channels, you send your message to an individual email address, phone number, etc—a message is one-to-one with a recipient. But for push notifications, you can target all of a person’s devices, which affects how we track the status of messages and associated metrics. For some statuses, like Sent, we count a status when it has occurred for all of a person’s devices. For others, like Opened, we count it when it has occurred for any of a person’s devices. For metrics, we’ll display an aggregate metric for the person rather than each device they own, because we (and you!) monitor engagement at a personal level, not a device level. In general, we recommend that you use Last Device when you send a message so you don’t overwhelm your audience with messages across all their possible devices. For example, when you use Last Device, a sent message is a single push notification sent to the push provider. When you use All Devices, we don’t count a message as being sent until we’ve sent a message to the push provider for each of a person’s devices. Do we record a status when it's true for all of a person's devices or any of their devices? Status All Devices Any Device Description Attempted ✅ We're in the process of sending messages. Sent ✅ We've sent messages to the push provider. Delivered ✅ Your app reported back that it received the message. Opened ✅ Your app reported back that the message was opened. Converted ✅ The person matched your campaign's conversion criteria. Failed ✅ We were unable to send messages to the push provider. This is typically due to a 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}}. error. See Failed for more information. Undeliverable ✅ We couldn't attempt delivery. This happens when a user has no devices associated with them. Bounced ✅ There's a temporary delivery issue, like a soft bounce in an email. This usually happens when a user has uninstalled your app, their device token has expired or their device token is invalid. Message failures If you see a message Failed, you can see the failure reasons on the Delivery page. Here’s a list of failure reasons you might see, what they mean, and how to fix them. Failure Reason Description Liquid error: variable is missing The recipient didn’t have a value for a variable you used in your message. For example, if your message used {{customer.email}} and the customer didn’t have an email attribute, the message will fail. Check that you’re using the correct variable name and either set a fallback or that the recipient has a value for that variable. Customer doesn’t exist A rare error indicating that the value you’ve used for the recipient doesn’t exist. Make sure you’re using the correct customer ID or email for a message. Customer is unsubscribed This error indicates that the customer has opted out of receiving messages. Verify the customer’s subscription status to make sure they can receive messages. Credentials not found Your push credentials failed or were otherwise empty. Check that you’ve provided your push certificate and that it hasn’t expired. Set campaign goals to measure engagement Unlike email, recipients can read and react to your push notification without “opening” it. While you can measure engagement with Opened metrics—where a person taps on a push notification to go to a link—you may find more success setting a campaign goal to measure whether or not a message helps you achieve a specific business goal. Ideally, your messages have a call to action, a result you want your audience to achieve. You want your audience to finish setting up their profile in your app, complete a purchase, sign up for a class, and so on. If you capture these actions as goals, you’ll be able to understand how many people engage with your message and achieve your goal—which is a much better metric of success than raw sent and delivered metrics. We track metrics at the message and journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. level. Setting a campaign goal as an event that you want your audience to achieve—a completed purchase, visiting a specific page, etc—can help you understand how your push notifications contribute to your business goals, even when it’s difficult to track the status of individual push notifications. Campaign Overview data On the Overview page, you can see how your push notifications perform in your campaign. Delivered You can track delivery using our SDKs, or by adding custom code to your app to report delivery metrics back to Customer.io when a customer receives a push. Opened To track opened metrics, you need to either integrate with our SDKs or add some code to your app to detect when opens occur and then send “open” events to Customer.io as documented in our Technical Integration Guide. Note, you need to include the CIO-Delivery-ID and CIO-Delivery-Token parameters from the push when sending open events to Customer.io. Delivery Logs data When filtering deliveries, you can choose to only see push notifications. In this view, you’ll be able to see: The name of the push notification and a link to the message Which customer triggered that message, and how many of their devices we attempted to send to When the push notification was attempted The push notification status (sent, failed, attempted, etc.) Conversion information where applicable You’ll also have the option to retry sending the push notification if its status is ‘failed’: --- ## Best practices for push notifications URL: https://docs.customer.io/journeys/push-best-practices/ Here are some things you can do to make sure that you can effectively communicate with your audience using push notifications. 1. Keep the Customer.io SDK up to date Mobile operating systems regularly change and we’re constantly improving our SDKs—both to account for changes in mobile ecosystems and to better support Customer.io features. You’ll have the best mobile experience if you keep your SDK up to date. You can subscribe to SDKs to find out when we release new versions. Or check for updates here. Our SDKs use three version numbers in the format major.minor.patch—for example, 4.3.1. Patch updates typically include bug fixes that can make your life easier. Minor versions often include new features or improvements that won’t break your integration. And major versions can include breaking changes or new features that require your attention—but generally include big new features and improvements. 2. Encourage customers to update your app While using the latest version of our SDK is important, your customers need to update your app to get the latest features and improvements! You may need to create segments to encourage people to update your app. You can do this by filtering people based on the cio_sdk_version device attribute. This tells you what version of the SDK they’re using. You can encourage people on older versions to update your app. Go to the People page. Select Filter by Conditions and click Add device condition. Select cio_sdk_version is equal to and use the drop-down to select a version. From here you can see how many people are on each version of the Customer.io SDK. You can also export the list to use as a segment and encourage people to update your app! 3. Generate tokens from the Customer.io SDK A device token represents your customers’ devices. While you might be able to fetch device tokens without using our SDKs, we’ve designed our SDKs to make getting and setting device tokens easy for you. Handling tokens with our SDKs ensures that your people are always up to date in Customer.io, and that your audience will get your messages! 4. Use device criteria when you segment your users When you send push notifications to a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. of people, considering using segments that include device criteria. This ensures that you only send push notifications to valid mobile users and can help you better understand the success of your messages. For example, when you create a segment of device users, you might want to make sure that a person was last_seen within a reasonable period of time, or that they have a device at all. If you send your message to a group of people without checking that they have devices, it’ll be hard to determine the success of your campaign! 5. Set goals for your campaigns and measure conversions Users can swipe away or dismiss push notifications without interacting with them—but they might still read the message and achieve your goal without interacting with a notification. Rather than measuring simple clicks, you should use goals in your campaigns to measure indirect conversions and make sure that you fully understand how your audience responds to your messages. 6. Make sure your push notifications have a clear call to action Make sure that you send notifications to your audience that are relevant to them and provide value. People are likely to dismiss or even disable notifications if they aren’t significantly meaningful or you send them too often. For example, you might send people notifications when tickets become available to an event they’re interested in, or when there’s a new article they’re interested in. If you send messages for every event or article, you might overwhelm your audience. If you send a notification for events or articles that don’t interest them, or don’t take them directly to the event or article they’re interested in, they won’t see the value in your messages. 7. Send to the last_used device When you send a push notification, you can send to all of a person’s device or just to their last_used device. In general, we suggest the last_used device because it’s the device that a person is most likely to be using at the time you send your message. Sending to all devices might mean that a person gets the same message on multiple devices, which can be annoying. It can also skew your metrics; a single person in your audience might achieve a conversion on one device and not on another. This could affect your conversion rate even though the person did what you wanted them to do! --- ## Frequently Asked Questions URL: https://docs.customer.io/journeys/push-faq/ This page contains answers to frequent questions about push notifications both in general and in Customer.io. How do conversions work with push notifications? Conversions are attributed to the last email or message sent, opened, or clicked before the person performs an event or enters/leaves a segment defined in your conversion criteria. If you track users performing an action or entering a segment after receiving a push, then the message will be marked as converted, regardless of the device we reached them on. What Firebase permissions does Customer.io need in order to send push notifications? Firebase’s documentation says that we need the cloudmessaging.messages.create permission since we use the FCM HTTP API to send push notifications, that permission however does not belong to the Firebase Cloud Messaging Admin role. The roles that have that permission (found here) include: Owner (roles/owner) Editor (roles/editor) Firebase Admin (roles/firebase.admin) Firebase Grow Admin (roles/firebase.growthAdmin) Firebase Admin SDK Administrator Service Agent (roles/firebase.sdkAdminServiceAgent) Firebase SDK Provisioning Service Agent (roles/firebase.sdkProvisioningServiceAgent) Alternatively, if you wanted to provide even more limited permissions, you should be able to define a custom role with only the cloudmessaging.messages.create permission. How do I know if a push notification was opened? Our SDKs can automatically track opens. If you don’t use our SDKs, you’ll need to add some code to your app; first, to detect when an open occurs, and then to send an event to our API. To track opens and attribute them to specific notifications, you’ll need to send us events including the CIO-Delivery-ID and CIO-Delivery-Token parameters from the push, as documented in our Technical Integration Guide. Why are more push notifications delivered to Android devices than iOS devices? Android and iOS report delivered metrics differently, which leads to a difference in the metrics you see in Customer.io. But it’s unlikely that there’s actually a significant difference in real delivery rates between your iOS and Android users. iOS devices only report delivery when a user has opted into notifications and a push notification appears to the user (whether in the notification tray, lock screen, etc). But Android reports the delivery of notifications regardless of opt-in status. This means that a message might report that it was delivered to an Android device, even if it’s been suppressed because a user hasn’t opted into notifications. This difference in reporting can make it seem like more push notifications are delivered to Android devices than iOS devices. We’re working on ways to reconcile the difference between the two platforms in the future, so that we can give you a better understanding of your audience’s engagement. How do I know if a push notification failed because the app has been uninstalled? Push notification services don’t provide a reliable way of detecting this with absolute certainty. However, when you look at a push notification’s status, getting a Bad Device Token error for a device is a reasonable indicator that this particular person no longer has the app installed. How do I track app uninstalls? Some push services use scheduled silent push notifications as a brute force way of seeing if an app is still installed on a device. Usage of silent background pushes for app uninstall tracking violates Apple and Google developer policies and is now significantly limited with iOS 15: With iOS & iPadOS 15 Beta 4: Background pushes will only be delivered if the app has been used in the foregraound in the past few weeks. Instead, you can use app activity (i.e. has logged in in last 30 days) to reliably measure conversions and engagement. Is there a character limit or best practice? Because push notifications go to so many devices, their text might get cut off at different points. What looks great on an iPad’s lock screen might get awkwardly truncated in a Pixel’s notification center. As devices and operating systems change, so do the number of ways a push notification can be seen. The only concrete restriction is a message size limit: 4KB (4096 bytes) for both Apple Push Notification service (APNs) and Android’s Firebase Cloud Messaging (FCM). This isn’t just for the message text, but for the entire payload— this includes any other custom data being sent along with that notification. Tip: To reduce payload size, omit whitespace and line breaks! To make sure your notifications are as successful as possible, we encourage you to: Keep it brief! A good general guideline is to make it actionable and beneficial in less than 40 characters. Test them! This way, you can see how they’re appearing in the wild, and make them shorter if you need to. I’ve set up push for iOS, but am getting an invalid token error! If you receive an Invalid device token (APNs BadDeviceToken) error, Apple doesn’t recognize the token you added via our updateDevice API call as valid for that app or environment. There are a few reasons why that might occur: You are using an incorrect token for that particular end-user, or an old token that no longer exists. Your app is built using the development configuration rather than production. Device tokens are unique to each environment and today, we only support the production environment. Apple rotates device tokens for several reasons, one of which is the end-user reinstalling the app or updating their OS. Because these tokens rotate frequently, we recommend that you send us an updateDevice API call every time your app is launched to ensure that we’ve always got the latest device token. To fix this issue, confirm that: The iOS .p8 app certificate you’ve uploaded is the same one that’s in use for the app that generated the device token. Device tokens are not re-used - they’re unique to the app and certificate. The device token is for the production environment, not using the development/sandbox environment. You’re attempting to send to the most recently generated device token. Once a new token has been generated, the old one will fail. If you’re still getting this error, get in touch with us at win@customer.io. How is the last used device determined? A given user’s “last used device” is the device they last used to access your app. It’s determined by the timestamp in the most recent identifyDevice API call. We recommend calling this every time your app is launched to keep the “Last Used” value up to date. This is sometimes a friendlier way of targeting people, and lets you send a message to the device a person is most likely to use rather than sending a message to every device a person owns. How do I know that a person saw or dismissed the push notification? Our SDKs can automatically track push delivered and opened metrics. You can reasonably infer “dismissed” notifications from those two metrics. If you don’t use our SDKs, iOS and Android’s push notification services don’t generally provide this information. There are some platform-specific ways to add code to your app to detect that a notification was received when the app is in the foreground or background. However, this won’t catch notifications received when your app isn’t running. How do I add deep-links, images, buttons, or other richer formatting? In our Rich Push editor, you can add images and deep links and specify whether iOS devices should play the default sound or not. In both the Rich Push and Custom Payload editors, you can add custom JSON for buttons and other formatting. How should I set up testing? We recommend that you set up a separate workspace to test your push notifications, even if (in the case of iOS, for example) you use the same configuration credentials for testing and production. If you have different versions of your app, notifications will be routed correctly based on the device IDs that each app sends to Customer.io—which should be unique per app. If you choose to use separate workspaces per version of your app (e.g., dev/staging/production) then each version of your app should register devices in the proper workspace. How do I find a device token so I can send a test push from the composer? Device tokens are located on an individual’s Person page: You can search for a the specific person you want to send the test to, and then go to the Devices tab to see their device tokens. You can then hover over a device and click to copy the token to a push notification. How do I target a test version of my app? We recommend that you create a new workspace, in which push notifications are configured with the certificates for your test app. You can also swap the certificates in your configuration. Can I send to iOS via FCM/Firebase? Yes, you can! You will see this option in your iOS configuration: It will be available for you if you have not set up Android as well. If you select this option, you will still need to setup the configuiration for Android if you haven’t yet done so. Whether or not you should do this depends entirely upon your setup and what’s easier for you. For example, if FCM is a critical part of your infrastructure, and you don’t plan to configure the Apple Push Notification Service anytime soon, this may be a valuable option. I want to send push notifications only to Android/iOS. How? On the push notification workflow item, you’ll be able to select a specific platform: A push notification with a platform specified here will only be sent if a customer has a device with that platform! How do I configure my notification to display text right-to-left (RTL)? The alignment for push notifications is determined by the iOS or Android OS and device settings. It is not configurable through the push notification payload. For iOS, the system looks at the first character in the title and automatically chooses RTL or LTR based on the character’s languague. On Android, the text alignment is defined by the device’s locale/language. I need a new feature! When will you add it? If you find that push notifications are missing a critical feature, let us know what it is, and what kind of messages you’d like to send using it. As an intermediate workaround, webhooks are really powerful! What kinds of apps do your SDKs support? Today, we support both native Android and iOS apps, as well as the following hybrid mobile platforms: React Native, Flutter, and Expo. If your app is built in another hybrid platform like Ionic, you might be able to use our SDKs, but you’ll need to write custom code (or “bridges”) to integrate our existing SDKs into your apps. Do Customer.io SDKs work with Segment’s SDK? Yes, and if you already use Segment, you should integrate with Customer.io as well! The Segment SDK collects data, and you can pipe that data to Customer.io through Segment. Segment’s SDK does not interpret push notifications, identify devices in Customer.io, etc. You need Customer.io’s SDKs to do those things. So, we strongly suggest that you integrate with our SDKs, even if you already use Segment’s SDKs. Do I have to use Customer.io SDKs to handle push notifications? No, you’re welcome to write your own integration. But we think that our SDKs greatly simplify the process of identifying devices and interpreting push notifications, without adding bloat to your app. Do Customer.io SDKs support in-app messaging? They do! See our in-app documentation for more information. --- ## Troubleshooting mobile issues URL: https://docs.customer.io/journeys/troubleshooting-mobile/ Having trouble with your mobile integration, push notifications, or in-app messages? Here are a few things that can help you fix the problem or gather information if you need to get in touch with Customer.io support.  This page is focused on our SDKs If you’ve written your own mobile integration with Customer.io, we may not be able to troubleshoot your app. Before you contact Customer.io Support If you’re having trouble with push notifications and in-app messages, there are a few things you can do to troubleshoot the problem before you contact Customer.io Support. These are common troubleshooting tips that are likely to fix your problem—and are the starting point for most of our SDK-related support conversations. Otherwise, we’ll help you gather the information you’ll need if you do need to contact us. 1. Update the SDK If you’re having trouble with your mobile integration, check your SDK version and see if there’s an update available. We’re constantly improving our SDKs, and you’ll get the best experience if you’re using the latest version. Minor version updates typically include bug fixes. SDK Latest version iOS 4.4.1 Android 4.17.0 React Native 6.4.2 Flutter 4.0.1 Expo 3.3.0 2. Try out our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask questions like: Can you help me integrate with the Customer.io SDK for iOS? My users aren’t receiving push notifications on Android. Can you help me troubleshoot? The tool will help you update your code and return detailed steps to find and troubleshoot issues. 3. Check out troubleshooting tips We have individual troubleshooting pages for each of our SDKs. We provide tips on these pages for some commonly seen problems, like issues displaying images in rich push notifications, problems registering device tokens to people in Customer.io, getting in-app notifications and so on. We may have already seen and solved your problem! iOS Android React Native Flutter Expo 4. Compare your implementation to our sample apps We’ve added sample apps to all of our SDKs. We use these sample apps to test changes to our SDKs, so we know they represent functioning apps. You can compare the appropriate sample app to your app and look for differences to pinpoint issues. iOS Android React Native Flutter 5. Check out our community We have a community where you can ask questions, share tips, and get help from other Customer.io users. Someone in the community may have encountered and solved the problem you’re having! 6. Gather logs and contact support If you do need to contact Customer.io support, you’ll first need to enable debug logging in your app and replicate the issue so we can see what’s happening in your app and help troubleshoot the problem. To enable debug logging, you’ll set logLevel to debug when you initialize the SDK. iOS iOS CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.logLevel = 'debug' } Android Android CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ) .logLevel("debug") .build() React Native React Native import { CustomerIO, CustomerioConfig } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug CustomerIO.initialize(env, data) Flutter Flutter import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "YOUR_SITE_ID", apiKey: "YOUR_API_KEY", region: Region.us, //config options go here logLevel: "debug" ), ); --- ## Get Started URL: https://docs.customer.io/journeys/sms-get-started/ We'll help you get set up with SMS so you can start sending messages to your customers. How it works While you’ll send SMS messages through Customer.io, SMS setup can be a bit complicated. If you send exclusively to people in the US or Canada, you might be able to setup SMS sending—including procuring your sender phone numbers—entirely through Customer.io. If you send messages to people outside of the US or Canada, you’ll need to set up a Twilio account and connect it to Customer.io—which is still very straightforward. Beyond that, you’ll need to store your audience’s phone numbers in the phone attribute so we know who to send messages to.  We send messages through Twilio’s US region You can send messages to people anywhere in the world, but we route data through Twilio’s US region. We don’t support Twilio’s regional APIs. This might be a problem if you have strict data residency requirements for things like GDPR compliance requiring EU data processing. Before you can send SMS messages Whether you send SMS support directly through Customer.io or Twilio, you’ll need to do a few things to meet the regulatory requirements to get your SMS phone numbers and start sending messages. You can still get set up with your provider (Customer.io or Twilio), but you should also do these things as soon as you can to avoid delays and get approved to send SMS faster. Update your privacy policy Update your terms and conditions Set up your opt-in and -out flow Beyond that, you’ll also have to articulate your SMS use cases when you register as a sender—regardless of whether you send through Customer.io or Twilio. Do I need to set up a Twilio account? In general, you can setup SMS without a Twilio account if you’re based in the US or Canada and you only send messages to people in the US or Canada. If you meet this criteria, you can sign up and we’ll help you get set up. If you don’t meet this criteria—or you want to use advanced Twilio analytics features like revenue attribution or sending queues—you’ll just set up an account through Twilio and connect it to Customer.io. flowchart LR B{Is your business based in the US or Canada?} B -.->|No| C[Work directly with TwilioManage phone numbers there] B -->|Yes| D{Do you send messages to people outside the US or Canada?} D -.->|Yes| C D -->|No| E{Do you need analytics like revenue attribution or sending queues?} E -.->|Yes| C E -->|No| G[You're eligible forCustomer.io native SMS!Contact support@customer.io] style G fill:#e1f5fe,stroke:#01579b,stroke-width:2px style C fill:#fff3e0,stroke:#e65100,stroke-width:2px If you’re eligible for Customer.io SMS If you’re eligible for our native SMS solution, you can sign up to manage your SMS phone numbers and billing in Customer.io. Once you’re signed up, we’ll help you set up your SMS sending in Customer.io. This involves gathering setting up sender phone numbers and other details. See Getting a phone number for more information. In particular, see the checklist on that page for the things you’ll need to do before you can get a phone number and start sending messages. Set up SMS through Twilio If you’re not eligible for our native SMS solution, you’ll need to set up a Twilio account and follow the process below to send SMS messages using Customer.io. To send SMS and MMS messages in Customer.io, you’ll need to have a Twilio account and the Sender phone number. This might be a regular phone number, short code, or an alphanumeric ID. Twilio can lease these numbers to you. Or, if you have a paid Twilio account with Alphanumeric Sending enabled, you can send messages from an Alphanumeric ID instead of a Twilio phone number. Set up a Twilio account if you don’t already have one. We recommend using a trial account to get started. Set up a Twilio-specific Sender if you don’t already have one. You can’t use your own phone number to send SMS; you need to purchase a number from Twilio. If you already set up your sender number, select it when you compose messages. In your Customer.io workspace, go to > Workspace Settings > SMS and add your Twilio Account SID and Auth Token. You’ll find these values in your Twilio dashboard.  Don’t use your test credentials You might see a set of test credentials on your Twilio account page. Don’t use these credentials! To send SMS with Customer.io, you need to use your live credentials. After you set up Twilio, we’ll sync your phone numbers from Twilio to Customer.io, and you’ll see the SMS message option in your workflows. Add people’s phone numbers in Customer.io No matter how you set up SMS, you need to store your audience’s phone numbers as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io so you can send them messages. We recommend that you call this attribute phone. If you store multiple phone numbers, you might label different phone numbers primary_phone, cell_phone, etc. You’ll select the phone number you want to use when you send your message. If you have multiple phone number values, you can use 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 pick the right one. {% if customer.primary_phone %} {{customer.primary_phone}} {% else %} {{customer.cell_phone}} {% endif %} Store phone numbers in E.164 format You should store phone numbers in E.164 format. If your numbers aren’t stored in this format, Twilio may not be able to detect your phone number format and messages may not make it to your audience. E.164 is the internationally-standardized format for all phone numbers, containing up to fifteen digits and usually written as shown below. See Twilio’s documentation for more information about E.164 phone numbers. [+][country code][subscriber number including area code] Here are a few examples: Country Local E.164 USA 415 555 2671 +14155552671 UK 020 7183 8750 +442071838750 China 021 6309 5246 +862163095246 Australia (08) 9287 8230 +61892878230  Twilio can send SMS messages almost anywhere We send SMS messages through Twilio, so we support all the countries Twilio supports. That accounts for most countries worldwide. But before you send SMS messages, you should check if there are any restrictions based on your recipients’ phone numbers. MMS messages When you set up SMS, you can also send messages with images. When you send an SMS message with an image, it becomes a multi-media message (MMS). MMS messages can help you make your point when 160 characters just aren’t enough. However, before you send an MMS message, you should take into account: Images must be PNGs, JPEGs, or GIFs. The URL you host must end in the image format for us to attach it to your message. Maximum image size is determined by recipient carrier, so you should limit image size to 600kb where possible. While our asset library accepts files up to 3MB, and Twilio sometimes allows attachments up to 5MB, the maximum image size for MMS is typically much smaller. Recipient carriers enforce their own size limits, many down to just 600kb. We recommend that you use the smallest possible images to ensure that your audience sees your image, regardless of their carrier and connection quality. If you use a short code or a toll-free carrier, your image may be limited to 600kb. Your message can also include up to 1600 characters. MMS messages are slightly more expensive than SMS messages. This is because MMS messages are typically larger than SMS messages; this pricing model is not unique to Twilio. But you may want to reserve MMS messages for your most meaningful mobile interactions. MMS messages are limited to the United States and Canada. MMS messages sent to someone in another country will fail. Link shortening By default, when you enable SMS, we automatically shorten links in your message body. Presently we use https://a.cx.io/lnk.abc123 (or https://e.cx.io/lnk.abc123 if you’re in our EU data center) as our format. Learn more about link shortening.  Preview links are for example only In your preview, you’ll see links that end in abc123. We don’t generate the shortened URL until send time. We show example links to help you see your links in context and check your character count. Link shortening prevents your links from taking up significant space in your messages. We track shortened links the same as we would any other link in your message; link shortening has no impact on link tracking.  Require logins if your links include personal information! Shortened links for your workspace expire after 90 days, and we’ll reuse link paths in new messages. If you link out to personal or sensitive information, you should require that people log in so that you don’t risk exposing sensitive information to people who click your links. Inbound messages We also support inbound, keyword-based messages from your audience. You can handle these messages with campaigns and events. See Inbound messages for more about handling replies from your audience. --- ## Send SMS/MMS messages URL: https://docs.customer.io/journeys/sms-send-messages/ Learn how to send SMS and MMS messages using Customer.io. Learn how to send smsShort Message Service: a text message, typically up to 160 characters, sent through a mobile carrier to a phone. and mmsMultimedia Message Service: a message sent through a mobile carrier to a phone that includes a picture or video. messages using Customer.io. Add SMS/MMS messages to your workflow Before you can send messages, you need to set up your Twilio account and enable SMS in your workspace. Drag SMS into your workflow. Give your message a Name that makes sense to you. Your audience won’t see this name. Click Settings in your message and update your message’s behavior.  Link tracking is on by default! Tracked links are long URLs and can take up a lot of space in your message. If you haven’t already, you should enable link shortening or disable link tracking. Learn more. Click Add Content to write up your message. In the From field, select the sender ID you want to send your message from.  Want to send from different numbers based on attributes or other criteria? You can use 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 send from different numbers based on attributes or other criteria. Learn more. In the To field, use liquid to set the customer’s phone number attribute—like {{customer.phone}} if you store your audience’s phone number in the phone attribute. (Optional) If you want to send an MMS message, enter a URL of your image (PNG, JPEG, JPG, or GIF) in the Image field. Your image must be smaller than 1.5MB. Enter the body of your message. SMS is limited to 160 characters per message, or 70 if you use some special characters like emojis. If you go over the character limit, your SMS will appear as multiple messages. See SMS “segments” or “credits” to learn more. While link tracking is on by default, you’ll want to use a liquid tag to group personalized links. Learn more.  Short links in your preview are for example only In your message preview, you’ll see links that end in abc123. We don’t generate the shortened URL until send time. We show example links to help you see your links in context and check your character count. Send a test message to test links in your message. Tracking links in messages By default, we track when people click links in SMS messages by appending your link with code that Customer.io understands. This means that tracked links are long URLs and can take up a lot of space in your message. If you want to keep this behavior, you should make sure that you enable link shortening in your workspace. If you don’t want to use Customer.io’s shortened links, then you should disable link tracking for your message. You’ll need to do this for each message individually. You can also enable or disable link tracking on a per-link basis using the {% cio_link url:"https://example.com" track:false %} 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}}. tag. The track parameter is a boolean where true enables tracking and false disables it. It’s true by default. {% cio_link url:"https://example.com" track:false %}  Require logins if your links include personal information! Shortened links for your workspace expire after 90 days, and we’ll reuse link paths in new messages. If you link out to personal or sensitive information, you should require that people log in so that you don’t risk exposing sensitive information to people who click your links. SMS “segments” or “credits” Your billing plan indicates how many SMS/MMS messages you can send before you incur overage fees. Twilio refers to these as Segments; if you’re billed for messages through Customer.io directly, we refer to them as Credits. Each SMS chat bubble, up to 160 characters, consumes a credit/segment. As you draft your message, we’ll show you how many credits your message consumes, so you know how much each message costs without having to count characters. While each chat bubble can contain up to 160 characters, characters like emojis limit your messages to 70 characters. We use smart encoding to prevent this from happening whenever possible, but this doesn’t work for all characters. The Segments/Credits field updates as you type to show you what your message costs without having to count characters or learn about encoding schemes.  MMS messages consume 3 credits Using an image in your message changes the encoding scheme for your message. This means that your message will consume 3 credits instead of 1. Send a test message To make sure your SMS works, you can send a test message clicking Send test… in the top right-hand corner of the composer. You’ll see a modal, into which you can enter a phone number to which you can send your message. 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. Using liquid to select your sender You may want to dynamically select between multiple Sender IDs at the time each message is sent. Liquid code can be added as a Manual Sender ID to accomplish this. For example, if you wanted to send from a different phone number depending on which Customer Success Manager is assigned to the customer, you could insert a code block such as: {% if customer.CSM == "stephen" %} +1647STEPHEN {% else if customer.CSM == "zack" %} +1800GOTZACK {% else %} +1800CUSTOMR {% endif %} Or if you set the phone number as a variable, you could simply use that variable in the From field. {{customer.csm_phone}} --- ## Link shortening URL: https://docs.customer.io/journeys/sms-link-shortening/ When you enable SMS and turn on the setting *Shorten links*, we shorten links in your SMS and WhatsApp message bodies. This page explains how link shortening works. How it works In Workspace settings > SMS, you’ll see a toggle for Shorten links. When this is enabled, we automatically shorten links in your SMS/MMS messages. When someone clicks a shortened link, the link directs them to Customer.io, where we’ll resolve the link and then send your audience to the ultimate destination. Shortened links are especially helpful with link tracking. By default, we track the links that people click in your SMS or WhatsApp messages, but tracked links won’t reliably fit inside a 160 character SMS message; shortened links fix that!  We filter short links for profanities We filter short link paths to ensure the randomly generated characters don’t contain profanities. If you see something inappropriate in a link path, please let us know. Short link format Shortened links use the format https://a.cx.io/lnk.abc123 (or https://e.cx.io/lnk.abc123 if you’re in our EU region). If you don’t use a custom domain, shortened links consume between 26 and 36 characters (out of the 160-maximum for SMS): 8 for the https:// prefix. 7 for the default domain (a.cx.io or e.cx.io). Your custom short link domain could consume fewer characters. 11-16 characters for the path (the slash and random characters /lnk.abc123). We start with a 10 character path and increase the length until we find a path that isn’t in use. Shortened links expire after 90 days. Enable or disable link shortening If you don’t want to shorten links, you can disable link shortening. But if you don’t shorten links, keep in mind that you probably won’t want to track links in your messages because tracked links consume significant space in your messages. Enabling or disabling link shortening affects all messages in your workspace—including active messages. Go to your > workspace settings > SMS. Turn Link shortening on or off. Add a custom short link domain if you want to use a custom domain. Otherwise, your shortened links will use the default domain (a.cx.io for the Americas or e.cx.io for our EU region). Add a custom short link domain Custom short link domains help you brand your links and make them easier for your audience to recognize. You can add up to 10 custom short link domains per workspace, which are shared across your SMS and WhatsApp channels; any domain you add is available for both channels, and you can set the default domain for each channel. As a part of this process, you’ll need to add CNAME and TXT records to your domain to verify that you own it and grant Customer.io permission to use it. Your custom short link domain cannot be the same domain you use for email link tracking. Go to your > Workspace settings > SMS or WhatsApp settings. Click Add custom domain. Enter your custom domain and click Add domain. Go to your domain registrar and add the appropriate records to your domain. The Add custom domain dialog contains the values you’ll need to copy to your domain host. CNAME record: points your custom domain to Customer.io’s servers. TXT record: verifies that you own the domain. The record name uses the format _cio.yourdomain.com. It can take up to 24 hours for these records to propagate, though it usually happens much faster. Until then, you’ll see that your custom domain is pending. In many cases, you can wait a minute or two and refresh the page to see that your custom domain is active. Use multiple short link domains You can add more than one custom short link domain to your workspace. This is helpful if you operate multiple brands, want to separate domains by message type, or want to rotate domains. When you have multiple custom domains, you’ll set one as the workspace default. This is the domain that all your shortened links use unless you override it for a specific message. Go to your SMS or WhatsApp settings and click Set as default next to the domain you want to use as the default for that channel; SMS and WhatsApp each have their own default domain. Track link-clicks in your messages By default, we track the links that people click in your SMS and WhatsApp messages. To do this, Customer.io appends tracking code to your URLs, which makes them much longer than the original link. These long URLs can easily push your message over the 160-character limit for SMS. Shortened links solve this problem by letting you track clicks without using up extra space. If you don’t use Customer.io’s shortened links, then you should disable link tracking for your message. You’ll need to do this for each SMS or WhatsApp message individually. You can also enable or disable link tracking on a per-link basis using the {% cio_link %} 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}}. tag with the track parameter; track is a boolean where true enables tracking and false disables it. It’s true by default. Enable or disable URL parameters You configure URL parameters in Workspace Settings. Check out URL parameters for more info. To track URL parameters, you must wrap links in the {% cio_link %} tag. {% cio_link url:"https://example.com" %} If you want to disable URL parameters for a specific link, set url_params to false: {% cio_link url:"https://example.com" url_params:false %} Disable link tracking for individual links To disable tracking for a specific link while keeping URL parameters enabled: {% cio_link url:"https://example.url" track:false %} Short link expiration Short link paths expire after 90 days. Your workspace can reuse paths after they expire, but immediate reuse is unlikely given the large number of possible combinations. While it’s unlikely that an expired path gets reused, or that users engage with messages older than 90 days, you should require a login if you send links to sensitive information. This ensures that you don’t expose personal information to users who click old links.  Do you need to extend the expiration period? If you’re on a premium or enterprise plan, contact us to request a longer expiration period for your shortened links. --- ## Getting started URL: https://docs.customer.io/journeys/inbound-sms/ People can reply to your SMS messages using keywords. You can use replies to track engagement or trigger campaigns. How it works SMS messages frequently include keywords that trigger downstream actions—like STOP to opt-out of messages or HELP to understand the things people can do through SMS replies. Customer.io handles these replies, helping you trigger campaigns and segment your audience based on replies to your messages. For example, you might ask people to reply to a message with a keyword if they want updates for a particular event or product. You can then add people who reply with the appropriate keyword to a segment and trigger a campaign to send them follow-up messages. Before you get started, you’ll need to: Make sure you’re set up for inbound messages Capture a phone attribute so we can trace inbound messages to the correct person in Customer.io Set up inbound message handling If you manage SMS sending entirely through Customer.io, this is already done for you. You don’t need to do anything to support inbound messages. If you have your own Twilio account, you need to point your messaging service’s Inbound Request Config to Customer.io. This forwards inbound messages to Customer.io. In your Twilio Console, go to the Develop tab and click Services. Select the messaging service you use with Customer.io and go to the Integration page. Under Incoming Messages, select Send a webhook and enter our URL: https://track.customer.io/inbound/sms for US customers or https://track-eu.customer.io/inbound/sms for EU customers. Click Save to apply the change. Now inbound messages are forwarded to Customer.io. You can test this by sending a message to your messaging service phone number and checking the Activity Log in Customer.io to check for your inbound message. You must use the phone attribute To successfully parse inbound messages, you must store phone numbers as a phone attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in E.164 format. For example, you might typically represent a phone number like 555-123-4567, but the E.164 format for that same phone number is +15551234567. When someone sends a message, we look for a person with the phone attribute that matches the phone number in the message. This works best if everybody in your workspace has a unique phone number. If you don’t set a phone number in E.164 format, we’ll do our best to parse numbers into this format when we resolve inbound messages to people in Customer.io—but we can’t guarantee that we’ll be able to do this. (We use Google’s libphonenumber library to parse phone numbers, and we skew towards US and Canadian numbers.) Reformatting phone numbers to E.164 You need to use this format in Customer.io so we can read them correctly. You typically write a US-based phone number like 555-123-4567 or a UK-based phone number like 020 7183 8750. But E.164 formatted-numbers look like +15551234567 or +442071838750. To format your phone numbers in E.164, you’ll follow this structure: A plus sign (+), which replaces the International Call Prefix (like 011) The International Country Calling Code (like 44 for the UK, 1 for North America) The Local Area Code The Local Phone Number (or Subscriber Number) Phone numbers should be unique To ensure that you don’t send duplicate messages and help us attribute inbound messages to the correct person, you should make sure that each person has a unique phone number in your workspace. While we recommend that you store unique phone numbers for each person, people in Customer.io can have the same phone number. If two people have the same phone number, and both of those people enter a campaign that sends SMS messages, you could send duplicate messages to the same phone number. If two people have the same phone number and you get an inbound message from that number, we’ll attribute inbound messages to the oldest profile—the person who was created first in your workspace. If two people share a phone number, and you receive an opt-out keyword from that number, we’ll opt both people out of messages. Opt-in and opt-out keywords affect a phone number, so everybody with the same phone number is opted in or out of messages when we receive an opt-in or opt-out keyword from that number. Inbound messages for unrecognized numbers If we can’t match an inbound message to a person we treat the inbound message as an anonymous event. You can’t respond to anonymous people with SMS messages in Customer.io. This typically happens when: You receive an SMS from someone you haven’t identified Your phone attributes aren’t in the proper E.164 format. Even if you identify the person who sent the inbound message, the inbound message will remain anonymous. There’s no way to reconcile the anonymous inbound message with the identified person later. We automatically handle opt-in and opt-out keywords You don’t need to set up anything special to handle opt-out or opt-in keywords. We automatically opt people out of messages from appropriate numbers when they reply with keywords like STOP or UNSUBSCRIBE—or opt them in if they reply with keywords like START or UNSTOP. You can add people to segments based on their opt-out status using the Opt out condition. Then you can use this segment to target (or exclude) people who’ve opted out of a particular sender number. Twilio automatically responds to STOP keywords When someone responds with a STOP keyword, Twilio sends users a reply indicating that they’ve been opted out. This message doesn’t go through Customer.io and isn’t logged by us. When someone opts out of messages this way, Customer.io logs the inbound message containing the STOP keyword and reflects the person’s opt-out status, but won’t reflect the SMS response from Twilio. You can customize this response in the Twilio console using Twilio’s Advanced Opt-Out feature. --- ## Inbound statuses and activities URL: https://docs.customer.io/journeys/inbound-metrics/ We show activities for inbound messages to help you understand who replied to your messages. If you expect people to respond to a message as a call to action, then you can use these activities to track and troubleshoot responses.  Have you set up inbound messaging? You need to set up inbound messaging to see inbound metrics and activities. See Getting started for more information. The Replied status Most messages have statuses like sent, opened, clicked, and so on. For SMS and MMS messages, we also track replied. While sometimes your call to action might be for users to click a link, SMS lets you set an action item as a reply to a message with a particular keyword. The replied status shows you if someone replied to your message. Replies in the Activity Log When you look at a person’s activity or the Activity Log, you can sort by the Replied to SMS activity. This will show you replies to SMS messages. Each activity shows the conversation and the complete reply. You might look at these activities to see replies in context or troubleshoot messages to see if inbound messages did or didn’t contain the right keywords to trigger a reply. Use replies as conversion goals Unlike events, you can’t use an inbound message directly as a conversion goal. You can, however, use a reply as criteria for a segment and treat segment membership as the goal. So, for example, if your goal for a campaign is to get replies to a survey, you could create a segment containing people who replied to the survey and use that segment as your conversion criteria. Attributing replies to your messages We attribute replies to the original delivery if the reply occurs within 72 hours of the original delivery. If the reply occurs outside the 72 hour window, we won’t attribute the reply to the original delivery. This means that replies sent outside the 72 hour window aren’t counted as conversions or logged as a metric for a particular message, campaign, broadcast, etc. --- ## Respond to inbound keywords URL: https://docs.customer.io/journeys/inbound-campaigns/ When people send you SMS messages, you can use campaigns to handle the incoming messages and trigger responses. How it works We treat inbound messages like events in Customer.io: they can trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. and add people to segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. We handle inbound messages by looking for keywords. Think of it a bit like the menu you hear when you call somewhere that has an automated phone system: the phone system prompts the user to press an option on the keypad to advance to the next message. Except in this case, rather than pressing the right button to advance in a menu, the user responds with a keyword from a list of supported keywords, and you send them down a path based on the keyword they used. Set up an inbound message campaign When you set up a campaign, click the Trigger. Select the Event trigger option. Change they perform the event to they send an inbound SMS. The Inbound message event that triggers your campaign doesn’t differentiate between replies to different phone numbers. If you want to handle inbound replies to your different sender numbers independently, you should add conditions to your trigger based on the to value. (Inbound messages come from your audience and to your SMS “sender” number.) Using inbound message data Like other event-triggered campaigns, inbound message variables start with event. For example, referencing the keyword variable would be {{event.keyword}}. Most of these values come directly from the inbound message, but there are rules to the keyword variable. { "body": "start", "from": "+15551234567", "keyword": "START", "messaging_service_sid": "MG1c4d997a947f60dfaddf382a32dffc11", "sid": "SMec32f5db600322613b0c592d4d7417f1", "to": "+15559876543" } Variable Description body The body of the inbound message from The phone number of the person who sent the inbound message keyword The keyword that the person used, if we can parse it from the body of the inbound message messaging_service_sid The Twilio messaging service SID that the inbound message came from sid Twilio’s unique ID for the inbound message to The phone number that the inbound message was sent to, also the “sender” of the message the person replied to Keywords We support the pre-defined keywords below. A person can respond with any of the supported terms—lowercase, uppercase, or any combination of cases—and we’ll match on the appropriate keyword. This simplifies the logic when you respond to inbound messages. You don’t have to account for each possible variation of our pre-defined keywords. Keyword Supported terms Description STOP STOP, CANCEL, UNSUBSCRIBE, OPTOUT, END, QUIT, REVOKE, STOPALL Opt-out keywords that unsubscribe the user from SMS messages START START, UNSTOP Opt-in keywords that resubscribe the user to SMS messages HELP HELP, INFO Keywords that trigger help or information responses YES YES, Y, OK, 👍 Affirmative response keywords NO NO, N, 👎 Negative response keywords Beyond our pre-defined terms, you can also tell people to respond with custom keywords. For example, if you want to run a survey, you could ask people to respond 1-5 to indicate satisfaction. If an inbound message contains multiple words, the keyword variable will be empty. If this happens, you can respond with a message that tells the user to try again with one of the supported keywords. flowchart LR B{"Does inbound message contain multiple words?"} B-->|No|d{Does inbound message have a recognized keyword?} d-->|Yes|e(Customer.io pre-defined keyword) d-.->|No|f(Custom keyword) B-..->|Yes|c(No keyword recognized) Example campaign and best practices In the campaign below, you’ll see that we’ve done a few things: We’ve set up a Multi-split branch to handle different keywords—STOP, START, and HELP. We’ve also added another branch to handle cases where we don’t recognize the incoming keyword. For the STOP keyword, we can’t send a follow-up message. Instead, we set an attribute on the person to help us track that they’ve opted-out of SMS messaging for that number. This lets us add people to a segment that we can include or exclude from different campaigns. Filter by sender number or outbound message If you want to handle keywords differently based on the sender number or a specific message, you can add that as a Filter to your campaign trigger. Set up branches to handle different keywords As you can see below, we’re handling branches for STOP, START, and HELP. We’ve also added a branch to handle cases where we don’t recognize the incoming keyword. We automatically treat all opt-out keywords as STOP and all opt-in keywords as START; this means you don’t have to create conditions for every possible matching keyword. You cannot wait for subsequent messages in your campaign In your campaign, you can’t wait for another inbound message or a follow-up event. You have to handle cascading inbound messages with subsequent campaigns. You can still use the Wait Until action to wait for other conditions, but you cannot currently wait for a separate inbound message to continue through a campaign. When to limit send rates If you want to send an SMS broadcast or newsletter, and you want to respond to inbound messages on the same “sender” number in Customer.io, you should throttle your broadcast so that your inbound-based responses aren’t delayed by messages queued from your broadcast. Imagine you send a broadcast to 100,000 people. SMS messages from a single phone number send at a maximum rate of 225 messages per second. This would take around 444 seconds to send, or around 7 minutes. If someone receives your message and responds immediately, and any subsequent message you try to send is added to the end of the queue. This means that any subsequent messages you send to your audience are be delayed by the time it takes to finish sending your broadcast. Throttling broadcasts and newsletters ensures that your sender phone number can continue to send messages as it normally would, without being blocked by messages queued from your broadcast.  You can’t limit campaign send rates this way When you start campaigns triggered by segments or attribute changes, you can let people who already meet your conditions enter the campaign immediately. If you choose this option, you can’t limit the send rate from, or the number of people who enter, your campaign. This could cause a queue of SMS messages that delays other campaigns or broadcasts that rely on the same sender numbers. --- ## Senders URL: https://docs.customer.io/journeys/sender-phone-numbers/ When you enable SMS in Customer.io, we'll sync your phone numbers and short codes. This page explains a bit more about how to add new phone numbers so you can select different senders for different messages. Syncing Phone Numbers (Senders) to Customer.io If you add or change any of your sender identities in your Twilio account, you’ll need to re-sync them in Customer.io before you’ll see your changes in Customer.io. Go to > Workspace Settings > SMS and click Sync from Twilio. Using a Manual Sender ID You’ll see Manual Sender IDs in your Twilio Settings. You can add an Alphanumeric Sender ID, a Message Service ID, or provide some Liquid that dynamically determines the Sender ID for each message here. Alphanumeric ID Alphanumeric IDs can help your messages stand out, but they have a few quirks: They’re one-way: Your customers cannot reply to them. Opt-in/opt-out: Customers must opt into messages from your ID, and be informed on how to opt out. Alphanumeric IDs aren’t supported in all countries, including the USA: If you try to send to an unsupported country from an Alphanumeric Sender ID, your message will fail to send. Set up an alphanumeric ID Enable alphanumeric IDs in Twilio first! You can add alphanumeric senders in Customer.io, but if you haven’t enabled the feature in Twilio, messages using your alphanumeric sender will fail. Go to > Workspace Settings > SMS and click Add Sender ID. Enter your Sender ID and the Name you’ll use to refer to your sender. Name: this is how you’ll refer to the ID internally; your customers won’t see this. Sender ID: You can use both upper- and lower-case ASCII letters, digits 0 through 9, and spaces in your ID. Your ID must contain at least one character that isn’t a number. --- ## Tracking URL: https://docs.customer.io/journeys/tracking-twilio-messages/ We track the status of SMS/MMS messages, so you can track the success of your campaigns and segment users based on the messages you've sent them in the past. SMS/MMS delivery logs When you send an SMS or MMS message, you can view the delivery logs for that message in the campaign’s sent messages page. Here you can correct and retry failed messages. Message failures are sometimes related to problems with liquid in your message (missing attributes, etc). In delivery logs, you’ll see: The recipient The name of the message The status of the message (sent or failed) Date and time sent Conversions after receiving the message Create a segment based on SMS/MMS messages As with an email, you can create a segment for SMS’s that have been sent, converted, etc. When you create a segment, select Message Data > SMS as the condition you want to match. You can then use the names of your messages to filter people who’ve received, or not received, messages. You might do this to send follow-up messages to engage with people depending on how they responded to a previous message. --- ## Frequently Asked Questions URL: https://docs.customer.io/journeys/faq-twilio/ Answers to common questions about SMS messages with Twilio. Does Customer.io manage my Twilio account? No. While Customer.io partners with Twilio to send SMS messages, we can’t set up or manage your Twilio account for you. Your Twilio account includes your sender numbers and other things that we can’t manage on your behalf—so you can make changes to your senders, keywords, and so on without having to wait on us to make changes to your account. What countries can I send SMS messages to? We send SMS messages through Twilio, so we support all the countries Twilio supports. That accounts for most countries worldwide. But, depending on where you send messages, there are a a couple of things you should know: Twilio only supports MMS messages for the US and Canada. Twilio has some guidelines for sending messages outside of North America. If you already know where you plan to send messages, you can check if there are any restrictions you need to be aware of before you start sending messages. Can an SMS trigger a conversion in my campaigns? Conversions in Customer.io track the last email or communication sent before the user performs an event, enters a conversion segment, or leaves a conversion segment. If you track users performing an action, entering a segment, or leaving a segment after receiving an SMS, then the Twilio action is considered converted. How does Twilio deal with SMS unsubscribes? If a user unsubscribes via SMS, he/she would no longer be able to receive SMS communication through your campaign. This is because the user will opt out of receiving messages from your Twilio phone number, and this decision is handled directly by Twilio. What if a user no longer wants to receive an SMS? These preferences are managed by Twilio. By default, Twilio will handle English-language messages on long codes in accordance with industry standards. These messages include STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, and QUIT, which a customer can use to stop receiving messages from your Twilio phone number. What if a user stopped receiving messages but wants to receive them again? Just like unsubscribes, these preferences are managed by Twilio. Twilio uses START, YES and UNSTOP long codes to opt customers back in to receiving messages from your Twilio phone number. How can I see if my SMS was delivered successfully? You would want to view successfully completed messages in the Campaign Sent tab, which will show a state of Sent, Failed, Bounced, or Delivered. You can also filter for these SMS messages in the Delivery Log. More about tracking sent SMS messages in Customer.io Can I segment based on SMS? Yes! To segment for an SMS, you can use the segment builder as normal: Do you support short codes? Yes we do! If you’ve leased a short code, or Twilio has done so on your behalf, we sync the sending of Customer.io Twilio messages with them. In Customer.io, they’ll sync just like any other Twilio numbers you have. To learn more about how short codes work with Twilio (as well as look at pricing information), you can learn more on Twilio’s documentation page. Can I send using Alphanumeric IDs? Yes! But there are a few important things to keep in mind when you do: Enable the feature in Twilio first! You can add these in Customer.io, but if you haven’t enabled it in your Twilio project settings, the message will fail. They’re one-way: Your customers cannot reply to them. Opt-in/opt-out: Customers receiving these should have opted in to your service and be informed on how to opt out. They’re not supported in all countries, including the USA: If you try to send to an unsupported country from an Alphanumeric Sender ID, your SMS will fail to send. Note that some require sender ID pre-registration with Twilio. If you’ve enabled Twilio in Customer.io and want to send using Alphanumeric IDs, here’s how. Can I use Liquid to determine the Sender ID? Yes! Liquid code can be added as a Manual Sender ID to accomplish this. For example, if you wanted to send from a different phone number depending on which Customer Success Manager is assigned to the customer, you could insert a code block such as: {% if customer.CSM == "stephen" %} +1647STAPHEN {% else if customer.CSM == "zack" %} +1800GOTZACK {% else %} +1800CUSTOMR {% endif %} Can I send WhatsApp messages through Twilio in Customer.io? Yes! When sending WhatsApp messages through Twilio, the message should align with one of Twilio’s predefined categories. After you create a WhatsApp message template in Twilio, you will need to submit the template for approval as described here. Once approved, you can send WhatsApp messages in Customer.io by following the instructions here. Do shortened links expire? Shortened links expire after 90 days. We reuse paths, but only within your workspace so that someone interacting with an old link will never see links or information sent from another workspace. But reusing links means that if you send a shortened link, you should avoid using URLs that include personal details (like a customer’s name). If you send links to personal information, you should require that people log in to your site to access the information. --- ## Smart character encoding URL: https://docs.customer.io/journeys/sms-character-encoding/ SMS messages typically support up to 160 characters. But some characters change the encoding scheme, and would otherwise drop your message to 70 characters. To prevent this sort of issue, we automatically replace certain characters with their GSM 7-bit unicode equivalents to maximize the number of characters in your message. How it works By default, SMS messages use the GSM 7-bit character set, which supports up to 160 characters. Going over the character limit will cause your message to split into multiple messages—which may not be a great experience for your customers and each message (called a segment or a credit depending on your plan) is counted towards your bill! While the default character set covers roughly 128 common symbols, it doesn’t cover all languages or symbols you might use in text messages. When you use a character outside the GSM 7-bit set, text messages switch to 16-bit UCS-2 encoding. The increased bit depth means that you can only send 70 characters in your message. To prevent this problem, we automatically replace certain characters with their GSM 7-bit equivalents to maximize the number of characters in your messages—so you don’t have to worry about complicated encoding schemes and message length limits. This is all a fancy way of saying that we encode messages to support 160 characters whenever possible. Some symbols still consume more than one character You’ll notice that, while we replace many characters with their GSM 7-bit equivalents, some symbols still consume more than 1 character after we replace them. For example, we replace the trademark symbol ™ with (TM), which takes up 4 characters in your message. This just happens to be the most efficient way to represent the symbol in a 160 character message. Emojis limit messages to 70 characters Emojis are a special case. They’re not part of the GSM 7-bit character set, and we can’t replace them with their GSM 7-bit equivalents—like we do with other unicode characters. So, when you use emojis, you’re limited to 70 characters in your message. Each emoji is counted as 2 of the 70 available characters. Smart encoding character replacements By default, we replace the following characters with their GSM 7-bit equivalents: UNICODE ORIGINAL REPLACEMENT ‘`’ ` ' ‘\u00a2’ ¢ cents ‘\u00a8’ ¨ " ‘\u00a9’ © (C) ‘\u00aa’ ª a ‘\u00ab’ « « ‘\u00ac’ ¬ NOT ‘\u00ad’ - ‘\u00ae’ ® (R) ‘\u00af’ ¯ - ‘\u00b0’ ° deg ‘\u00b1’ ± +/- ‘\u00b2’ ² ^2 ‘\u00b3’ ³ ^3 ‘\u00b4’ ´ ' ‘\u00b5’ µ u ‘\u00b6’ ¶ P ‘\u00b7’ · . ‘\u00b8’ ¸ , ‘\u00ba’ º o ‘\u00bb’ » » ‘\u00bc’ ¼ 1/4 ‘\u00bd’ ½ 1/2 ‘\u00be’ ¾ 3/4 ‘\u00c0’ À A ‘\u00c1’ Á A ‘\u00c2’ Â A ‘\u00c3’ Ã A ‘\u00c8’ È E ‘\u00ca’ Ê E ‘\u00cb’ Ë E ‘\u00cc’ Ì I ‘\u00cd’ Í I ‘\u00ce’ Î I ‘\u00cf’ Ï I ‘\u00d0’ Ð D ‘\u00d2’ Ò O ‘\u00d3’ Ó O ‘\u00d4’ Ô O ‘\u00d5’ Õ O ‘\u00d7’ × x ‘\u00d9’ Ù U ‘\u00da’ Ú U ‘\u00db’ Û U ‘\u00dd’ Ý Y ‘\u00de’ Þ TH ‘\u00e1’ á a ‘\u00e2’ â a ‘\u00e3’ ã a ‘\u00e7’ ç c ‘\u00ea’ ê e ‘\u00eb’ ë e ‘\u00ed’ í i ‘\u00ee’ î i ‘\u00ef’ ï i ‘\u00f0’ ð d ‘\u00f3’ ó o ‘\u00f4’ ô o ‘\u00f5’ õ o ‘\u00f7’ ÷ / ‘\u00fa’ ú u ‘\u00fb’ û u ‘\u00fd’ ý y ‘\u00fe’ þ th ‘\u00ff’ ÿ y ‘\u01c3’ ǃ ! ‘\u0262’ ɢ G ‘\u026a’ ɪ I ‘\u0274’ ɴ N ‘\u0280’ ʀ R ‘\u028f’ ʏ Y ‘\u0299’ ʙ B ‘\u029c’ ʜ H ‘\u029f’ ʟ L ‘\u02b9’ ʹ ' ‘\u02ba’ ʺ " ‘\u02bb’ ʻ ' ‘\u02bc’ ʼ ' ‘\u02bd’ ʽ ' ‘\u02c6’ ˆ ^ ‘\u02c8’ ˈ ' ‘\u02ca’ ˊ ' ‘\u02cb’ ˋ ' ‘\u02dc’ ˜ ~ ‘\u02ee’ ˮ " ‘\u02f7’ ˷ ~ ‘\u0302’ ̂ ^ ‘\u0303’ ̃ ~ ‘\u0313’ ̓ ' ‘\u0314’ ̔ ' ‘\u0330’ ̰ ~ ‘\u0332’ ̲ _ ‘\u0334’ ̴ ~ ‘\u0337’ ̷ / ‘\u0338’ ̸ / ‘\u0347’ ͇ = ‘\u1d00’ ᴀ A ‘\u1d04’ ᴄ C ‘\u1d05’ ᴅ D ‘\u1d07’ ᴇ E ‘\u1d0a’ ᴊ J ‘\u1d0b’ ᴋ K ‘\u1d0d’ ᴍ M ‘\u1d0f’ ᴏ O ‘\u1d18’ ᴘ P ‘\u1d1b’ ᴛ T ‘\u1d1c’ ᴜ U ‘\u1d20’ ᴠ V ‘\u1d21’ ᴡ W ‘\u1d22’ ᴢ Z ‘\u1dcd’ ᷍ ^ ‘\u2010’ ‐ - ‘\u2011’ ‑ - ‘\u2012’ ‒ - ‘\u2013’ – - ‘\u2014’ — - ‘\u2015’ ― - ‘\u2017’ ‗ __ ‘\u2018’ ' ' ‘\u2019’ ' ' ‘\u201a’ ‚ , ‘\u201b’ ‛ ' ‘\u201c’ " " ‘\u201d’ " " ‘\u201e’ „ ,, ‘\u201f’ ‟ " ‘\u2020’ † + ‘\u2021’ ‡ ++ ‘\u2022’ • * ‘\u2023’ ‣ > ‘\u2024’ ․ . ‘\u2025’ ‥ .. ‘\u2026’ … … ‘\u2027’ ‧ - ‘\u2028’ ‘\u2029’ ‘\u2030’ ‰ /1000 ‘\u2031’ ‱ /10000 ‘\u2032’ ′ ' ‘\u2033’ ″ '' ‘\u2034’ ‴ ’'' ‘\u2035’ ‵ ' ‘\u2036’ ‶ '' ‘\u2037’ ‷ ’'' ‘\u2038’ ‸ ^ ‘\u2039’ ‹ < ‘\u203a’ › > ‘\u203b’ ※ * ‘\u203c’ ‼ !! ‘\u203d’ ‽ ?! ‘\u203e’ ‾ - ‘\u2043’ ⁃ - ‘\u2044’ ⁄ / ‘\u2045’ ⁅ [ ‘\u2046’ ⁆ ] ‘\u2047’ ⁇ ?? ‘\u2048’ ⁈ ?! ‘\u2049’ ⁉ !? ‘\u204a’ ⁊ & ‘\u204b’ ⁋ P ‘\u204c’ ⁌ < ‘\u204d’ ⁍ > ‘\u204e’ ⁎ * ‘\u204f’ ⁏ ; ‘\u2051’ ⁑ ** ‘\u2052’ ⁒ - ‘\u2053’ ⁓ ~ ‘\u2054’ ⁔ ~ ‘\u2055’ ⁕ * ‘\u2056’ ⁖ … ‘\u2057’ ⁗ ’’'' ‘\u2058’ ⁘ …. ‘\u2059’ ⁙ ….. ‘\u205a’ ⁚ .. ‘\u205b’ ⁛ …. ‘\u205c’ ⁜ + ‘\u205d’ ⁝ : ‘\u205e’ ⁞ : ‘\u2070’ ⁰ ^0 ‘\u2071’ ⁱ ^i ‘\u2072’ ⁲ ^n ‘\u2073’ ⁳ ^m ‘\u2074’ ⁴ ^4 ‘\u2075’ ⁵ ^5 ‘\u2076’ ⁶ ^6 ‘\u2077’ ⁷ ^7 ‘\u2078’ ⁸ ^8 ‘\u2079’ ⁹ ^9 ‘\u207a’ ⁺ ^+ ‘\u207b’ ⁻ ^- ‘\u207c’ ⁼ ^= ‘\u207d’ ⁽ ^( ‘\u207e’ ⁾ ^) ‘\u207f’ ⁿ ^n ‘\u2080’ ₀ _0 ‘\u2081’ ₁ _1 ‘\u2082’ ₂ _2 ‘\u2083’ ₃ _3 ‘\u2084’ ₄ _4 ‘\u2085’ ₅ _5 ‘\u2086’ ₆ _6 ‘\u2087’ ₇ _7 ‘\u2088’ ₈ _8 ‘\u2089’ ₉ _9 ‘\u208a’ ₊ _+ ‘\u208b’ ₋ _- ‘\u208c’ ₌ _= ‘\u208d’ ₍ _( ‘\u208e’ ₎ _) ‘\u208f’ ₏ _y ‘\u20a9’ ₩ KRW ‘\u20b9’ ₹ INR ‘\u20ba’ ₺ TRY ‘\u20bd’ ₽ RUB ‘\u20d2’ ⃒ ' ‘\u20d3’ ⃓ ' ‘\u20e5’ ⃥ \ ‘\u2122’ ™ (TM) ‘\u2150’ ⅐ 1/7 ‘\u2151’ ⅑ 1/9 ‘\u2152’ ⅒ 1/10 ‘\u2153’ ⅓ 1/3 ‘\u2154’ ⅔ 2/3 ‘\u2155’ ⅕ 1/5 ‘\u2156’ ⅖ 2/5 ‘\u2157’ ⅗ 3/5 ‘\u2158’ ⅘ 4/5 ‘\u2159’ ⅙ 1/6 ‘\u215a’ ⅚ 5/6 ‘\u215b’ ⅛ 1/8 ‘\u215c’ ⅜ 3/8 ‘\u215d’ ⅝ 5/8 ‘\u215e’ ⅞ 7/8 ‘\u2202’ ∂ partial ‘\u2207’ ∇ nabla ‘\u220f’ ∏ prod ‘\u2211’ ∑ sum ‘\u221a’ √ sqrt ‘\u221d’ ∝ prop ‘\u221e’ ∞ inf ‘\u2220’ ∠ angle ‘\u2221’ ∡ mangle ‘\u2222’ ∢ sangle ‘\u2224’ ∤ ! ‘\u2225’ ∥ ‘\u2226’ ∦ ! ‘\u2227’ ∧ and ‘\u2228’ ∨ or ‘\u2229’ ∩ intersect ‘\u222a’ ∪ union ‘\u222b’ ∫ int ‘\u2248’ ≈ ~= ‘\u2260’ ≠ != ‘\u2264’ ≤ <= ‘\u2265’ ≥ >= ‘\u2282’ ⊂ subset ‘\u2283’ ⊃ superset ‘\u2284’ ⊄ !subset ‘\u2285’ ⊅ !superset ‘\u2286’ ⊆ subseteq ‘\u2287’ ⊇ supseteq ‘\u2288’ ⊈ !subseteq ‘\u2289’ ⊉ !supseteq ‘\u228a’ ⊊ subsetneq ‘\u228b’ ⊋ supersetneq ‘\ua730’ ꜰ F ‘\ua731’ ꜱ S ‘\ufe10’ ︐ ' ‘\ufe11’ ︑ ' ‘\ufe13’ ︓ : ‘\ufe14’ ︔ ; ‘\ufe15’ ︕ ! ‘\ufe16’ ︖ ? ‘\ufe50’ ﹐ , ‘\ufe51’ ﹑ , ‘\ufe52’ ﹒ . ‘\ufe54’ ﹔ ; ‘\ufe56’ ﹖ ? ‘\ufe57’ ﹗ ! ‘\ufe59’ ﹙ ( ‘\ufe5a’ ﹚ ) ‘\ufe5b’ ﹛ { ‘\ufe5c’ ﹜ } ‘\ufe5f’ ﹟ # ‘\ufe60’ ﹠ & ‘\ufe61’ ﹡ * ‘\ufe62’ ﹢ + ‘\ufe63’ ﹣ - ‘\ufe64’ ﹤ < ‘\ufe65’ ﹥ > ‘\ufe66’ ﹦ = ‘\ufe68’ ﹨ \ ‘\ufe69’ ﹩ $ ‘\ufe6a’ ﹪ % ‘\ufe6b’ ﹫ @ ‘\uff01’ ! ! ‘\uff02’ " " ‘\uff03’ # # ‘\uff04’ $ $ ‘\uff05’ % % ‘\uff06’ & & ‘\uff07’ ' ' ‘\uff08’ ( ( ‘\uff09’ ) ) ‘\uff0a’ * * ‘\uff0b’ + + ‘\uff0c’ , , ‘\uff0d’ - - ‘\uff0e’ . . ‘\uff0f’ / / ‘\uff10’ 0 0 ‘\uff11’ 1 1 ‘\uff12’ 2 2 ‘\uff13’ 3 3 ‘\uff14’ 4 4 ‘\uff15’ 5 5 ‘\uff16’ 6 6 ‘\uff17’ 7 7 ‘\uff18’ 8 8 ‘\uff19’ 9 9 ‘\uff1a’ : : ‘\uff1b’ ; ; ‘\uff1c’ < < ‘\uff1d’ = = ‘\uff1e’ > > ‘\uff1f’ ? ? ‘\uff20’ @ @ ‘\uff21’ A A ‘\uff22’ B B ‘\uff23’ C C ‘\uff24’ D D ‘\uff25’ E E ‘\uff26’ F F ‘\uff27’ G G ‘\uff28’ H H ‘\uff29’ I I ‘\uff2a’ J J ‘\uff2b’ K K ‘\uff2c’ L L ‘\uff2d’ M M ‘\uff2e’ N N ‘\uff2f’ O O ‘\uff30’ P P ‘\uff31’ Q Q ‘\uff32’ R R ‘\uff33’ S S ‘\uff34’ T T ‘\uff35’ U U ‘\uff36’ V V ‘\uff37’ W W ‘\uff38’ X X ‘\uff39’ Y Y ‘\uff3a’ Z Z ‘\uff3b’ [ [ ‘\uff3c’ \ \ ‘\uff3d’ ] ] ‘\uff3e’ ^ ^ ‘\uff3f’ _ _ ‘\uff5b’ { { ‘\uff5c’ | ‘\uff5d’ } } ‘\uff5e’ ~ ~ ‘\uff61’ 。 . ‘\uff64’ 、 , --- ## Opt-out keyword handling URL: https://docs.customer.io/journeys/sms-keyword-opt-out/ People can opt-out of SMS messages by sending an opt-out keyword to your Twilio sender number—like `STOP` or `UNSUBSCRIBE`. In Customer.io, we'll respect your users' preferences and show you which senders people have opted-out of, so you can see whether or not they're eligible to receive your messages. How it works While Customer.io doesn’t receive SMS replies directly, we capture opt-in and opt-out information from Twilio so that we respect your audience’s preferences and show you which numbers people have opted out of. We show a list of opt-outs in the SMS Opt-outs section of any person’s profile. This list updates in near real-time as people opt out or back into SMS messaging for a given phone number.  Opt-outs affect all profiles with the same phone number People can share the same phone number in Customer.io. When someone sends an inbound SMS to opt out of messages from an SMS sender identity, any profiles in the workspace that share the same phone numbers are also opted out of SMS messaging. This list only shows explicit opt-outs for a given phone number. While it updates if someone opts out or into SMS messaging for a given phone number, it doesn’t show the numbers people have opted into. flowchart LR a{Has user opted into sender in the past?} a-->|Yes|b{Has user sent an opt-out keyword to this sender?} b-->|Yes|c(Number is on the opt-outs list) a-..->|No: user won't get SMS from this number|d(Number doesn't appear on the opt-outs list) b-.->|No: user can still get SMS from this number|d  This feature does not handle other opt-out or opt-in methods This feature only reflects opt-out keywords sent to Twilio. It doesn’t handle other opt-out methods, like if you change someone’s opt-in status manually in the Twilio console. Learn more Override an opt-out You might want to remove someone from the list if they were mistakenly marked as opted out or if you change their opt-in status manually in Twilio. Note that removing an opt-out entry only removes the opt-out from the person in Customer.io; it doesn’t change their opt-out status in Twilio. If a person is still opted out of messages in Twilio and you try to send that person a message, Twilio will block your message and their opt-out will re-appear in Customer.io. To override an opt-out: Go to the People page. Select the person whose opt-out you want to override. Scroll to the SMS Opt-outs section and click Manage. Click Remove next to the number you want to override. Segment users by SMS opt-out status When you send an SMS message, we’ll automatically ignore members of your audience who’ve opted out of messages. You might segment people who’ve opted into or out of SMS messages to target the right people with SMS messages. When you create a segment, you can use the Opt out condition to include or exclude people who’ve opted out of a particular sender number. Then you can use this segment as a filter for messages. SMS opt-out in campaigns When someone opts out of SMS messages but reaches an SMS message in a campaign, we’ll skip the message. While this ensures that your campaigns comply with various regulatory requirements, it means you may skip parts of your campaign workflow for certain users. You can handle this situation gracefully by setting up segments containing people who’ve opted out of SMS messages and then filter people based on their opt-out status: Use True/False branches to send someone alternative messages if they opt out of SMS. Filter users out of your attribute or segment-triggered campaign entirely by setting up a condition for the campaign that excludes people who are in your opt-out segment. Filter users out of your event-triggered campaign entirely by setting a campaign filter to exclude people who belong to your “people who opted out of SMS” segment. If you used Twilio before joining Customer.io If you’ve used Twilio before joining Customer.io, you may have stored opt-out statuses that aren’t reflected in Customer.io. Contact Customer.io to ask us about migrating opt-outs from Twilio to Customer.io. While this isn’t strictly necessary—any message sent from Customer.io through Twilio to a person who’s opted out of SMS will be blocked by Twilio—migrating opt-outs to Customer.io prevents you from skewing your delivery metrics as you populate opt-out information in Customer.io. It also can save you a bit of money: each message “bounced” by Twilio costs 1/10th of a message credit, which can add up as you populate opt-out information in Customer.io. Migrate from a webhook-based opt-out solution  If you’re new to Customer.io, you can skip this section. If you started sending SMS messages after July, 2025, you can skip this section. We’ll automatically capture opt-out status for you. Before we supported SMS opt-out keywords in Customer.io, you might have set up a webhook to pass opt-out statuses to Customer.io and them in an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You don’t need to do that anymore; our new solution captures opt-out status from Twilio automatically. But, to bridge your old opt-out data with our new opt-out solution, you’ll want to: Disable your opt-out webhook. Set up inbound messaging in Twilio. This is how we’ll get opt-out statuses from Twilio. Set up segments that capture both the old opt-out attribute and the newer opt-out per phone number. For example, to capture users who are opted-out of SMS messages from a particular sender number, your segment should allow Any of the following conditions: The person’s opt-out attribute is true. The person is opted-out of SMS messages from a particular sender number. FAQ What if someone opts out without using a keyword? Imagine someone wants to opt out of SMS, but they tell you via phone call, email, or another channel. In this case, you’ll need to manually update their status in Twilio. This will not automatically update their opt-out status in Customer.io until you attempt to send them a message. That’s when we’ll check Twilio for opt-out information. A user opted-out but I don’t see them on the opt-outs list In general, this probably means that the opt-out wasn’t triggered by a keyword. But we also rely on Twilio to tell us about opt-outs. If someone sends an opt-out keyword but it isn’t reflected in Customer.io, it’s possible that there was an interruption in the service we use to capture opt-out information from Twilio. --- ## Validate Mobile Phone Numbers URL: https://docs.customer.io/journeys/validate-mobile-phone-numbers/ If you send SMS or WhatsApp messages, you may want to make sure that you're not trying to send messages to landlines or phone number types that you can't deliver SMS and WhatsApp messages to. You can solve this problem using Twilio’s lookup API, to people’s phone numbers, carriers, and line types (like mobile, landline, or VoIP) and make sure that you only send SMS and WhatsApp messages to valid recipients! This can help you prevent bounces, and filter out undeliverable messages. As a part of this recipe, you’ll: Create a segment to capture all people with a phone attribute. Send people in this segment through a campaign that calls Twilio’s lookup API. Capture carrier.type from the webhook response as a phone_type attribute value. This attribute will determine whether a phone number belongs to a mobile device, landline, or VoIP system. Below is an example of a response from Twilio’s lookup API. { "caller_name": null, "carrier": { "error_code": null, "mobile_country_code": "310", "mobile_network_code": "456", "name": "verizon", "type": "mobile" }, "country_code": "US", "national_format": "(510) 867-5310", "phone_number": "+15108675310", "add_ons": null, "url": "https://lookups.twilio.com/v1/PhoneNumbers/+15108675310" } Prerequisites Before you can validate people’s phone numbers, you need: A Twilio account and an active payment method. At the time this article is published, each phone number you validate costs $0.005 (Twilio’s lookup API costs $0.005 per request). People in your workspace with a phone attribute. Your phone attribute must store values in the standard, E.164 format. Recipe As a part of this recipe, we’ll capture the carrier type—which tells us whether or not a phone number belongs to a mobile device, landline, or VoIP device—but you can capture any of the fields from Twilio’s lookup API as attributes using this process. Set up a data-driven segment of people with a phone attribute—ensuring that the attribute “exists”. This segment represents the people we want to validate with Twilio’s lookup API. Create a campaign that filters for the segment you created in the previous step with the following settings: What causes a person to enter a campaign?: They meet conditions. Define the trigger condition: You can use any condition, but we’re using the Signed up condition to start our campaign, because we want to identify people’s phone number type as soon as possible. Filter: The segment you created in the previous step. We only want people to enter the campaign if they have a phone attribute. In the Workflow step, add a Webhook action and click Add Content. Set up the webhook Request: Set the method to Get. Set the request URL to: https://[YourTwilioAccountId]:[Your TwilioAuthToken]@lookups.twilio.com/v1/PhoneNumbers/{{ customer.phone | encode }}?Type=carrier You can get your Twilio account ID and auth token from your Twilio account. {{ customer.phone | encode }} represents a URL-safe version of each person’s phone number. Go to the Response tab and set an attribute using the {{response.carrier.type}} property. This captures the type of device—mobile, landline, or VoIP. Click Send a test to try your webhook using the profile in the Sample Data sidebar. Assuming you’ve set everything up correctly, you’ll see a complete webhook response. Start your campaign. Whenever someone signs up and has a phone attribute, you will automatically obtain the line type. You can then segment based on this type to send SMS-based messages and campaigns. --- ## Getting a phone number URL: https://docs.customer.io/journeys/get-a-phone-number/ If you're new to SMS messaging, you'll need to request a sender phone number that you can send messages from. Before an SMS provider will issue a phone number, they'll require you to have some updates in place to ensure that you're compliant with their policies, various privacy laws, *and*, most importantly, you're clear about how you plan to use your audience's phone numbers. Before you can get a phone number and send SMS messages, you’ll need to register as a sender, which includes several steps—including making sure that your privacy policy and terms and conditions are updated to reflect your SMS-use case. We’ll help you register as a sender, but it can take 1-2 weeks for carriers to review and approve or deny your request. You need to do the following things before we submit your request to Twilio and respective carriers—so you can register for phone numbers and start sending SMS as quickly as possible. Having these things in place before you contact us helps you avoid delays and get approved to send SMS faster. Update your privacy policy Update your terms and conditions Provide clear opt-in and opt-out instructions Write example messages representing your campaigns Contact Customer.io to register as an SMS sender You might work directly with Twilio We’re currently working on a native solution for SMS, so you can do everything in Customer.io. But that solution is only available to select users right now. While the information here is generally true for anybody who sends SMS messages, this section is primarily for users who aren’t already registered with Twilio to send SMS messages through Customer.io and have an SMS-use case that we can support natively. If you want to send SMS through Customer.io, and you meet our eligibility criteria, you can sign up today! Are you eligible to manage SMS phone numbers in Customer.io? While we’re working on our native SMS solution, we only support users meeting the following criteria today. If you meet our criteria, you can sign up to manage your SMS phone numbers and billing in Customer.io. If you don’t meet this criteria, you can still send SMS through Customer.io! You’ll just set up an account directly through Twilio and manage your phone numbers there. You’re eligible for our native SMS solution if: Your business is based in the US or Canada. You only send messages to people in the US or Canada. You don’t require advanced analytics like revenue attribution or sending queues. flowchart LR B{Is your business based in the US or Canada?} B -.->|No| C[Work directly with TwilioManage phone numbers there] B -->|Yes| D{Do you send messages to people outside the US or Canada?} D -.->|Yes| C D -->|No| E{Do you need analytics like revenue attribution or sending queues?} E -.->|Yes| C E -->|No| G[You're eligible forCustomer.io native SMS!Contact support@customer.io] style G fill:#e1f5fe,stroke:#01579b,stroke-width:2px style C fill:#fff3e0,stroke:#e65100,stroke-width:2px How long will it take for me to be approved? When you request a phone number and register as a sender, you need to be approved by both Twilio and cellular carriers. This can take a while; we typically see approval responses in 1-2 weeks—though it can take longer if Twilio and carriers request changes to your privacy policy and terms and conditions. As regulatory requirements change and penalties for non-compliance increase, Twilio and carriers have become more strict in their approval process! Following the guidance in this section can help you avoid delays and get your number approved faster. More importantly: it’ll help you avoid penalties for non-compliance. --- ## Update your privacy policy for SMS URL: https://docs.customer.io/journeys/your-privacy-policy/ As part of the registration and approval process for your SMS messaging, carriers require that your Privacy Policy and Terms of Service include specific language that outlines how you collect, use, and store customer information and how you manage consent for SMS messaging.  This section contains recommendations and examples, but it does not constitute legal advice. Contact your legal team to ensure that you’re compliant with all applicable laws and regulations before you make changes to your privacy policy or terms and conditions. Update your privacy policy Before you request a sender phone number, you need to update your privacy policy to include information about how you’ll message your audience and use their phone numbers. You must provide links to your privacy policy and terms and conditions on your public website when you submit a request for a sender number. You’ll need to include information about: Collection of phone numbers: Make a clear statement that you collect phone numbers for the purpose of sending SMS messages. Purpose and frequency of messages: Explain how you’ll use the phone numbers you collect—whether you’ll send transactional or marketing messages—and how often your audience can expect them. Clear consent language: Explicitly state that users who provide their phone numbers consent to receive messages from your organization. Opt-in disclosure and opt-out instructions: Provide straightforward methods for users to opt into, and out of, messaging services—like replying with START or STOP. Data usage disclosure and security information: Explain how you use user data, particularly phone numbers, and assure users that you won’t share their data with third parties without their consent. Support contact information: Offer a way for users to contact your organization for assistance or inquiries related to your messaging service. Example language Here is an example of compliant language for your privacy policy: Privacy Policy Example By providing your phone number and opting in to receive communications, you consent to receive recurring marketing and non-marketing text messages (SMS and MMS) from [Business Name]. Message frequency may vary. These messages may include updates, promotional offers, account notifications, and other information related to your use of our services. Message and data rates may apply. Reply STOP to unsubscribe from SMS messages. For help, contact us at [support email address here]. Consent is not a condition of any purchase. You may still use our services without agreeing to receive text messages and you can opt out at any time.  Your policies must be publicly accessible You’ll need to make sure that your privacy policy and terms and conditions are publicly accessible, preferably as links from your SMS opt-in form. If these pages aren’t publicly accessible, you might not be approved to send SMS messages. Terms and conditions Like your privacy policy, your terms and conditions (or terms of service—however you refer to them) must include the following items. Your terms and conditions must be publicly accessible, preferably as links from your SMS opt-in form. SMS Messaging Terms: A section stating that, by providing your phone number, the user agrees to receive SMS messages from your business. Message Frequency: A statement describing how often users should expect to receive messages (like “You may receive up to X messages per month”). Opt-Out Instructions: Clear instructions for stopping SMS messages (like “Reply STOP to cancel”). Help Instructions: Information on how to get help (like “Reply HELP for help” or an email/phone number). Carrier Disclaimer: Standard language stating that message and data rates may apply and that carriers are not liable for delayed or undelivered messages. Your policies must be publicly accessible Your privacy policy and terms and conditions must be publicly accessible, preferably as links from your SMS opt-in form and entries in your sitemap. If these pages aren’t publicly accessible, you might not be approved to send SMS messages. --- ## Opt-in and out flow URL: https://docs.customer.io/journeys/opt-in-and-out-flow/ You need to provide your users with clear instructions to opt into and out of your SMS messages. You must have designs for your opt-in and out flow before you request a sender number. Twilio and carriers evaluate your opt-in and out flow as part of your registration process. You must preview your opt-in and out flow before you register as an SMS sender You do not need to have completed your opt-in and out flow before you contact Customer.io to register as an SMS sender—in part because you won’t be able to do it until you have a sender number! But you need to have designs or a preview to demonstrate your opt-in and out flow. Twilio and carriers evaluate your designs to make sure that you’re ready to send messages. What format should my opt-in and opt-out preview be? It’s a best practice to have your opt-in and out flow in a live, preview-able state. It doesn’t need to be publicly accessible; you might place your opt-in flow on a staging website or otherwise hide it from your public site. But providing a functional URL makes it easier for Twilio and cellular carriers to preview and test your opt-in flow. If that won’t work for you, a PDF design of your opt-in and out flow is sometimes an acceptable alternative. Writing opt-in and opt-out instructions We partner with Twilio to send SMS messages. By default, Twilio supports a number of keywords to opt into or out of messages. You may want to use their list of keywords in the opt-in and opt-out instructions that you add to your privacy policy. Beyond your privacy policy, you should make sure that you already gather explicit consent for marketing messages—even before you request a sender phone number. This is often something like a checkbox on your signup form that allows users to explicitly opt-in to receive marketing messages—assuming you send marketing messages. Example opt-in and out flow For example, you might include the following with a checkbox on your site: Explicit Opt-in Example By entering your phone number and checking the box, you agree to receive marketing messages from <Business name> at the phone number provided above. You may receive up to 4 messages per month. Data & message rates may apply. Reply STOP to opt out and HELP for help. [Privacy Policy Link] [Terms of Service Link] --- ## Brand and campaign registration URL: https://docs.customer.io/journeys/sms-brand-campaign/ Before you can register for a phone number, you'll need to register at least one brand and one campaign. The brand represents 'who' sends messages (you or a third party) and the campaign represents the kinds of messages you plan to send. How it works Brand and Campaign are regulatory terms for sending messages in the US and Canada. Before you can register for phone numbers, you have to declare who you are—your brand—and what kinds of messages you’ll send—your campaigns. We’ll help you register your brand and campaigns. If you want to get started right away, take a look at campaigns below. Before you contact Customer.io to help you register as an SMS sender, you’ll need to provide at least three example messages for each of your campaign use cases—each kind of message you want to send. Otherwise, it can be helpful to understand different kinds of brands and campaigns as they come with different requirements, fees, and allowed messages-per-day. Most senders have relatively simple brand and campaign registrations: you are a direct brand and one campaign because you send messages on your own behalf for a single, universal purpose. But this can get more complicated if you send messages on behalf of sub-accounts or you want to send different kinds of messages—like transactional and marketing messages. You must register different campaigns for different messaging purposes, like one campaign for transactional messages and another for marketing messages. Brand registration Most senders are considered standard, “Direct” brands. Generally, this means that you send messages representing yourself—you don’t send messages on behalf of another company or brand. Customer Type Description Direct Brand You’re a business owner that sends messages on your own behalf. You have a business Tax ID (not including a US Social Security Number). Independent Software Vendor (ISV) You’re a company that sends messages on behalf of your customers. See the ISV Onboarding Guide for a detailed registration walkthrough with Twilio. Sole Proprietor You’re a student, hobbyist, someone working at an organization or someone trying out SMS messaging products for the first time. flowchart TD A["Start"] --> B["Are you registering to send messageson behalf of yourself/your business,or on behalf of your customers?"] B -->|"For myself/my business"| C["You are a 'Direct'customer"] B -->|"For my customers"| D["You are an 'ISV' customer. You will complete this processfor each sub-customer."] C --> E["Are you using Twilio to sendmessages from a US 10-digit longcode number to people in the US?"] D --> E E -->|"No"| F["You do not need toregister for A2P 10DLC"] E -->|"Yes"| G["Are you a business with a Tax ID? (notincluding a US SSN)"] G -->|"No"| H["Do you have a US orCanadian address?"] G -->|"Yes"| I["How many daily messagesegments will you send?"] H -->|"No"| J["You cannot register for A2P10DLC and should use aToll-Free number"] H -->|"Yes"| K["Register for aSole Proprietor Brand"] I --> L["Fewer than 6,000"] I --> M["More than 6,000"] L --> N["Do you require highmessage throughput?"] M --> O["Register for aStandard Direct Brand"] N -->|"Yes"| O N -->|"No"| P["Register for aLow Volume Standard Brand"] classDef redBox fill:#ff4757,stroke:#ff4757,stroke-width:2px,color:#fff classDef blueBox fill:#5352ed,stroke:#5352ed,stroke-width:2px,color:#fff classDef grayBox fill:#a4b0be,stroke:#a4b0be,stroke-width:2px,color:#fff class F,J redBox class K,O,P blueBox Brand throughput Your brand entitles you to a certain number of message “segments” per day, where each segment is up to 160 characters. In traditional SMS applications, a segment is a single chat bubble. Most users will register as a Standard Direct Brand, which grants you more than 6000 segments per day if you use an A2P 10DLC phone number or short code. Sole Proprietor Brand Low Volume Direct Brand Standard Direct Brand Campaigns per Brand One Campaign per Brand Each Brand may register up to five Campaigns, unless a clear and valid business reason is provided for exceeding this limit Each Brand may register up to five Campaigns, unless a clear and valid business reason is provided for exceeding this limit Daily message volume 1,000 SMS segments and MMS per day to T-Mobile (approximately 3,000 SMS segments and MMS per day across US carriers) Up to 2,000 SMS segments and MMS per day to T-Mobile (approximately 6,000 SMS segments and MMS per day across US carriers), with the exception of companies in the Russell 3000 Index, who will be able to send 200,000 SMS segments and MMS per day to T-Mobile From 2,000 and up to unlimited SMS segments and MMS per day to T-Mobile, depending on your Trust Score Brand trust score (for A2P 10DLC) When you register as a standard brand and request an A2P 10DLC number, Twilio submits your brand to The Campaign Registry (TCR) for review. TCR will assign a score from 0 to 100 based on your brand’s reputation. The higher your score, the higher your throughput. Twilio also submits the Brand for “secondary vetting,” which assigns a score from 0 to 100 and gives access to higher default throughput and message limits toward US mobile carriers. ISV registration Talk to Customer.io if you want to register as an independent software vendor (ISV). Campaign registration Your Campaign use case type describes the general type of messages you want to send to your audience, like marketing or account verification messages. If you send different kinds of messages, you might have different campaigns—and possibly different phone numbers for each campaign. Generally, you can have up to five campaigns per brand. There are a few different categories of use case types: Standard: see the full list of standard use cases below Low-Volume Mixed: offers lower messaging volume (fewer than 2,000 message segments per day on T-Mobile) and throughput than standard campaign use cases. But this type also comes with a lower monthly fee. Special: for non-profits and emergency services. See the full list of special use cases The different campaign types have varying monthly fees associated and messaging throughput associated with them. See a list of A2P Campaign type fees in this support article. Note that Low Volume Standard Brands receive lower messaging throughput for campaigns than Standard Brands. Standard Campaign Use Cases When you register as an SMS sender, you’ll need to provide at least three example messages for each of the campaign use cases that apply to you. When your campaigns are approved, you’ll need to work within the boundaries of your allowed campaign use cases. If you want to send messages outside of your allowed use cases, you’ll need to contact Customer.io to request a new campaign. Campaign use case Description 2FA Any authentication or account verification like one-time passwords Account Notifications Notifications about the status of an account or part of an account Customer Care Support, account management, and other customer interactions Delivery Notifications Information about upcoming deliveries Fraud Alert Messaging Notifications about potential fraudulent activity or spending alerts Higher Education Message campaigns from colleges, universities, and other educational institutions Marketing Promotional content like sales and limited time offers Mixed A campaign that covers multiple use cases like Customer Care and Delivery Notifications. Note that mixed campaigns are likely to have lower throughput and a higher cost per message. Polling and voting Messages conducting polls and votes for non-political activities—like customer surveys. Not for political use. Public Service Announcement PSAs to raise awareness about a given topic Security Alert Notifications about compromised systems (software or hardware) --- ## What kind of phone number do I need? URL: https://docs.customer.io/journeys/phone-number-types/ You can send messages using three different kinds of phone numbers. In general, all three have similar regulatory requirements, but they have different daily sending limits. You'll want to pick the type of number that best fits your business needs. Most of the guidance here is focused on the United States and Canada—the regions we typically support through Twilio. You can use international numbers, but you may need to work with Twilio to support them. Types of phone numbers You can send messages using three different kinds of phone numbers. In general, all three have similar regulatory requirements, but they have different daily sending limits. You’ll want to pick the type of number that best fits your business needs. Toll-free numbers: Like 1-800 numbers, these phone numbers aren’t regional and are great if you don’t have a physical presence in the United States or Canada. 10-digit numbers: Standard 10-digit phone numbers that include an area code. These numbers appear “local” to your audience. Short codes: 5 or 6 digit numbers that represent your brand. Which type should I choose? Use this decision tree to help determine which phone number type best fits your business needs: flowchart TD A["Do you have a physical presencein the US or Canada?"] --> B["Yes"] A --> C["No"] B --> D["Do you send high-volume messagesor need strong brand recognition?"] C --> E["Do you send high-volume messagesor need strong brand recognition?"] D --> F["Yes - High volume/branding"] D --> G["No - Standard volume"] E --> H["Yes - High volume/branding"] E --> I["No - Standard volume"] F --> J["Short Code• 5-6 digits• Higher cost• Best deliverability• Highest throughput• Strong brand recognition"] G --> K["10-Digit Number• Standard phone number• Local area code• Appears local to customers• Multiple numbers for regions"] H --> L["Short Code• 5-6 digits• Higher cost• Best deliverability• Highest throughput• Strong brand recognition"] I --> M["Toll-Free Number• 1-800 style number• Non-regional• Good for online businesses• Lower cost than short codes"] style J fill:#e1f5fe style K fill:#f3e5f5 style L fill:#e1f5fe style M fill:#fff3e0 Toll-free: good for online-only businesses Toll-free numbers are great for online-only businesses that don’t have a physical presence in the United States or Canada. If you’re an online company, or you aren’t located in North America, you might want to use a toll-free number. Toll-free numbers typically support lower throughput rates than 10-digit and short-code numbers. If you broadcast a large number of messages simultaneously, this means that it might take longer for your messages to be delivered. 10-digit: good for local businesses 10-digit numbers are great for local businesses that have a physical presence in the United States or Canada. They’re also great for businesses that want to send messages to people in the United States and Canada. You may also have multiple 10-digit numbers, which can be useful if you want to send messages to different regions or countries. Typically, you’ll want to use an area code local to your audience. So, if you have locations in every state, you might have 50 numbers. If you want to send to individual municipalities close to your storefronts, you might have even more! Short codes: good for brands Short codes provide a great way to brand your sender number. While they cost more than other options and have the strictest regulatory requirements, they also: Are less likely to be blocked by carriers and recipients Offer a higher throughput than toll-free or 10-digit numbers—more messages per second Are much more recognizable to your audience Short codes are great for large campaign or transactional messaging. Phone number throughput Different types of numbers send messages at different rates—commonly known as “messages per second” or MPS. By default, toll-free numbers send messages at roughly 3 messages per second. 10-digit and short-code numbers, on the other hand, can send at up to 225 messages per second depending on your trust score and account/brand type. For standard brands—which accounts for most cases—throughput is determined by your trust score and account/brand type. You may have higher total throughput if you use multiple phone numbers, but this is typical of things like geographically distributed businesses (e.g. you have physical stores in multiple states). You cannot use multiple phone numbers to circumvent rate limits imposed by carriers or regulations. --- ## HIPAA compliance and privacy regulations URL: https://docs.customer.io/journeys/hipaa-standards/ If you send messages relating to healthcare in the United States, you may need to be HIPAA compliant. This page contains best practices to help you with HIPAA compliance and protect your audience's personal information. HIPAA compliance requirements The Health Insurance Portability and Accountability Act (HIPAA) is a United States law that ensures organizations handling individuals’ digital health information keep it secure and confidential. The information protected under HIPAA is referred to as Protected Health Information (PHI). PHI includes not only medical details, like test outcomes and treatment records, but also personal identifiers connected to those records like a patient’s name, contact details, account numbers, demographic data, and related identifiers. HIPAA applies specifically to Covered Entities, which are: Health insurers and plans (including Medicare, Medicaid, HMOs, and employer health plans) Healthcare providers (like doctors, pharmacies, and clinics) Health information clearinghouses and related organizations Because these entities often rely on outside partners, HIPAA also outlines how they can share PHI with Business Associates—organizations that perform services involving access to PHI. Business Associates must follow strict safeguards and use the information solely for the intended healthcare purposes to remain HIPAA-compliant. Customer.io is a Business Associate for our customers who need to navigate HIPAA compliance in messaging with their users. Best Practices to support HIPAA Compliance If your work involves protected health information (PHI), you must implement and maintain appropriate administrative, technical, and physical safeguards required under HIPAA. These controls help ensure the privacy and security of your users their data. HIPAA obligations also extend to any subcontractors or third-party partners who might access PHI on your behalf. You are responsible for ensuring your partners follow compliant practices and maintain the same level of protection. Compliance isn’t a one-time task! You and your teams should regularly review their processes, confirm that safeguards remain effective, and update processes as needed to meet evolving requirements. Here are some best practices to help with HIPAA compliance as you use Customer.io: Execute a Business Associate Agreement (BAA): You can request to execute a BAA with Customer.io outlining your adherence to HIPAA’s privacy and security standards. If you manage your own Twilio account to send SMS messages you’ll also want to sign a BAA with Twilio directly. Limit access to health information: Only give access to team members who absolutely need access to personal information for their jobs, and regularly review who has access to that information. Don’t give access to PHI to team members who don’t need it for their jobs. Don’t send protected health information (PHI) in messages. SMS, push, and email are not fully secure communication channels and can expose sensitive information if your users lose their devices, share them with others, or are otherwise compromised. Instead, you should send your audience messages with links to a secure portal where they must log in to access their health information safely. Using secure links ensures PHI remains protected behind proper authentication, and prevents the disclosure of sensitive data over unencrypted channels. Use strong passwords and two-factor authentication: Protect Customer.io access—and any system with access to health data—with complex passwords and multi-factor authentication. Train your team regularly: Ensure everyone handling health information knows the rules, recognizes phishing emails, and understands how to report security incidents. HIPAA compliance requires ongoing review, evaluation and strong partnerships. The law has changed as recently as 2025. Additional Tips for sending HIPAA compliant messages While our documentation isn’t legal advice, we’ve included some tips that can help you send HIPAA compliant messages—and general practices that are probably good to follow even if you’re not working with protected health information (PHI). Create a patient communication preference form Your audience should have an easy way to set their communication preferences. While you can do this with keywords over SMS, it’s tough to explain everything involved in communicating health information in an SMS message. Instead, you should create a patient communication preference form where your audience can determine what information they’re comfortable getting over SMS and other channels. You can link users to this form from SMS messages, so they can opt-in or out at their leisure, independently of any messages you might send. With a form, you can also link users to complicated policy information so users can learn more about, and opt into or out of, health information they might receive over SMS. You can store these preferences in attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., which you can use to segment your audience and make sure you abide by their preferences. You can manage form submissions with our form integrations, or with a custom event from our JavaScript integration. Follow users channel and time-based preferences SMS and push notifications are particularly visible to your audience, and they could show up at an inopportune time! Nobody wants a notification to pop up during a meeting with information about test results. Even email can be a vulnerable channel, especially if your users haven’t used an email address in a while—like an email address from a past job or an old university email address. Make sure that your users contact information is up to date try to send people messages through the channels they prefer at the right times. While our Subscription Center functionality doesn’t manage channel-based preferences, you can use attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to manage preferences and then use conditions in campaigns and broadcasts to send users messages through the channels they prefer. You can use our automatic geolocation feature to better understand your users’ local times and schedule messages accordingly. Gather SMS consent specific to HIPAA While patients can opt into SMS messaging and agree to receive protected health information (PHI), they can revoke consent for either, or both, at any time. You can use traditional keywords like START and STOP to opt users into and out of SMS messaging all together, but you may need to store consent to transmit PHI—or get health-related messages—over SMS in another attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. In general, it’s easier to manage additional consent with a patient communication preference form, but you can also use keywords over SMS. For example, you could send a message asking if a user wants to opt into health information over SMS, and they could reply with Yes or No. Keep a history of consent and risk-acknowledgement You should keep a history of your audience’s consent and their acknowledgement of risks associated with SMS communications. While you can use attributes in Customer.io to represent consent (or non-consent), you’ll need to export message data from Customer.io to store the history of consent and risk-acknowledgement beyond the current state in Customer.io. The best way to do this is with our Data Warehouse integrations, which help you capture attribute changes and message history that you can store in a data warehouse or database of your choice. Do not include PHI in support tickets If you need support with an issue relating to HIPAA compliant messages, you should avoid exposing protected health information (PHI) to support teams or non-health professionals outside of your organization (including Customer.io and Twilio). When you submit support tickets or request technical assistance, don’t include protected health information (PHI) in your communications; that includes support communications with Customer.io, Twilio, and other vendors. While we’re happy to help, we’re not covered by your HIPAA agreements and should not have access to real patient data—even incidentally! Do not include in support tickets: Patient names, dates of birth, or contact information Medical record numbers or account IDs Specific medical conditions, treatments, or diagnoses Insurance information or billing details Screenshots containing actual patient data Database queries with real PHI Log files that might contain patient information Do this instead: Use fake names like “Patient X” when describing scenarios Replace real dates with examples like “01/01/2025” Use placeholder medical record numbers like “MRN12345” Create mock data that illustrates your technical issue without exposing real information Redact or blur any PHI from screenshots before you share them with support Ask your support contact to sign a BAA before you share real data (most won’t, which means you shouldn’t share it) Remember: Technical support can be just as effective using anonymized examples, and this approach protects both your patients and your HIPAA compliance! If you need help with HIPAA compliance Reach out to your Customer.io representative if you have questions about HIPAA compliance for Customer.io or want to explore a Business Associate Agreement (BAA) with us. You can also learn more about our security qualifications and certifications, including our HIPAA readiness. --- ## Get started URL: https://docs.customer.io/journeys/in-app-getting-started/ In-app messages let you incorporate dynamic, personalized content into your app or website with very little development or engineering and help you continue conversations across channels. By personalizing messages based on the activities your audience performs in- or outside your app, you can maintain highly relevant interactions with your audience. What is an in-app message? An in-app message is a message sent to a user inside your app or on your website. It’s distinct from a push notification in that a user must be in your app or on your website to receive it. They can’t get your message if they don’t open your app or log in to your site. While we refer to in-app messages as messages, you can make them persistent and use them as banners or other UI elements. They aren’t necessarily one-time messages! How it works When you send an in-app message, the message waits in a queue until your recipient opens your app or website on one of their devices. When a recipient opens your app or website, we poll for new messages and send messages in the queue. When someone gets a message, we log events based on their responses—whether they tap an action in the message, dismiss it, etc. You’ll target your message to your audience by their ID or email rather than by device or browser. In-app messages appear in the first device or browser that the person visits after you send a message. After a person receives your message, they won’t get it on another device or browser session (unless they enter a campaign again or you re-broadcast your message). sequenceDiagram Participant a as App User Participant b as SDK Participant c as Customer.io c->>c: Trigger in-app message c-->>b: If app isn't open, hold until user opens app a->>b: User opens app b->>c: Identify user c->>b: Send in-app message b->>a: User sees in-app message Before you begin To send in-app messages, you’ll need to install an SDK (software development kit) in your website or mobile app. If you’re not a developer, this can sound intimidating, but don’t worry! All you need to do is copy a block of code to your website. For your mobile apps, this process probably requires a mobile developer, but you can still start drafting messages in Customer.io without setting up your app. flowchart LR a{Where do I start?} a-->|I'm a marketer|f{Are SDKs already integrated?} f-->|yes|g[Create in-app messages] a-->|I'm a developer|h[Integrate mobile SDK] f-.->|no, contact a developer|h{Integrating app or website?} h-->|Website|i(Add JS snippetto your website) h-->|App|b(Install mobile SDK) i-->g b-->g Set up web and in-app messaging Go to Settings > Workspace Settings and click Get Started next to In-App. Click Enable in-app. After you enable in-app messaging, you’ll need to update your website and/or mobile app to receive in-app messages. Set up your website You’ll need to add our JavaScript snippet to your website and identify people who visit your site so that you can send them messages. Set up your mobile app Check out our mobile SDK documentation to incorporate the in-app notifications SDK. You might need a mobile developer to help you add our SDK to your app. iOS SDK Android SDK React Native SDK Expo Plugin SDK Flutter SDK Test your in-app setup Before you send your first message, go to the in-app settings page and click Send Test to check your setup. This’ll help you that your integration with Customer.io is set up properly. You’ll need the ID or email address of the user you want to send a test to. If you don’t have a test user, you’ll want to add one to your workspace first—maybe even yourself! A test user isn’t just for in-app messages; you can also test emails, full campaigns, etc. Go to Workspace Settings > In-App Settings and click Send Test. Enter the ID or email address of your test user and click Send Test. Make sure that you’re logged in or identified on your app or website as the user you want to send your test to. If you aren’t, you won’t see the test message and won’t be able to tell if your setup is working properly! Open the app or website you want to test. If you see the test message, everything’s working properly! If you don’t see a message, check out our troubleshooting tips.  Refresh your page or restart app after you send a test! Before you send your first message, we poll slowly for messages. We update this rate when you send your first message—but you’ll need to refresh your page or restart your app to engage the new polling rate. If you don’t do this, it may take up to 3 minutes to see your first message! Disable in-app messaging Disabling in-app messaging cancels all outstanding in-app messages and prevents your campaigns from sending in-app messages in the future. If you have campaigns with in-app messages in them, you may want to remove those messages first to make sure that your campaign and broadcast workflows still make sense. Go to Settings > Workspace Settings > In-App. Click Enable or Disable. Polling for messages We normally check for new in-app messages every 10 seconds. But we adjust this rate when people don’t have active messages in queue to minimize unnecessary network traffic. When you first start sending in-app messages, it may take a few minutes to ramp up in-app polling frequency to the normal rate. This won’t affect your users. But, if you have your website or app open when you send your very first test message, you’ll want to clear your cache or restart your app to see messages at the normal polling rate. Polling Frequency Active Messages in Queue? Description Low (180 seconds) No This is the polling rate before you send your first message. We also revert to this rate if you don't send a message within 30 days after your last in-app message expires. Inactive (60 seconds) No This is the rate for people who've received a message in the last 30 days but do not have an active message in the queue. Active (10 seconds) Yes This is the rate for people who have an active message in the queue. --- ## Set up your website URL: https://docs.customer.io/journeys/in-app-web/ While we call some messages *in-app* messages, they're not limited to mobile apps! You can send in-app messages to your website visitors too. How it works While we call some messages “in-app” messages, they’re not limited to mobile apps! You can use “in-app” messages to send things like banners and modal messages that appear on your website. These are great ways to alert your users about new features, send promotions, ask for feedback using surveys, and so on. To get set up with in-app messages, you’ll copy our JavaScript source snippet to your website. You’ll send in-app messages using our JavaScript snippet. If you already use our JavaScript library to identify people and track events, you’re most of the way there! If you aren’t using our JavaScript library yet, don’t worry! It’s a simple block of code you can copy and paste to your website. You’ll need to identify people before you can send them messages, but we’ll walk you through that process too. flowchart LR a(Send message)-->b{Is user on your website?} b-->|yes|c{Are they on the right page?} c-->|yes|d{Are they identified?} d-->|yes|e(User sees message) b-.->|no, wait for the user to visit your site|g(User does not see message) c-.->|no, user must be on a page matching your rules to see the message|g d-.->|no, user must be identified to see message|g Enable in-app messaging If you use our JavaScript integration, you only need to enable in-app messaging to start sending in-app messages to the people you identify on your website. Go to Settings > Workspace Settings, click In-App, and Enable in-app messages. If you don’t already have a JavaScript integration: See our JavaScript SDK documentation to get started with our JavaScript integration and in-app messaging. If you’re not a developer, don’t worry! You’ll just need to copy and paste a block of code into your website.  Want to send anonymous messages? If you want to send in-app messages to people who aren’t identified, you’ll need to set up anonymous messaging. See anonymous messages for more information. If you use our classic JavaScript snippet If you use our classic JavaScript snippet, you’ll need to add a bit of code to your snippet to support in-app messaging. See our classic JavaScript library instructions. // Enables in-app messaging t.setAttribute('data-use-in-app', 'true'); Identify your website visitors Unless you’re sending anonymous messages, you must identify people before you can send them messages. You can do this with the identify method. You’ll send an identify call whenever someone logs into your website, provides their email address, or otherwise agrees to use your service. Learn more about identifying people. //cioanalytics.identify(id, attributes) cioanalytics.identify('f4ca124298', { email: 'person@example.com', first_name: 'Cool', last_name: 'Person', plan_name: 'premium' // Strongly recommended when you first identify someone created_at: 1339438758, // This is the timestamp when the user // first appears, in Unix format. }); Listening for in-app message events You can also listen for events when people see or interact with your messages. For example, imagine that you send an in-app message with a button. When people click the button, you want to enable a setting. In this case, you’d listen for the click-event (called in-app:message-action) to enable the setting for your users. See our JavaScript SDK documentation for more information. --- ## Send in-app messages URL: https://docs.customer.io/journeys/send-in-app-message/ Send an in-app message as a part of a campaign or broadcast workflow and put your message in front of people who open your app or visit your website. Before you begin You’re welcome to try out our in-app message editor and set up campaigns with in-app messages before you integrate our SDKs. But your audience won’t be able to see your in-app messages until you integrate with our SDKs for your app or your website. How it works You can send in-app messages as a part of any campaign or broadcast workflow. In addition to creating your message, you’ll also determine when and where your message will show up for your audience: Page rules: the pages and screens a person must visit to see your message. Expiration: this determines how long a message will remain available to a person. It ensures that people only get relevant messages. Display and Position: where your message will appear on a page or screen. You can add different settings for steps in a multi-step message. When you send a message, people don’t necessarily see it right away! Your audience will see your in-app message when: They visit a page or screen that matches your page rules. They’re identifiedThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously.. The message has not expired. graph LR a[user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Send an in-app message Drag an In-App Message into your workflow. Select your message, give it a NameThe name of a message. This is just a friendly name to help you understand the purpose of a message in your workflow., and click Add Content. Select a starting template, a previous message, or start from scratch. Set your message settings: Priority: negotiates between multiple messages with the same Page Rules and Display settings. Page Rules: determine the pages where a person can see your message. Set your message’s Display and Position. This determines where your message appears on a page. You can find these settings by selecting the message background or going to Steps and selecting the Step for your message. See Display and Position for more information. Build your message. Remember, you can use 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 customize your message!  Try the Live Preview feature Click Live Preview to see your message on your website. You can even adjust display settings in real time to play with different positions, message widths, and more. Click Save Changes. Message expiration By default, messages expire 30 days after they’re sent, but you can change the expiration period to make sure that people only see your messages while they’re relevant. If someone doesn’t see your message within the expiration period, we’ll cancel the message. However, many messages are more time-sensitive. For example, you wouldn’t want to send someone an expired coupon code. To prevent people from receiving old messages, make sure that you change the Expiration for your message. You can set your message to expire At a specific date and time At a relative time after the message is sent (for example, 10 days after the message is sent) Or using 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 configure the expiration date based on an event property, profile attribute, trigger variable, or snippet relative to the send time of the message. The maximum expiration window is 60 days. Persistent messages By default, in-app messages only show once—the message disappears after the customer clicks, taps, or refreshes the page. You can check the Persist Message option to show your message across multiple sessions and page views. Your audience will continue to see a persistent message until they close it or until the message expires. Make sure your messages include a Close button or an icon so that customers can permanently dismiss your message. If you use one of our web SDKs, you’re all set to send persistent messages. For mobile devices, you must use the versions below or later to take advantage of this feature. Persistent messages are available starting in: Android 3.7.0 iOS 2.8.4 React Native 3.2.0 Flutter 1.3.0  Test messages won’t persist While you can test persistent messages, they won’t persist across pages like live messages do. Message Priority When you send a message, a user won’t see it until they visit a page or open your app. This means that a user can become eligible for multiple messages. We use priority to determine the order in which we deliver queued messages that have the same Display and Page Rule settings. Priority affects the order in which we deliver queued messages, but it doesn’t replace messages that are already displayed. If a person visits a page and gets a low-priority message before you send a higher-priority message, they’ll see the low-priority message. Our SDKs won’t replace the lower-priority message with the higher-priority message. Priority applies based on your Display setting: Modal (Position does not matter): We only display one modal at a time. If you send two modal messages, we’ll display the one with the highest priority. If you send two messages with the same priority, we’ll display the one that was sent first. Overlay (Position matters): We can display up to six overlay messages at a time! One for each Position—top left, center, right and bottom left, center, right. Inline (Position ID matters): Priority affects the order messages are queued for delivery to a specific position ID. If a message is already displayed in an inline position, higher-priority messages won’t replace it until the current message is dismissed or expires. Tooltip (Target matters): Priority affects the order of queued tooltip messages. Tooltips only display if the target element exists on the page. You can avoid competing messages using Page Rule triggers or by using conditions in campaigns to check for competing messages. Page rules cause messages to appear when a person visits specific pages in your app or website, helping you deliver messages that are relevant to the pages your audience visits. Page rules By default, your audience will see your message on the first page or screen they visit. Page rules let you determine the specific platforms and pages or screens that a person must be on to see a message. They help you send messages that are relevant to the places your audience visits and the things they do. You can set two kinds of page rules: Include: your message will appear on any of these pages. Exclude: your message will not appear on any of these pages. Click Add Rule to add a page rule. Click it again to add multiple rules. When you add a rule, you’ll set the platform and then the pages or screens a person must visit to see your message. You can use wildcards (*) in rules to match multiple pages. For example the /journeys/ path would only match a single page but /journeys/* would match all pages in the /journeys/ path. You can also use 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 set a page rule based on an event property, profile attribute, trigger variable, and so on. For example /{{event.product_family}}/* would match pages that begin with the value of the product_family event property. You can look for query parameters (or values of query parameters) in the URL using the contains operator. For example, contains utm_source would match pages with a utm_source query parameter. Evaluating multiple page rules When you set multiple rules of the same type, any matching path satisfies the rule. For example, if you have multiple include rules, a page matching any of your rules will show your message. Similarly, any page matching your exclude rules will not show your message. When you set both include and exclude rules, a person must match at least one include rule and not match any exclude rules to see your message. If a person goes to a page that matches an exclude rule, they won’t see your message even if they also satisfy an include rule. flowchart LR a(person visits page) a-->b{Does page match at least 1 include rule?} b-->|yes|c{Does the page match an exclude rule?} c-->|no|d(show message) b-..->|no|e(Do not show message) c-.->|yes|e Test your page rules Page rules may contain regular expressions, and the rules along which we evaluate them can be hard to understand. That’s why we have the Test Page Rules section below the page rule editor. Here you can enter a URL or page path and see whether your message will appear or not. Use tests to fine-tune your page rules and make sure that your message appears on the right pages before you send it. What is a “page”? A “page” means something different for your mobile apps and website: Mobile Apps (iOS and Android): the page is the same as the name value that you send in screen events. For example, billing* would cover all screens in your app that begin with “billing”. Websites: the page is the URL (window.location.href) unless you pass page calls with a different name parameter. For example, https://example.com/*/billing would cover paths on your website like your in-app billing page https://example.com/ui/billing or documents about billing under https://example.com/billing.  Use * to represent all pages When you select a channel, you have to enter a page rule. But, if you want to show a message on every page on your website or app, you can simply enter *. This wildcard matches all pages or screens in your app or website—even if you don’t track your audience via page or screen calls. Page rules in single-page applications If your website is a single page application you must send page calls to tell Customer.io what “page” a person is on. You might use page rules to point out new features within specific areas of your app or new products on your website and you can even use 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 set a dynamic value (like {{event.product_url}}). Page rules also help avoid conflicting messages. If you send two messages of the same priority without page rules, they’ll appear one after the other; if you set a page rule for at least one message, it’ll distribute the messages across your app, and only show when your audience is on a page matching the rule. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. Position your message Click the message background to get to the display and position settings. The Display and Position settings determine where your message appears on a page or screen. You can set different values for different steps in a multi-step message, giving you more control over things like product tours or other flows where you need your message to move. The Overlay option effectively limits your message to your website visitors because mobile apps won’t display the message. Display setting iOS Android Web Description Modal Position your message at the top, in the center, or at the bottom of your audience’s screen. Overlay Position your message in the location you select, relative to your audience’s browser. Inline Replace an empty element with your message. For example, you may have dynamic div elements on a homepage that you periodically update with new content—like new products or features. Tooltip Anchor your message to a specific element on the page using a CSS selector. Great for feature callouts, onboarding flows, and product tours. Tooltip messages Tooltips are anchored to a specific element on a page, like a button, menu item, or feature. They’re ideal for drawing attention to new features, guiding users through your interface, or providing contextual help. Tooltips are available for web only. You can string together multiple tooltips to create a tooltip tour—which can help with things like onboarding flows or product tours. When you select the Tooltip display type, you’ll configure: Target: A CSS selector for the element you want to anchor your tooltip to—like #my-button, .nav-link, or [data-tooltip="feature"]. The tooltip is “anchored” to this element. Position: Where the tooltip appears relative to the target element: Top, Bottom, Left, or Right. The default is Top. You can click Select Target to open a live preview window and select an element on your website, or enter a CSS selector manually. Tooltips appear automatically when the message is delivered and the target element exists on the page. They are not triggered by hovering over the target element. If the target element doesn’t exist on the page, the tooltip won’t display.  Include a way to close your tooltip Unlike modals, tooltips don’t support the Dismiss on outside click setting. Make sure you include a close button or CTA in your tooltip so people can dismiss it. Specifying your tooltip’s target element The easiest way to set a tooltip’s target is to use the visual selector—you don’t need to write CSS selectors by hand. Click Select Target in your tooltip’s display settings (or use Live Preview) to open a preview of your website: Click Select Target to open a live preview of your website. If you haven’t set a target yet, your tooltip won’t appear in the preview. You have to select a target to make your tooltip appear in the preview. In the preview bar at the bottom, click Select Element. Click the element on your page that you want to anchor your tooltip to—a button, a nav item, a feature card, etc. The CSS selector is generated automatically and synced back to the editor. Make sure that the element you anchor your tooltip to is visible on every page in your page rules. If the element isn’t on a page, the tooltip will never appear. If you set your tooltip up with a page rule where some pages have the element and some pages don’t, the tooltip will only appear on the pages that have the element! If you’d rather set your tooltip’s target element manually, your CSS selector should reliably match a single element on the page. Here are some tips: Use an ID when possible: #onboarding-button is the most reliable option. Use a data attribute for elements without IDs: add something like data-tooltip="feature-name" to your HTML element and target it with [data-tooltip="feature-name"]. Avoid fragile selectors like .container > div:nth-child(3) that break when your page structure changes. Inline messages Inline messages are a special type of in-app message that appear inline with the content on a page or screen. They’re a great way to add interactive elements to your page without interrupting the user’s experience or having to release a new version of your app. To set up inline messages, you need to add empty elements to your website or views to your app to contain your inline messages. Then, when you send an inline message, you’ll specify the Position ID for your inline message—the empty element or view where you want to display your message. You might need to work with your developer or marketer counterpart to work with inline messages. For example, if you’re a marketer, you might need to work with a developer to add elements or views to your app. And, if you’re a developer, you’ll need to work with whoever sends messages to figure out where you should place inline message positions and what IDs to set for them. flowchart LR a(Send inline in-app message) a-->b{Is the recipient on the right page?} b-->|yes, page matches page rules|c{Does the position ID exist?} c-->|yes|d(Display inline in-app message) b-.->|no, page doesn't match page rules|e(Don't display inline in-app message) c-.->|no|e Custom message width By default, the max width of a message is 414 pixels, a common breakpoint for mobile devices. This might not be wide enough for certain messages—especially ones that you display on your website. If you need more space, you can set a custom Message Max-Width (px) for your in-app message. This expands messages to the set value, while still being responsive (100% of width) when the screen size is smaller than the max-width value. Custom background overlay color and opacity When you use the modal display position for your in-app message, we display your message on top of a background overlay. The overlay has a default hex color of #000000 (black) with a 20% opacity. You can change the Overlay Color and Opacity values to better fit your brand when you configure your in-app message’s display settings. Dismiss on outside click (Web only, modals) The Dismiss on outside click setting lets users dismiss a modal message when they click outside the message on your website. This setting is only available for the Modal display type—it doesn’t apply to tooltips, overlays, or inline messages. We wait 1 second after the message appears before we check for clicks outside the message. This prevents the user from unintentionally dismissing the message when they click on something on your page. If you enable this setting, clicking outside the message also sends the in-app:message.dismissed event—just like if someone clicks the close button in your message. You can listen for this event and perform additional actions when a person dismisses a message. Limit your message to web or mobile audiences By default, when you set up an in-app message, your audience can see your message in your mobile app or on your website—anywhere you’ve integrated in-app notifications. You can limit your message to specific message channels using page rules. When you set a page rule, your message will only go to the channels and pages that match your rules. If you want to send a message to any page on your website or mobile app, you can use a wildcard (*). You can also use wildcards in your path to send your message to all pages within a directory or all pages that match a pattern. For example, if you want to send a message to all pages in the billing path on your website, you could set a page rule like Web contains /billing/*. Segmenting an audience of app users When you set up your campaign, you may also want to make sure your segment includes, or filters for, people who have a version of the SDKs supporting in-app messages. If someone in your audience doesn’t have a version of your app that supports in-app messages, they’ll never receive your in-app messages. Messages intended for them will be sent, but never opened, which can produce inaccurate metrics. Our SDKs have implemented in-app messages in different versions. So, if you create segments or filters for this purpose, you should group platform AND cio_sdk_version conditions:  Use filters for event triggered campaigns If you send an in-app message as a part of an event-triggered campaign, you might want to use a segment to exclude people who don’t have versions of your app supporting in-app messages. Test your in-app message You can send a test version of your in-app message to make sure that your message looks and behaves the way you expect it to. Click Send test and find the person you want to send your message to. If your test looks good, then you’re all set to send! If not, you can edit your message and send more tests to check your work.  Testing your first message? Refresh your app! Our SDKs poll to determine when we should deliver a message. But before you send your first message, we poll slowly because you haven’t sent anything yet! When you send your first test message, you should clear your cache or restart the app where you expect to get the message to kick-start our normal polling rate. --- ## Global styles for in-app messages URL: https://docs.customer.io/journeys/global-styles-in-app/ In Design Studio, you can define colors, fonts, and more in Global Styles to reuse them across your messages. This way, you spend less time adding the same branding to each message. Global styles apply to emails made in Design Studio and in-app messages. How Design Studio works Design Studio is a flexible editor that helps you create beautiful, responsive messages faster than ever before. Use it to set branding across emails made in Design Studio and in-app messages. If you’re new to Design Studio, this is the core of what you need to know: You continue to create an in-app message in a workflow, like a campaign or broadcast. By default, new messages use the styles set in Design Studio. You create global styles in Design Studio > Styles. Set global style variables (your colors, fonts, radii, and spacing). Then assign them to standard components, like paragraphs and headings, so each part of a message uses the right branding. When you update global styles, you decide whether to “Save only” or “Save and publish”. Choose “Save only” if you don’t want workflows (your campaigns, broadcasts, etc) to use the latest styles. Choose “Save and publish” if you’re ready to update messages in your workflows (and start sending messages with the latest styles). Go to Design Studio’s page on Global Styles for more information: How to set global style variables How to set default branding for components How to publish styles to emails & in-app messages Get started In-app messages use global styles set in Design Studio. The variables you assign to components are the defaults for in-app messages. If you’re creating an in-app message from scratch, it will automatically pull in your global styles. For existing messages, you can choose to sync global styles from Design Studio. Global styles only apply to in-app messages made with our drag-and-drop editor (aka the visual editor); we are sunsetting the legacy editor. Exceptions for in-app messages In-app messages work a little different from emails, so here are a few notes on which settings apply to in-app: In-app messages pull in the default, light mode styles. Dark mode styles do not yet apply to in-app messages. In-app messages and emails have the same breakpoint: 600px wide. Both pull in desktop and mobile styles based on the recipient’s screen size. With modal in-app messages, the breakpoint continues to be 600px wide; however, it’s based on the message width, not the screen width. For instance, if a modal message is 300px wide, but the screen width is 800px, the modal message will display the mobile version of the styles. The single button component automatically use global styles. However, the components built from multiple buttons do not automatically use global styles; you have to reset the properties to pull in your branding. Sync global styles to in-app messages Any message made before we rolled out global styles for in-app messages in February 2026 do not automatically use Design Studio; we won’t change your messages without your permission. You can choose to pull in global styles by syncing individual in-app messages: Open your in-app message. Click Sync with Global Styles. Click Sync. Syncing does not save the message; you’ll have time to review first. Review your message: Do the styles and formatting look correct? If not, you can edit the message further or click Discard Changes to go back. If something doesn’t sync as you expect, click and select your message background (click outside the message) to open Message properties. Click next to a property that you want using your global style. You’ll likely only run into this issue if you started an in-app message from one of our out-of-the-box templates. Click Save Changes. Any future sends of this message will include your global styles. Set global styles on multi-button components Single button components automatically use global styles. However, components with multiple buttons need an extra step to start using global styles. To change the styling of multi-button components to match your global styles, you need to manually reset them in the message. After you reset and save, the styles will use global styles (and any changes to them) moving forward. Open your in-app message. Drag in a multi-button component or click one to open the properties panel. Find a property that was modified in your global styles and click . The button preview will now show your global style. Do this for each property you modified in global styles, like Fill, Hover, etc. Click Save Changes. Any future sends of this message will include your global styles. --- ## Anonymous messages URL: https://docs.customer.io/journeys/anonymous-in-app/ You can show in-app messages to anonymous visitors to your website and convert them to members of your audience by inviting them to create an account, become users, or make their first purchase. How it works With anonymous in-app messages, you can show messages to people who visit your website or use your app but haven’t been identified—people who haven’t logged in, provided their email address, and so on. These messages specifically exclude people you’ve identified. You can determine the pages where your message will appear, and any unidentified people who visit the page will see your message! That way you can show visitors a banner, pop-up, or even present them with a survey to learn what brought them to your website. flowchart LR a(Person visits your website/app) a-->z{Is the person identified?} z-->|no|b z-.->|yes|x(Person does not see anonymous message) b{Does person already have an anonymous_id?} b-.->|no|c(Generate anonymous_id) b-->|yes|d c-->d{Is the person on a page with an in-app message?} d-->|yes|g{Has person reached message frequency limit?} g-->|no|f(Person sees anonymous message) d-.->|no|x g-.->|yes|x Enable anonymous in-app messaging for your website Anonymous in-app messaging is a feature for our JavaScript sourceAn integration that feeds data into Customer.io—a source of data. integration. If you haven’t set up our JavaScript integration, you’ll need to do that first. If you use our legacy JavaScript snippet, you’ll need to update to the new source SDK to use this feature. To support anonymous messaging, you need to modify your JavaScript SDK configuration to include the anonymousInApp: true flag. This flag tells us that you want to support anonymous in-app messages. !function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute('data-global-customerio-analytics-key', i);t.src="https://cdp.customer.io/v1/analytics-js/snippet/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._writeKey=key;analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.15.3"; analytics.load("YOUR_CDP_API_KEY", { "integrations": { "Customer.io In-App Plugin": { anonymousInApp: true } } }); analytics.page(); }}(); Enable anonymous in-app messaging for your app If you use our mobile SDKs, you don’t need to do anything special to enable anonymous in-app messaging. You just need to use (or upgrade to) a version of our SDK that supports anonymous in-app messaging. Mobile SDK Minimum required version iOS SDK v3 and later 3.14 Android SDK v4 and later 4.12 React Native SDK v4 and later 4.11 Flutter SDK v2 and later 2.9 Expo SDK v2 and later 2.8 Show an anonymous message Unlike other messages, you don’t need to select your audience for anonymous messages: we’ll show your message to all unidentified people who visit your website or use your app. However, you don’t want to over-message your audience, so you should use page rules to make sure that you only show messages on pages where they’re useful. Go to Anonymous and click Create anonymous message. Give your message a Name and a Description, then click Next. Set Page Rules to determine the pages where you want your message to appear. See Page rules for more information. Set a Frequency for your message. By default, a message will appear on every page an anonymous person visits that matches your Page Rules criteria until they dismiss it. But you should set a frequency to make sure that you don’t over-message your audience! Set a goal and conversion criteria. By default, an anonymous visitor converts if they become an identified profile within one week of clicking a tracked response (e.g, a link or button) in an in-app message. You can optionally add conditions to the goal like people must perform an event or enter or leave a segment. You can also change the conversion time period as well as the criteria from “clicking” to “viewing” the message depending on your needs. Create your message and then click Next. Here are a few tips to keep in mind: Use the Display setting to determine where your message appears. You can’t use 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}}. in your message because we don’t have any information about the person you’re messaging. They’re anonymous! The goal for most anonymous messages is to identify your audience. Write your message with a call-to-action and maybe even a button that encourages people to sign up, create an account, or provide their email address. Schedule your message. You need to determine when your message starts and ends. Review your message and click Launch anonymous message. Best Practices Because anonymous messages will go to everybody you haven’t identified yet, you may want to target your message to specific pages. You can set Page Rules to determine where your message will appear. You should also set a Frequency to make sure you don’t over-message your audience. You don’t want to annoy your visitors! Remember that your primary goal with anonymous messages is to get people to identify themselves, so your message should encourage them to sign up, create an account, enter their email address, and so on. You may want to add a button to your messages that takes people to a signup or login page. Style your message to match the look and feel of your website or app. This helps your audience trust both your messages and your website or app. Anonymous metrics: measure effectiveness When you show an anonymous message, we track three things: Views: The number of times anonymous visitors saw your message. Clicks: The number of times anonymous visitors clicked buttons in your message. We track clicks for each button. Converted: The number of identified profiles that met your goal and conversion criteria. (If multiple anonymous IDs were merged into the same identified profile, that counts as one conversion.) You can use these metrics to determine how effective your message is. If you see a lot of views but few clicks, you might want to experiment with your message’s content, call to action, or button text. FAQ How do I know if I’m set up to send in-app messages? Go to Settings > Workspace Settings > In-app. If in-app messages are enabled, then you’re ready to go! What is an anonymous person? An anonymous person is anybody you haven’t yet identified by ID or email address. In most cases, this means someone you can’t message—you can’t send them an email, an SMS, push notification, and so on because you don’t know them yet. When someone visits your website or uses your app for the first time, we assign them an anonymous ID. For our JavaScript client, this ID lets us track their activity on your site or app. You can see anonymous activity in the Activity Log, but you won’t see a “person” in your workspace. For our mobile SDKs, we won’t show anonymous activity in the activity log, but we can use their screen events to make sure we surface messages to people who visit the right screens in your app. When you formally identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them by ID or email address, we: Create or update a profile for them in Customer.io. You’ll see them in the People section of your workspace. Merge their anonymous activity with the identified profile. This lets you do things like message people based on their anonymous activity after you identify them. Your goal with anonymous messaging is to turn someone from an anonymous user into an identified person. Where does my message appear? When you set up your message, you can determine both: Which pages or screens your message will appear on. These are called Page Rules. Where on the page or screen your message will appear. These are called Display preferences. If you don’t set Page Rules, your message will appear on every page on your website or mobile app. That might be overwhelming for your visitors, so make sure you set page and frequency settings to make sure you don’t over-message your visitors. By default, we display messages as modals in the center of your page. That might obstruct your content, so you can set Display preferences to move your message. For example, rather than a modal message that covers your content, you might want to move to an Overlay style message that appears in one of the corners of your page. How many times will a user see my message? A user will see your message on every page meeting your page rules until: You identify them. Once you identify a person, they’re no longer anonymous, and they won’t see your anonymous message. They meet your frequency settings without dismissing the message. Imagine that you set a Frequency setting of 2. If a person sees your message once, they can see it again on another page or screen in your app. If they dismiss the message, they won’t see it again. If you want to show a message until your audience interacts with it, you can set the frequency to Show Always. If a user has already seen your message but clears their browser cache, or you otherwise reset their anonymous ID in your app, they’ll get a new anonymous_id. We’ll treat this new ID as a new person, and they’ll see your message again. flowchart LR a(Personvisits website) a-->b{Is person anonymous?} b-.->|No, they've been identified|c(Do not show anonymous message) b-->|yes|d{Does the page match the message's page rules?} d-.->|no|c d-->|yes|e{Has the person reached the message's frequency limit?} e-.->|yes|c e-->|no|f(Display anonymous message) --- ## Set up your notification inbox URL: https://docs.customer.io/journeys/inbox-setup/ Unlike other messages, inbox messages aren't displayed immediately to people who visit your website or use your app. Instead, you'll display these messages through a custom notification inbox that your audience can access at their leisure. The notification inbox lets you send messages to your audience that they can access at their leisure.  We’re actively working on this feature We’re just getting started with inbox messaging. For now, the feature requires some extra work on your part to build your own inbox, style messages in your client, and listen for events. We’re working on features to make this all easier. How it works Inbox messages aren’t displayed immediately (or along our normal prioritization settings for in-app messages), but rather are displayed through a custom inbox UI. Your audience typically sees them when they open the inbox in your app or on your website. Check out Colby’s video below for a quick overview and demonstration of the feature. Inbox messages differ from other messages in two important ways: Unlike other message channels, messages aren’t “delivered” in the traditional sense. Instead, you’ll fetch them when people visit your site. Inbox messages aren’t delivered as rendered messages. They’re delivered as JSON payloads. So when people visit your site, you’ll fetch messages. Then when people open the inbox, they’ll see an interact with messages. General setup process Before you can send inbox messages, you’ll need to do a few things first: Make sure you’re set up with our JavaScript SDK on your website. See our in-app messaging documentation for more information. Set up your inbox. Tell your team members what payloads, types, and topics to use in messages: because inbox messages are delivered as JSON payloads, your coworkers—anybody who uses Customer.io to send inbox messages—needs to know what to send in messages. When you’re done, you’re ready to send messages messages! flowchart LR a{Have you added the JS SDK to your site?} a-->|yes|b(Set up your inbox) a-.->|no|f(Add the JS SDK to your website)-.->b b-->c{Does your inbox expect anything more than title and body?} c-->|yes|d(Tell your team about payloads) c-.->|no|e d-->e(Send inbox messages) Minimum supported versions To support the notification inbox, you need to use of our client SDKs. See the table below for minimum version requirements. If you use our JavaScript snippet, you don’t need to worry about the version requirements. We’ll automatically use the latest version of the SDK when people visit your website. But any other import method needs to use a version of the SDK that supports the inbox feature. The examples on this page are for our JavaScript package. See the documentation for your mobile SDK for help setting up the inbox in your mobile app. SDK Minimum required version JavaScript Snippet N/A; automatically get latest version on page load JavaScript import cdp-analytics-browser 0.3.11 iOS 4.2 Android 4.16 React Native 6.2 Flutter 3.3 Expo 3.1 The message payload Inbox messages are delivered as a JSON payload. Our SDKs help you listen for the payload and render messages in your inbox. By default,messages contain a properties object with a title and body, but you can send arbitrary JSON—whatever you set up your inbox to expect. So, when you set up your inbox, make sure that your team members understand the structure of the payload—which fields are required, and which topics or types of messages your inbox expects. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey, {{customer.first_name}}, your order shipped!", "body": "You can track your order #{{event.order_number}} here:", "link": "https://example.com/orders/{{event.order_number}}" } } Field Type Required? Description messageId string true Unique identifier for the message. sentAt string true When the message was sent. expiresAt string true When the message will expire. opened boolean true If true, the message has been opened. topics array false The topics that the message belongs to. type string false The type of message. properties object true Your custom data payload representing the content you want to render. The payload cannot be empty. Topics and Types The topic field acts as a way to filter messages when you call the inbox() method. But, because these two values are also included in the message payload, you can use the topic and type fields to help you render messages. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Set up your inbox If you haven’t already, you’ll need to set up in-app messaging. See getting started with in-app messages for more information. On your website, you’ll use our JavaScript SDK’s inbox() API to retrieve and manage inbox messages. When you’re done setting up your inbox, you’re ready to send messages and display them to your audience. Our SDKs support the following methods: total() / totalUnopened(): Get the total number of messages, or the number of unopened messages in the inbox. markOpened() / markUnopened(): Determine whether the message was opened or not. markDeleted(): Delete the message. trackClick(actionName?): Track click metrics. If your message has a button or a link, you can use this method to indicate a click on the message. Access the inbox When you fetch messages, you can filter by the “topics” that the message belongs to. This lets you—or your audience—determine the messages the inbox displays. You’ll set the topics that a message belongs to when you send the message. If you don’t allow users to filter messages, and you just want to display all messages to users, you can fetch all inbox messages without parameters. // Get all inbox messages const inbox = analytics.inbox(); // Filter by topic const orderInbox = analytics.inbox('orders', 'shipping'); Get Messages // Get all messages const messages = await inbox.messages(); // Get counts const total = await inbox.total(); const unopened = await inbox.totalUnopened(); // Subscribe to updates const unsubscribe = inbox.onUpdates((messages) => { console.log('Inbox updated!', messages); // Update your UI }); Notification inbox code example Here’s a simple example for an inbox UI. This example assumes you’ve already set up your backend to trigger inbox messages and you’ve already loaded the Customer.io JavaScript SDK. // Cache messages and inbox instance to avoid re-fetching in each handler. // Alternatively, you could call inbox.messages() in handlers for simpler code // at the cost of an extra async call on each button click. let currentMessages = []; let inboxInstance; // Helper to escape HTML and prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Render function function renderInbox(messages) { currentMessages = messages; // Store for handlers const container = document.getElementById('inbox'); container.innerHTML = messages.map(message => { const props = message.properties; return ` <div class="inbox-message ${message.opened ? '' : 'unread'}"> <div class="inbox-message-content"> <h4>${escapeHtml(props.title)}</h4> <p>${escapeHtml(props.body)}</p> <small>${new Date(message.sentAt).toLocaleDateString()}</small> </div> <button onclick="handleRead('${message.messageId}', ${message.opened})"> ${message.opened ? 'Mark Unread' : 'Mark Read'} </button> <button onclick="handleDelete('${message.messageId}')">Delete</button> </div> `; }).join(''); } // Update the unread badge async function updateBadge() { if (inboxInstance) { const count = await inboxInstance.totalUnopened(); document.getElementById('unread-badge').textContent = count; } } // Wait for analytics to be ready, then initialize inbox and load messages cioanalytics.ready(() => { inboxInstance = cioanalytics.inbox('orders', 'announcements'); inboxInstance.messages().then(renderInbox); inboxInstance.onUpdates(renderInbox); // Update unread badge updateBadge(); }); // Message actions async function handleRead(messageId, isOpened) { const message = currentMessages.find(m => m.messageId === messageId); if (message) { isOpened ? await message.markUnopened() : await message.markOpened(); await updateBadge(); } } async function handleDelete(messageId) { const message = currentMessages.find(m => m.messageId === messageId); if (message) { await message.markDeleted(); await updateBadge(); } } Import types for TypeScript If you use TypeScript or you use our cdp-analytics-browser package with a JavaScript framework like React, Vue, or something else, you can import types. import type { InboxAPI, InboxMessage } from '@customerio/cdp-analytics-browser'; Best practices Initialize the inbox within the cioanalytics.ready() callback. This ensures the Customer.io analytics SDK is fully loaded before you interact with the inbox. This is especially important if you’re using the inbox in a React component or single-page application. Update message state. Mark messages as opened when displayed, track clicks to measure engagement, and delete dismissed messages to keep inboxes clean. Test thoroughly. Before going live, verify your payload structure, confirm messages appear in your inbox API calls, test topic filtering, and check that state changes work correctly. --- ## Send inbox messages URL: https://docs.customer.io/journeys/send-inbox/ You can send inbox messages as a part of a campaign or broadcast workflow. Set up a message Before you try to send an inbox message, make sure you’ve set up your inbox. When you set up a message, you’re essentially determining the id or name of the message you want to send. You’ll use one of these values to send and populate your message when you’re ready to send it. In your campaign or broadcast workflow, drag an Inbox message block into your workflow. Click Add Content to set up your message. Define your message settings: Type: Provides a way to differentiate messages in your inbox. For example, if a message type is rich, that might tell your inbox client to display a rich message with support for images, etc. Expiration: The time between when the message is sent and when it should expire. Messages only expire if they’re sent but not delivered. By default, messages expire after 60 days. Topics: These are the topics that the message belongs to, used to filter the inbox (when you call analytics.inbox('topic1', 'topic2')). You can let your audience filter on topics when they open the inbox. In the JSON area, provide the JSON payload that represents your message. By default we provide a title and body field, but you can add other fields like an image or a link. You can use 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 include dynamic content from your trigger data, like {{customer.<attribute_name>}} to include customer attributes. If you reference event data, you’ll use {{event.<attribute_name>}}. For example, your message might look like this: { "title": "Hey, {{customer.first_name}}, your order shipped!", "body": "You can track your order #{{event.order_number}} here:", "link": "https://example.com/orders/{{event.order_number}}" } When you’re done, save your message. Personalize your message You can use 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 include dynamic content in your message. You can use profile attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in any message in the format {{customer.<attribute_name>}}. You can use event data in messages triggered by an event or an API call. Source Example Campaign Broadcast Newsletter Description Profile attributes {{customer.first_name}} Yes Yes Yes Attributes belonging the message recipient. Event data {{event.order_number}} When triggered by an event N/A N/A Data from an event that triggered the message. Trigger data {{trigger.order_number}} N/A Yes N/A Data sent in the message_data object when you trigger a broadcast or a transactional message. Message topics and types The message Topic provides a way to filter messages in the inbox, and the Type provides extra context to help you render the message. Neither field is required to send (or display) messages. These are values that you’ll implement when you set up your inbox. So if you’re not sure what values to use here, talk to team members who developed your inbox! If you don’t want to filter messages by topic, and you don’t want to assign “types” to your messages, you can leave these fields empty. Troubleshooting Inbox messages follow the same delivery patterns as regular in-app messages. Check our in-app messaging FAQ for additional troubleshooting. My message doesn’t appear Verify that you’ve enabled in-app and inbox messaging in workspace settings. Ensure the In-App Plugin loads (analytics.inbox should be defined). Check that the message isn’t filtered out by analytics.inbox(). If you call analytics.inbox('topic1') and the message’s topic is topic2, it won’t appear in the inbox. Check that the user is identified. Confirm that the message you intend for people to see hasn’t expired. You may need to investigate a specific deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. to see the expiration date. Topics don’t filter correctly Remember that topics are set in the message template, not the API call. Messages with empty topics arrays match all topic filters. Calling inbox('topic1') only returns messages that include 'topic1' in their topics array. --- ## Trigger inbox messages from your backend URL: https://docs.customer.io/journeys/send-inbox-txnl/ You can send inbox messages in response to user activity directly from your backend. While we call this a *transactional* message, it's not a transactional message in the traditional, legal sense—it's a message that you send to your audience that they can access at their leisure. How it works You can leverage our transactional messaging feature to send inbox messages immediately from your backend without triggering a campaign or broadcast. This gives you a way to send one-to-one messages using Customer.io without having your message-sending logic inside Customer.io. flowchart LR a{Did user perform behavior that triggers an inbox message?} a-->|yes|b(Trigger inbox_message) a-.....->|no|c(Message not sent) b-->d{Is the recipient on your website or app?} d-->|no, wait for user to return to your website or app|d h{Does the inbox send markOpened when the message is displayed?} d-.->|yes|e{Does the user open the inbox?} e-.->|no, wait for the user to open the inbox|e e-->|yes|h h-->|yes|i(Message is marked as opened) h-.->|no|j(Message is sent but not opened/delivered) Setup process Set up a message template: This represents the “template” for the message you want to send. You’ll also use 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 personalize the message for the recipient. Set up your backend to trigger your inbox message: This is where you’ll send the message payload to Customer.io. You’ll reference the template to make sure that you send the correct message. 1. Set up a message When you set up a message, you’re essentially determining the id or name of the message you want to send. You’ll use one of these values to send and populate your message when you’re ready to send it. Go to the Transactional page and click Create Message. Give your message a Name and a Description, then click Next: Add Content. The name and description help your team members understand what kind of message this is (like “Order Update”). Select Inbox as the message type and click Add Content. Define your message settings: Type: Provides a way to differentiate messages in your inbox. For example, if a message type is rich, that might tell your inbox client to display a rich message with support for images, etc. Expiration: The time between when the message is sent and when it should expire. Messages only expire if they’re sent but not delivered. By default, messages expire after 60 days. Topics: These are the topics that the message belongs to, used to filter the inbox (when you call analytics.inbox('topic1', 'topic2')). You can let your audience filter on topics when they open the inbox. In the JSON area, set the title and body of your message: these are the two “message” fields you’ll send with your message. You can send other fields in the payload when you trigger the message, but these are the two fields that represent the message itself. You can use 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 include dynamic content from your trigger data. Use {{customer.<attribute_name>}} to include customer attributes and {{trigger.<attribute_name>}} to include data from your payload—these are values you can set when you send a transactional message. For example, your message might look like this: { "title": "Order #{{trigger.order_number}} has shipped!", "body": "You can track your order for {{trigger.order_number}} here: {{trigger.tracking_url}}", } When you’re done, save your message and click Next: Configure Settings. Update your message settings. We recommend that you use the default settings, but you should Set a trigger name so that it’s easier to send your message later. By default, you’ll trigger a message using the transactional_message_id, which is the last number in the URL of your message; the trigger name makes this more human readable. Click Next: Send Message. Now you’re ready to send your message! You’ll need to update your backend to trigger your message. We also recommend that you test your message to make sure it displays the way you expect it to. Using Liquid in messages You can use 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 include dynamic content from your trigger data. Use {{customer.<attribute_name>}} to include customer attributes and {{trigger.<attribute_name>}} to include data from your payload—these are values you can set when you send a transactional message. Customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.: these are attributes that are already set on your audience. For example, {{customer.first_name}} corresponds to the first_name attribute on your customer’s profile. Trigger data: data sent in the message_data object when you trigger an inbox message. For example {{trigger.order_number}} corresponds to message_data.order_number in your payload. Your trigger data comes from the message_data object in your transactional message payload. Message types The message type gives you a way to categorize messages that you want to show in your inbox. You might do this if you want to let users filter the messages they see. If you don’t want to filter messages by type, and you don’t want to assign “types” to your messages, leave this field empty. If you send a message with a type that isn’t specified in the analytics.inbox() parameter, the message won’t appear to the user. 2. Set up your backend to trigger inbox messages You’ll trigger inbox messages using our inbox_message API. You send calls to this endpoint using one of our App API-based libraries (Node.js, Python, Ruby, Go) or by sending requests directly to POST https://api.customer.io/v1/send/inbox_message. Triggering an inbox message is fairly similar to the way you send events to Customer.io. But, where events can trigger campaigns or get used in other capacities, the inbox_message endpoint explicitly triggers a message for the end user. The following examples show how to trigger an inbox message using the libraries listed above. See our API documentation for more information on the inbox_message endpoint. transactional_message_id: The transactional_message_id is the ID of your transactional message. You can find this in the URL of your transactional message or in the code sample in the Overview tab for your transactional message. identifiers: The identifiers are the identifiers for the recipient. You can use the id, email, or cio_id identifier. message_data: The message_data is the data you want to include in your message. You can use 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 include dynamic content from your trigger data. Below are examples showing how to trigger an inbox message using our SDKs. Node.js Node.js const { APIClient } = require("customerio-node"); const client = new APIClient("YOUR_APP_API_KEY"); const request = { transactional_message_id: "order_shipped", identifiers: { id: "user_123" }, message_data: { order_id: "ORD-5678", tracking_url: "https://track.example.com/5678", product_name: "Blue Widget" } }; client.sendInboxMessage(request) .then(res => console.log(res)) .catch(err => console.log(err.statusCode, err.message)); Python Python from customerio import APIClient client = APIClient("YOUR_APP_API_KEY") request = { "transactional_message_id": "order_shipped", "identifiers": { "id": "user_123" }, "message_data": { "order_id": "ORD-5678", "tracking_url": "https://track.example.com/5678", "product_name": "Blue Widget" } } response = client.send_inbox_message(request) print(response) Ruby Ruby require "customerio" client = Customerio::APIClient.new("YOUR_APP_API_KEY") request = { transactional_message_id: "order_shipped", identifiers: { id: "user_123" }, message_data: { order_id: "ORD-5678", tracking_url: "https://track.example.com/5678", product_name: "Blue Widget" } } begin response = client.send_inbox_message(request) puts response rescue Customerio::InvalidResponse => e puts e.message, e.code end Go Go import "github.com/customerio/go-customerio/v3" client := customerio.NewAPIClient("YOUR_APP_API_KEY") request := customerio.SendInboxMessageRequest{ TransactionalMessageID: "order_shipped", Identifiers: map[string]string{ "id": "user_123", }, MessageData: map[string]interface{}{ "order_id": "ORD-5678", "tracking_url": "https://track.example.com/5678", "product_name": "Blue Widget", }, } body, err := client.SendInboxMessage(context.Background(), &request) if err != nil { fmt.Println(err) } fmt.Println(body) cURL cURL curl --request POST \ --url https://api.customer.io/v1/send/inbox_message \ --header 'Authorization: Bearer YOUR_APP_API_KEY' \ --header 'content-type: application/json' \ --data '{ "transactional_message_id": "order_shipped", "identifiers": { "id": "user_123" }, "message_data": { "order_id": "ORD-5678", "tracking_url": "https://track.example.com/5678", "product_name": "Blue Widget" } }' --- ## Inbox message metrics URL: https://docs.customer.io/journeys/inbox-metrics/ When you send an inbox message, it'll remain in the *Sent* state until the recipient returns to your website or app and opens the notification inbox. When the message is displayed to the user, you'll report that the message was opened. Inbox messages follow a similar delivery patterns as regular in-app messages. This means that messages are sent but not displayed until the recipient returns to your website or app. The major difference is that inbox messages aren’t “delivered” in the traditional sense. Rather, you’ll fetch and display messages to your audience when they open your notification inbox. When you mark a message as opened, we’ll report that it’s delivered as well. This means you’ll track opens and clicks using your inbox client, but the time between when you send a message and it actually appears to the user is dependent on your users’ behaviors. flowchart LR a(Send inbox message) a-->b{Is the recipient on your website or app?} b-.->|no, wait until they return to your website or app|b b-->|yes|c{Did they open the inbox?} c-->|yes|d(Message is displayed to the user) d-->f{Does the inbox send markOpened when the message is displayed?} f-->|yes|g(Message is opened) f-.->|no|h(Message is not opened) c-.->|no|e{Is the message expired?} e-->|no, wait for the user to open the inbox|c e-..->|yes|i(Message expires and is removed from the inbox) Use the Inbox API to mark status and delete messages In general, you’ll want to display the message when your audience opens the notification inbox. When users open the inbox and see your message, your inbox can report that the message was opened using the markOpened() method. You’ll do the same thing to delete the message when a user wants to remove it from their inbox or if you want to track clicks on the message. Use the following methods to update the message state and track engagement: markOpened(): Mark the message as delivered and opened markUnopened(): Mark the message as delivered but not opened markDeleted(): Mark the message as deleted trackClick(actionName?): Track a click on the message Available metrics Like other in-app messages, inbox messages have the following metrics. Note that the delivered metric is typically reported along with the opened metric when the message is opened, because, for inbox messages, “delivery” doesn’t really happen independently of visibility (as it might for email). Metric Reported by Description sent Customer.io The message was sent by the user. delivered markOpened() The message was delivered to the user. opened markOpened() The message was opened by the user. clicked trackClick() The message was clicked by the user. failed Customer.io We couldn’t send the message. This is typically caused by a 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}}. rendering issue. --- ## Forms URL: https://docs.customer.io/journeys/in-app-forms/ You can use in-app messages as forms to collect feedback from your audience.  We’re adding to this feature! Today, forms support text input, but we’re working on support for more input types—like checkboxes and radio buttons. How it works In-app messages—sent to your mobile app or website—can include form components to collect text-based feedback from recipients. When you add a Form or form-styled components to your message like Long Text or Short Text, we add a Form element to your message. Whenever someone submits the form, we’ll: Capture the submissions for your campaign, broadcast, or newsletter. Add people to a segment based on the name of the form they submitted. (Optional) Let you trigger campaigns based on the form submission so you can respond to users and thank them for their feedback. Create a form In a campaign or broadcast workflow, you can drag an In-App Message into your workflow. In a newsletter, you’ll just select the In-App Message option. Select a starting template, a previous message, or start from scratch. Set your message settings: Priority: negotiates between multiple messages with the same Page Rules and Display settings. Page Rules: determine the pages where a person can see your message. Set your message’s Display and Position. This determines where your message appears on a page. You can find these settings by selecting the message background. See Display and Position for more information. Drag a Form component into your message. Within the form, you can add additional form components like Short Text or Long Text fields. When you add additional components, you’ll have to drop them within the boundaries of the form element. Set a Name for the form. We use the name of the form to determine which forms people submitted. To select the form, you may want to use the breadcrumbs at the bottom of the message to select the Form element. If you started your in-app message from a another message, you might want to set a new name for the form so that you can differentiate between your in-app form submissions. Select the form inputs (short text, long text, etc) and set names for each field. Form field names will help you see which fields people submitted. See Field names for more information. Form components You can drag a Form into your message to add a free-form Long Text input and a submit button to your message. You can also drag other form components in your message, like additional Short Text or Long Text fields, but you must drop them inside the form element. If you add individual form components outside of the Form element, they won’t be captured when people submit the form! You can use the breadcrumbs at the bottom of the message to see the boundaries of your form—so you know where to add form components—or check the code preview to see which elements are inside your x-form element. Short text vs long text Short text and long text inputs are different in how they’re rendered. Both include labels, but you can expand a long text input to show a larger text area. You can’t do this with a short text input. Short text Long text Multi-line No Yes Expandable No Yes Max length 255 characters 500 characters The form name We automatically group people into segments based on the form they submitted, and you can use the form submissions directly to trigger campaigns. These features are based on the Name of the form component—not the Name of the message! When you add a form element to your message, you can set a Name for the form. You can also select the form component to set the name. If you need to change the name later, you might want to use the breadcrumbs at the bottom of the message to select the Form element; that can make it easier to find without trying to click through other form elements. Field names Each input you add to your form has its own name. Form field names are how we’ll identify the fields people submit. You’ll see these fields when you view or export form submissions. If you don’t set a name, we number the fields as “form field 1”, “form field 2”, and so on. We strongly suggest that you write your field names without spaces, like snake case (favorite_color), camel case (favoriteColor), or kebab case (favorite-color). This affects the way you reference field names in liquid as you personalize messages or rely on event properties in actions. Form submissions When someone submits your form, we’ll capture the feedback under your campaign, broadcast, or newsletter’s Form Submissions tab. Click Export all on the form submissions tab to download a CSV of all form submissions. Form submissions are also captured as events in Customer.io, so you can use them to trigger campaigns, create segments, personalize messages, and other things you can do with eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. in Customer.io.  Set unique form names If you don’t set a name for your form, it’ll get set to “In-App Form”; if you have multiple forms named In-App Form, you’ll capture people who filled out different forms in the same segment or campaign. Trigger campaigns from form submissions When someone submits your form, you might want to follow-up with them—to thank them for providing feedback, request a meeting, and so on. You can either use form submission events to trigger campaigns, or add people to segments based on form submissions and use that as future criteria for campaigns. When you start a new campaign, set the trigger to Form submission and select the Name of your in-app form component. You can also reference form submission fields in liquid as you personalize messages or rely on event properties in actions. See Using form submissions in messages or actions for more information. Segments based on form submissions We automatically create a segment for each in-app form using the Name of the form. You can use this segment to target people who have submitted your form. Using form submissions in messages or actions You’ll reference field names in 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}}. using the field name, like {{event.submissions.<field_name>}}. For example, if your form field name is favorite_color, you’ll access it in Liquid using {{event.submissions.favorite_color}}. While you can use spaces in field names, like Favorite Color, spaces make it harder for you to referene fields in Liquid and other areas where you might rely on event properties. For example, if you have a field named Favorite Color, you’ll access it in Liquid using {{event.submissions["Favorite Color"]}}. --- ## Lead capture URL: https://docs.customer.io/journeys/in-app-lead-form/ Use the Lead Form component for in-app messages to gather the names and email addresses of people who see your messages. How it works The Lead Form component is a form containing Name and Email fields. When people submit the form, we’ll add the person to your workspace so you can send them messages and convert them to a customer or user. This component helps you capture leads from anonymous messages. For example, you might send an anonymous message with a Lead Form that offers people coupons when they sign up with their email addresses. When they submit the form, we’ll add them to your workspace and you can send them an email or in-app message with their coupon code. Note that when people submit your form, we’ll add them to your workspace, but we won’t automatically identify them in the browser. By default, people remain anonymous as they browse your website or use your app. Learn more. The Lead Form component When you drag a Lead Form component into your message, it adds two things to your message: a box containing form fields and a submit button. You can customize both, but you’ll select them separately. For example, when you select the form, you can customize the names of the fields, text size, and so on. But, if you want to change the name of the button from Submit to something like Let’s go!, you’ll have to select the button itself. You must be a Workspace Admin to add a Lead Form component to your message. See Form integrations require Workspace Admin permissions for more information.  Add a Tracked Name for the submit button! The Tracked Name field helps you track when people submit your form. Make sure you select the submit button and add a tracked name to help you track the success of your lead capture form. The Name field People can provide their names along with their email addresses. This gives you a way to address people in follow-up messages. If you use this field, we’ll add a name attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to people who submit your form. If you separate names into first and last name attributes, or use other attributes to store names, you may want to disable the Name field and capture names some other way. Set a tracked name for the submit button The Tracked Name is how we group responses to messages. Setting a tracked name helps us do things like count the number of responses to your form and generate metrics like the percentage of people who submit your form. It also helps you understand what button people click—whether they dismiss your message or submit your form. Give your message a close button Not everybody will respond to your form. If you set up an anonymous message, and you don’t set it inline, make sure that you add a Close Button to your message so there’s a clear and obvious way for your audience to dismiss it. The lead form does not identify people in the browser session When someone submits the form, we’ll add them to your workspace, but we won’t automatically identify them in the browser. We’ll capture their anonymous activity up to that point, but anything they do after they submit your form will remain anonymous. flowchart LR a(Anonymous activity)-->b{Person submits lead form} b-->|In Customer.io|c(New person in your workspace) a-->|Merge anonymous activity with new person|c b-->d(Anonymous person on your website or app) Each form generates a new Form integration Whenever you add a form component to your message, Customer.io creates a new Form integration on the Integrations page. This integration has the same name as your anonymous message. Click the form integration to view submissions. From here you can: Trigger different campaigns for each independent form Track and troubleshoot form submissions separately from other integrations Form integrations require Workspace Admin permissions The lead form component generates a new “Forms” integration, and the ability to create integrations is limited to Workspace Admins today. If the lead-form component is disabled, it’s likely that you don’t have the Workspace Admin role and need to talk to your account administrator to upgrade your role. --- ## Surveys URL: https://docs.customer.io/journeys/in-app-surveys/ With Customer.io, you can send web and in-app surveys (NPS, CSAT, etc) using different arrangements of buttons. We track the buttons your audience clicks, and you can set up conditional branches in your campaigns or broadcasts to handle different responses to your survey. How it works You can use our in-app feature to send a message with an arrangement of buttons to solicit feedback from your audience—for net promoter score (NPS), customer satisfaction (CSAT), and other kinds of microsurveys. We track the buttons that people click or tap in your messages, so you can see how your audience feels about your products, features, and so on. You can even use responses to follow-up with people based on their feedback. Buttons in our surveys do two things: They record a rating or response. They dismiss the message. flowchart LR b(User gets in-app message) b-->c{How does the user respond?} c-.->|User closes message|d(No tracked response) c-->|User picks a survey option|e(Customer.io captures response) d-.->f(Dismiss in-app message) e-->f  Your survey could have multiple steps! A multi-step message provides a way to ask follow-up questions or thank your audience for their feedback automatically when they respond to your survey. See Multi-step messages for more information. Create a survey In any message that you want to act as a survey, drag Buttons into your message. When you select any of the buttons, you’ll see that they’re set up with: The action Behavior set to Dismiss. You shouldn’t change this value unless you want to link your audience somewhere in your app. The Tracked Name set to a value that’s meant to help you understand your rating scale. You might want to change this value to reflect your rating scale or your button text. For more about setting tracked names and strategies to help you group responses, see Tracked names and responses.  Use Tracked Name values to group responses If your buttons have tracked names 0-10, like in an NPS survey, you’d have to set up branches for each number. But, if you add detractor, passive, and promoter as tracked names, you can set up branches for each of those groups. This makes it easier to manage conditional branches. See Tracked names and responses for more information. Set up conditional branches based on survey responses You can use conditional branches to act on your audience’s responses to your surveys. For example, you might send a follow-up message to thank your audience for their feedback, or link them to information about a feature that they indicate interest in. To do this, you’ll add a Wait Until block to your message. This lets you wait until a person engages with your message and set conditions based on the button(s) people click or tap in your message. You’ll send people down different branches in your workflow depending on how they respond to your survey. In your campaign, add a Wait Until branch. Add your conditions: Click Condition and select Message in the Add condition dropdown. In the next box, select the message you want to listen to responses for. Change the condition to is clicked. In the last box, set the Tracked name you want to act on. Repeat this process for all the options in your message. When you’re done with your conditions, you should set a Max Time condition for people who never receive your message or dismiss it without picking an option. You might want to set your wait time to the expiration time for your message! This means that people have the maximum amount of time to respond to your survey before you act on their lack of response. Tracked names and responses Each button has a Tracked Name. This is the value we record when someone clicks or taps a button in your message. By default, the Tracked Name for each button matches the button text. So, for a row of 5 buttons, the buttons are named 1-5. You might want to change tracked names if you change the names of your buttons—both to make it easier to understand your data and to make it easier to set up conditional branches based on your audience’s responses. For example, if you have an NPS survey with buttons 1-5, you might want to set the Tracked Name for 1 to Very Dissatisfied and 5 to Very Satisfied; this’ll make it easier to understand your rating scale. We count your audience’s responses (for any button that has a Tracked Name) under Tracked Responses in the metrics tab for your campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or broadcast. We organize responses by message and action name, helping you understand how your audience responded to your messages and surveys. We don’t track the close button by default By default, the Close Button does not have a tracked name. But you might want to set a tracked name for the Close button if you want people to pass through a campaign even if they don’t respond to your message. Setting a tracked name for the close button lets you create a conditional branch for people who dismiss your message without setting an arbitrary Wait period. --- ## Inline messages URL: https://docs.customer.io/journeys/inline-in-app/ Unlike traditional in-app messages, which appear on top of your app or website, inline in-app messages act like a part of the content on your page. They let you dynamically populate parts of your app and talk to your customers without interrupting their experience. How it works Unlike traditional in-app messages, which appear on top of your app or website, inline in-app messages act like a part of the content on your page. They let you dynamically populate parts of your app and talk to your customers without interrupting their experience. To support inline messages, you need to add empty elements to your website or views to your app to contain your inline messages. Then, when you send an inline message, you’ll specify the Position ID of the for your inline message—the empty element or view where you want to display your message. You might need to work with your developer or marketer counterpart to work with inline messages. For example, if you’re a marketer, you might need to work with a developer to add elements or views to your app. And, if you’re a developer, you’ll need to work with whoever sends messages to figure out where you should place inline message positions and what IDs to set for them. flowchart LR a(Send inline in-app message) a-->b{Is the recipient on the right page?} b-->|yes, page matches page rules|c{Does the position ID exist?} c-->|yes|d(Display inline in-app message) b-.->|no, page doesn't match page rules|e(Don't display inline in-app message) c-.->|no|e Set up your website for inline messages Wherever you want to display an inline message, you’ll need to add an empty element (like a div) with an id attribute to your website. When you set up your message in Customer.io, you’ll use the id of your element in the Position field. You can add multiple positions to your website, so you can display inline messages wherever it’s convenient for you and your users. For example: <div id="inline-message-container"></div> Avoid setting height and other constraints on your position element. Our SDKs automatically adjust the size of the container when messages load or users interact with them. Setting a fixed height might interfere with message rendering. Set up your mobile app for inline messages To support inline messages in your mobile app, you’ll need to: Update to a version of our SDK that supports inline messages. See the table below for minimum version requirements. Add views to your app where you want to populate content from Customer.io. When you set up your message in Customer.io, you’ll use the ID of your view in the Position field. Avoid setting height and other constraints on the view in your app. Our SDKs automatically adjust the size of the view when messages load or users interact with them. Setting a fixed height might interfere with your message. Platform Minimum SDK version iOS 3.8 Android 4.6 React Native 4.4 Flutter 2.3 Expo 2.1 Send an inline in-app message Before you can send inline messages, make sure that you’ve set up your website or mobile app to support inline messages. When you set up your message, you’ll select the Inline Display option and set the Position ID where you want to display your message. The position ID is the id attribute of an element you want to replace on a page or screen; you might need to talk to a developer to get your position ID. We recommend that you set your inline message to persist until it expires or is dismissed. That way, you aren’t left with an empty spot in your app or website. Message priority for inline messages The priority setting for messages only affects the order of delivery; we won’t replace messages that are already displayed based on priority. If you send multiple inline messages to the same Position ID, and a user hasn’t received a message yet, we’ll display the highest-priority message. But if a user has already received a message, the next message will only appear when the user dismisses the current message, or the current message expires, regardless of priority. For example, if a person visits a page and gets a low-priority inline message, then you send a higher-priority message to the same Position ID, they’ll continue to see the low-priority message until they dismiss it. Time to display We poll for in-app messages when a customer opens your website or app. This means that when a customer first opens your app or website, it might take a moment for the message to appear. But we’ll cache the message in the user’s browser or app when it first appears to the user, so it’ll continue to appear when a customer refreshes or returns to your app or website. Inline message metrics When you send a message, we’ll record that it’s been Sent. Your message sits in queue until a person opens your app or website. When people visit your app and we display the message, we’ll show that it’s been Opened—and then it becomes eligible for subsequent metrics like clicks, etc. We don’t record subsequent impressions. If you set up your message to persist (which we recommend), it’ll cache on the client and continue to display until it expires or is dismissed—but we’ll only record the Opened metric when we first deliver the message. Why is my message sent but not opened? When you send a message, it waits in queue until someone opens your app or website. It’s typical that messages remain in the Sent state for a while. But if you notice that most, or all, of your messages remain in the Sent state, you should check that: The Position ID exists. Make sure you typed the correct ID in the Position field. Your message’s page rule specifies pages in your website or app where the Position ID doesn’t exist. If you send your message on pages where the Position ID doesn’t exist, we won’t display your message. flowchart LR a(Send inline in-app message) a-->|message is Sent|b{Is the recipient on the right page?} b-->|yes, page matches page rules|c{Does Position ID exist?} c-->|yes, message is Opened|d(Display inline in-app message) b-.->|no, message remains Sent|e(Don't display inline in-app message) c-.->|no, message remains Sent|e --- ## Multi-step messages URL: https://docs.customer.io/journeys/multi-step/ Multi-step messages let you send a single in-app message that contains sub-messages or steps, if you will. With multi-step messages, you can respond to user inputs or feedback without having to set up multiple messages or complicated campaign logic. How it works You can add multiple steps to your in-app message, where users progress through each step as they interact with your message—click a button; go to the next step within the in-app message. You might do this to walk new users through an onboarding flow, showcase new features in your app, immediately respond to surveys, and so on. Each step has its own settings including display, position, and content. To step through your message, you’ll use the show step action. This action determines which step in your message to show in response to user input. For example, if your message contains a survey, you might provide two steps after your survey: one thanking users who respond and another telling users where they can provide feedback if they opt not to respond. You can link these steps to different buttons in your survey with the show step action, so your message adapts to your users’ behavior immediately and appropriately. Set up a multi-step message In your in-app message, click Steps in the upper left corner. Click Add step to add a new step to your message. Update the Display and Position for your step. By default, steps inherit these settings from the previous step, but you might want to set different settings for each step. This is useful for things like product tours. See Display and Position for more information. Go to the step that should lead to your new step and select a button, link, or any “clickable” element. Then go to the Action area and set: Behavior to Show step. Target step to the step you want to show when the user interacts with the button, link, or other “clickable” element. Tracked name to something descriptive—this is what you’ll see in your campaign’s metrics. For example, if your message contains two steps, you might go to a button on the first step and set the action to Show step, with the Target step set to the second step in your message. Steps can operate in any order. For example, if your first step has multiple buttons, you could send your audience to different steps based on which button they click. Field Example Value Description Behavior Show step Shows another step in the same message. Target step Message 2 The step you want to show when the user interacts with part of your message. You don’t have to send people through your steps sequentially. You can send them to different steps based on their input. Tracked name go-to-message-2 The event name you want to use to track this interaction.  Set the tracked name to something descriptive In your campaign’s metrics, the Tracked Responses area shows individual clicks for each step in your message. If you set the Tracked name to something descriptive, it’ll be easier to tell which steps your users interacted with! Create a tooltip tour You can create a product tour by setting up a multi-step message with tooltips. Each step in your tour can point to a different element on the page, guiding people through your interface one feature at a time. Set up a multi-step message and add a step for each stop in your tour. For each step: Set the Display to Tooltip Configure the Target (CSS selector) and Position for that step’s element. Each step can point to a different element—for example, step 1 might target #inbox-button and step 2 might target #compose-button. Add a button or link on each step with the Show step action to advance people to the next tooltip in the tour. Click Select Target to open a live preview where you can click Select Element and visually pick the anchor element for each step. You might also add buttons on each step to close/skip the remaining steps in the tour to give users a way out if they don’t want to continue the tour. On the final step, include a close button or CTA so people can dismiss the tour.  Mix display types in a tour You can mix tooltip steps with other display types in the same multi-step message. For example, you might start with a modal introducing a new feature, then guide people through a series of tooltips pointing to specific UI elements. Best practices for tooltip tours Keep tours short—3 to 5 steps is ideal. Longer tours risk losing people before they finish. Target elements that are visible on the page when the tooltip displays. If a target element doesn’t exist, your tooltips won’t appear. Use page rules to make sure your tour only appears on pages where all the target elements exist. Use descriptive tracked names for each step’s CTA so you can see where people drop off in your tour metrics. Renaming, duplicating, and deleting steps You can rename, duplicate, and delete steps in your message, but remember that we use each message’s name to determine which message to show. If you delete or rename a message, you’ll need to update the show step action in your journey to use the new name. In your in-app message, click Messages in the upper left corner. Click and select the option to rename, duplicate, or delete the message. Update the show step action for any steps in your message that relied on the deleted or renamed message! Multi-step metrics A multi-step message is still considered a single message. We aggregate metrics for the complete message. This means that when someone sees your message, we’ll record an Open, regardless of how many steps they progress through in your message. We’ll record a single click for the entire message, whether someone clicks on one step before dismissing the message or progresses through all the steps in your message. But, while we aggregate metrics for the complete message, you can see the Tracked names people clicked in the Tracked Responses area, including clicks for every step in your message. Make sure that you give your buttons, links, and other actions descriptive Tracked names so that you can easily tell where and how people interact with your messages. Listening for step changes You can listen for events from in-app messages, like when someone closes a message or clicks a button. For multi-step messages, you can listen for the Message Changed event, which fires when someone engages a step in a sequence. You can use this event to add special handling for people who progress through your message—like highlighting a new feature on a page as someone moves through a tutorial. The code changes a bit depending on whether you’re using our JavaScript library for your website or one of our mobile SDKs, but the event contains an actionName parameter that tells you the name of the step shown to a user. window.addEventListener('in-app:message-changed', (event) => { console.log(event.detail.actionName); }); See the JavaScript library reference or your SDK’s in-app documentation for more information. Best practices Give your steps descriptive names. By default, we call each step “Step 1”, “Step 2”, and so on. But, if you give your steps descriptive names, it’ll be easier to remember, or communicate with your team, what each step is for. For example, you might have multiple steps in your message that don’t display sequentially depending on user inputs! You might want an introduction step, a call to action step, and so on. Set descriptive Tracked names for your show step actions. This makes it easier to tell which steps your users interact with. Not only does this help you determine engagement in Tracked Responses metrics. You can also trigger follow-up campaigns and add people to segments based on the tracked name. Tracked names in events and downstream campaigns You can use tracked names in events and downstream campaigns. For example, you might want to trigger a follow-up campaign based on whether someone clicks a button on the second screen in your message. --- ## In-app metrics URL: https://docs.customer.io/journeys/in-app-metrics/ When you send an in-app message, you can see its status per person, and metrics to determine how it's performed across your audience. How it works When you send a message to a person, Customer.io reports the status of your message until it’s Sent. After that, we rely on the device to tell us when a message is Opened. We aggregate these statuses as metrics, helping you determine how your message performed across your audience. However, we begin displaying metrics before your message expires. While metrics can always help you understand how your audience responds to your message, you should probably consider metrics incomplete until your message expires and your in-app message stops being sent. flowchart LR k[message Sent]-->d{is the app or website open?} d-->|yes|f[message Opened] d-.->|no|e[message Sent] f-->g{Does the user engage with the message?}-->|yes|h[message Clicked] g-.->|no|i[message Opened] e-.->|user opens app or visits website|f e-.->|user doesn't open app or visit website|j[message expires, stays Sent] Statuses: the state of an individual message A message status is the last metric reported for an individual in-app delivery: the state of that delivery for a person. A Sent message only indicates that we’ve created the message intended for a person, but it hasn’t reached a person’s device yet. The message remains in this state until a person opens your app or visits your website. Unlike other messages, in-app messages are displayed as soon as a person opens your app or visits your website; a person doesn’t have to do anything to “open” the message, so in-app messages don’t have a “delivered” state. Instead, we consider in-app messages Opened as soon as they’re displayed. When a person engages with the message—they tap a button or click a link in the message—it is Clicked. If your message doesn’t have something a person can engage with—a button, link, etc—it’ll never be Clicked. Sent This indicates that a person has triggered an in-app message—the campaign workflow has reached an in-app message—and we’ve created the individual message (known as a deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery.). However, the message hasn’t left Customer.io until it’s been Opened. A message can be Sent but not Opened if a person doesn’t open your app or website to the appropriate page and the message expires. Opened The message has been delivered and displayed to a person. Clicked The person clicked an Action in your message that has a Tracked Name. If a person clicks an item that doesn’t have a Tracked Name, like a close button, the message will remain in the Opened state. See Track and measure responses for more information. Failed We couldn’t render your message, and it never left Customer.io. Message failures are most commonly caused by unresolved 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}}.—a variable didn’t exist and didn’t have a fallback, making the message unreadable. When you see a failure, you may want to check the liquid in your message and ensure that you have a fallback in place—an if statement that uses a static value if a variable doesn’t exist for a person. Metrics When you send a message, we’ll show the metrics for your message—the total number of deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. that achieve each message status. Metrics can help you understand how well your message performs with your audience. In particular, your clicks-to-opens ratio helps you understand how many people who received a message followed a call to action. Your message will continue gathering metrics until it expires. So, when you check your metrics, understand that they may be incomplete—at least until your audience opens your app, goes to your website, or the message expires. Track and measure responses To track clicks in an in-app message, you must set a Tracked Name for the action—where the action is a button, link, or other clickable item in your message. We’ll also track the elements with Tracked Names that people click in the Tracked Responses section of your campaign’s Metrics tab. This helps you understand exactly how your audience responds to your messages and microsurveys beyond the general Clicked metric. Make sure you give your actions descriptive names so that you and your teammates can understand how people respond to your in-app messages at a glance! Only set names for actions you want to track If you set a Tracked Name for an action, we’ll track it—both as a message click and as a tracked response. For things like a close button, you may not want to set a tracked name—otherwise, you’ll record Clicked metrics when people dismiss your message. We often think of a “click” as a successful interaction, and dismissing a message is probably not the kind of interaction you’re looking for! So, for elements you don’t want to track, leave the Tracked Name blank. --- ## Test your messages URL: https://docs.customer.io/journeys/test-in-app/ You can send test messages and preview in-app messages on your website before you send them to a live audience. These features help you validate your integration and make sure your messages look and behave the way you want them to. There are different ways to test your in-app messages: Live preview: Preview your web message in real time on your actual website. Adjust display settings on the fly and see changes sync back to the editor automatically. Send a test message: Send a one-time test of your message to check that it shows up in the right place and behaves the way you expect. Send test Live preview How it works Sends a one-time test message Opens an interactive session Settings Uses editor settings Adjustable on the fly via preview bar Platforms Web and mobile Web only Duration One-time 30-minute session Preview your message on your website Live preview lets you see your in-app message on your actual website as you build it. Unlike Send Test, which sends a one-time test message, live preview opens your website in a new window. You can adjust display settings—like position, display type, and max width—directly on your website and see your changes reflected in the editor in real time. This is a great way to make sure that your message looks and behaves the way you want it to before you send it to your audience. To start a live preview session: (Optional) In your in-app message, select a person you want to preview your message for. This populates the preview message, so you can test 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}}. in your message. Click Live Preview. Enter the URL of the page on your website where you want to preview the message. Click Start. Your website opens in a new browser window with a preview bar at the bottom. Use the preview bar to adjust display settings. Changes you make in the preview bar sync back to the editor automatically. For tooltip messages, your tooltip won’t appear unless you’ve already selected a target element. If you haven’t selected a target yet, click Select Element in the preview bar and click the element you want to anchor the tooltip. This automatically generates the CSS selector for you. Return to Customer.io and click End Preview to end your preview session when you’re done.  Preview sessions last up to 30 minutes If your session expires, you can always click Live Preview again to start a new one. Send a test message from a campaign or broadcast In a campaign or a broadcast workflow, you can click Send Test to test a real message. This lets you make sure that your message behaves the way you expect: it shows up on the right page or screen; it appears in the right place on a page or screen; and the content all renders as you expect it to. Click Send Test. Enter the email or ID of the person you want to send your test to and click Send Test one more time. Open the app or website where you expect the test to appear and verify that your message works the way you expect it to. If you’re sending your very first message or test, you should start with your app or browser closed. If your app or browser are open when you send your very first test message, you may need to close and reopen your app, clear your browser cache, or refresh your page to get your message right away! 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. Test your page rules Page rules may contain regular expressions, and the rules along which we evaluate them can be hard to understand. That’s why we have the Test Page Rules section below the page rule editor. Here you can enter a URL or page path and see whether your message will appear or not. Use tests to fine-tune your page rules and make sure that your message appears on the right pages before you send it. Tips for testing messages You probably want to have some debugging tools available when you open your message. Nothing fancy but: For your website: open the inspect view in your browser so you can verify the styles in your message. You may even want to inspect the network calls to make sure that your buttons perform the correct actions. For your mobile app: you may want to set the Customer.io SDK log level to debug so you can trace every aspect of your in-app message, from receipt to dismissal. Close your browser or app before you send your first test! Before you send your very first message, we poll for messages at a slow rate. We update this rate when you send your first message—but only if your browser or app is closed. If you have your app or website open when you send your first message, it may take up to 3 minutes to see your message. This delay only occurs for the very first message you send from Customer.io. If you send your first test before you close your app or browser, you can clear your cache or restart your app to actively poll for messages. Troubleshooting tips for your in-app integration If you send a test message and don’t receive it: Check that you sent your test to the correct user. Make sure that you’ve identified the person you want to send your test to. If testing your website, you might try sending a manual identify call in the console to get your message. If you get the message after you send an identify call manually, then your problem might be in how you send identify calls from your code! cioanalytics.identify(<id or email>) Check your browser console or debug messages for errors on your website. You can trace traffic to and from Customer.io to find the error. On your website, you should see calls for i (identify) and p (page) before you get your message. If you have a single page app, make sure that you’ve set up page calls correctly. We only send page calls automatically when a page loads. If you have a single page app, that won’t happen, so you’ll need to send page calls manually when the route changes. --- ## Localize messages URL: https://docs.customer.io/journeys/localize-in-app-message/ If you have a localized app, you may want to deliver in-app messages in the language your audience uses. There are various language settings that can help ensure that your audience gets messages in their preferred language. How it works Customer.io has built-in localization features to help you deliver messages in the language your audience uses. You can send your audience translated versions of a message based on a specific attribute in Customer.io. This means that you’ll need to store language information in Customer.io. When a customer’s language attribute matches a localized version of your message, we’ll deliver that message to them. If we don’t have a localized version of your message for a customer’s language, we’ll deliver a “default” message. Beyond that, there’s also an accessibility Language setting you can use to tell your audience’s device or browser what language and locale you’ve formatted a message in. This setting does not determine which localized version of a message you send to your customers. But it does help browsers and assistive technologies support your audience. The Language setting: helps screen readers use the correct pronunciation and cadence. helps your users’ devices or browsers translate a message if they don’t receive versions of your message in their preferred language. You can use these two settings together to ensure that your audience understands your messages. Localize an in-app message If you have a localized app, you may want to deliver in-app messages in your audience’s preferred languages. Remember, we deliver localized messages to your audience based on a specific profileAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., not their browser or device settings. So, before you localize messages, you’ll want to set up your language attribute. When you create an in-app message, you’ll start with your Default message. This will act as a “template” for your other languages. It’s also the language that everyone receives if you don’t set up a message that matches their language attribute—a fallback message. Then you’ll click Add Language and select the different languages you want to support. You can then edit the message content for each language you add. Your localized messages are based on your default message, so you’ll only need to edit the text content of your messages! For buttons and other actions, you might want to change URL/Link settings to match the language of the message, but you’ll want to leave the Behavior and Tracked Name settings the same across all languages. This helps us track the performance of your message across all languages.  Set the Advanced > Language setting for your default message! This setting ensures that anybody who gets the default message can easily translate it in their browser (or app/OS) if it isn’t right for them! See Language Settings below for more information. Send your in-app code for translation If you need to send your messages to a translation partner, you can click to see the HTML for your Default message. From here, you can easily copy your message code and send it to a translator. You can even leave comments in the code view to help your translator understand what they need to translate and what they should ignore. Your translation provider can return your translated content in the same format, and you can copy and paste it back into the message editor’s code view. Use the Language setting The Language setting is an accessibility feature that explicitly tells a browser the language of your message. When your audience translates a message in their browser, the Language setting tells the browser what language the original message is in so it can provide a more accurate translation. The language setting ensures that anybody who doesn’t receive a localized version of your message can easily translate it in their browser (or app/OS). It also tells screen readers which language and locale your message is in so they read your message with the correct pronunciation and cadence. --- ## In-App FAQ URL: https://docs.customer.io/journeys/in-app-faq/ Frequently asked questions about in-app messages in Customer.io Can I stop an in-app message after it’s sent? In most cases, you can’t stop messages after they’re Sent. You’ll want to set an Expiration period for your messages to make sure that your messages expire when they’re no longer useful. However, if you’re willing to stop a campaign, you can use the Exit Immediately option to cancel in-app messages that have been sent but not opened. Note that stopping messages in this way only works for in-app messages. You can’t stop other kinds of messages in this same way. You will cancel in-app messages that have been “Sent” but not “Opened” when you: Delete an in-app action from a campaign workflow. This recalls any unopened messages associated with the action. Delete a campaign containing in-app messages. This recalls any unopened in-app messages sent by the campaign. Stop a newsletter that was in-progress. This recalls any unopened in-app messages sent by the newsletter. Delete a newsletter. This recalls any unopened in-app messages sent by the newsletter. Delete an API Triggered broadcast containing an in-app message. This recalls any unopened in-app messages sent by the broadcast. Disable in-app messaging for your workspace. This recalls any unopened in-app messages for the entire workspace. How do in-app messages handle unsubscribes? In-app messages ignore our standard unsubscribed attribute. People can’t unsubscribe from in-app messages unless you give them that option through a custom subscription center with an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you use with segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. to prevent your messages from going to people. Abusing in-app messages can cause your audience to ignore your messages or delete your app. When you set up in-app messages, be conscious of the frequency and relevance of the messages you send. To make sure that you don’t over-message your audience, you might: Set up page rules to ensure that your messages are relevant to the pages/screens your audience visits. Set up delays between in-app messages in campaigns. Filter people out of campaigns if they received messages or other campaigns within the same time frame. What happens if a person doesn’t have a version of my app that supports in-app messages? If you send a message to a person who hasn’t updated their app to a version supporting in-app messages, your message will appear as sent, but will never be opened. If you want to filter out people who can’t yet receive your messages, you can set up a segment to target people with an appropriate version of the Customer.io SDK. Why are my messages delayed? How do I send real-time in-app messages? When a message is “sent” from Customer.io, the Customer.io web and mobile SDKs fetch and store it on the browser or device; they’ll wait for your audience to match the the page rule(s) specified in the message before they’ll display it. This process makes messages appear quickly to users when they open a page. But event-triggered messages aren’t sent until a person performs an event. An event has to travel back to Customer.io before we’ll send the corresponding in-app message. This results in a delay between when a person triggers an event and when the message appears in your web or mobile app. If you want in-app messages to appear in real-time, you’ll need to send them in advance so that the messages are stored locally when a user begins their session. You can use page rules to display messages immediately based on your audience’s activities in your app rather than waiting for an event trigger your message. Here’s how you can take advantage of caching to send real-time in-app messages: Use a campaign or broadcast to trigger the message(s) to your target audience. Set up each in-app message with a custom page rule determining it will appear. When a user opens your app, all in-app messages with the Sent status will be retrieved and saved in the device’s local storage, waiting for the user to visit the right page or screen. When the user goes to the page/screen defined in a message’s page rule, they’ll immediately trigger the in-app message, retrieving it from local storage. If your use case doesn’t have a target page, use a custom page or screen view instead to force the in-app message to show. This call is processed client-side and cause the in-app message to show immediately. In the long term, we plan to extend page rule functionality to events so that our SDKs can trigger messages on the browser or device. Until then, you’ll want to use page rules to achieve real-time display. Can I use page rules for in-app messages on my website even if I don’t send page events? Yes! If you don’t send page events, we’ll use window.location.pathname to determine the page that a person is on. This means that page rules will always work for in-app messages on your website, even if you don’t log page events. How do I send in-app messages to a specific platform? By default your message will appear on the first page or screen your audience visits in any platform where you support in-app messages—web, iOS, or Android. When you add a page rule, you automatically exclude platforms not covered by your rule. Click Add page rule when you setup your message to select the platform you want to send your message to. When you set up a page rule, you can enter a page path or screen name—or you can simply set contains * to send your message on any page or screen on that platform. What is the value that I see in the Recipient field for deliveries? When you look at Deliveries & Drafts, the Recipient field shows a person’s cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. This is their canonical identifier in Customer.io—a unique value that we assign to each person in your workspace. We use cio_id to resolve in-app messages to the right people whether you identify them by ID or email. If you click the cio_id, it’ll take you to the person that you sent the message to—who you’ll probably recognize better by their email address or ID. How do I configure a button to open a link AND close the message? The openURL action doesn’t automatically dismiss a message. We’re working to add this option to the editor in the future. In the meantime, you can use custom classes and a short script to open a link and close your message when someone clicks a button/link: Add an in-app-button class to each button. Add the <script> in the example below to your message. This script listens for clicks on buttons (with the in-app-button class) and calls message.dismiss() to close the message. Example in-app message <x-base> <x-message border-color="#cccccc" border-style="solid" background="#ffffff" border-radius="8px"> <x-row :layout="[90,10]"> <x-column> <x-heading-3>Which option do you prefer?</x-heading-3> </x-column> <x-column> <x-image align="right" behavior="dismiss" width="28px" margin="0px" src="https://storage.googleapis.com/cio-in-app-templates/assets/close-padding.603304e2.png" /> </x-column> </x-row> <x-row :gap="16" :layout="[50,50]"> <x-column> <x-cta class="in-app-button" id="docs" href="https://customer.io/journeys/in-app-getting-started/" :new-tab="true" action="docs" font-weight="700" :full-width="true" tracked-response-name="Secondary" behavior="openUrl">Docs</x-cta > </x-column> <x-column> <x-cta class="in-app-button" id="landing" href="https://customer.io/journeys/in-app-messages" :new-tab="true" action="landing" font-weight="700" :full-width="true" tracked-response-name="Primary" behavior="openUrl" >Landing Page</x-cta > </x-column> </x-row> <x-watermark /> </x-message> <script> const inAppButtons = document.querySelectorAll('.in-app-button'); // Add a click event listener to each button inAppButtons.forEach(button => { button.addEventListener('click', () => { message.dismiss(); }); }); </script> </x-base> --- ## NPS Surveys URL: https://docs.customer.io/journeys/web-nps-survey/ Net promoter score surveys (NPS) help you measure how likely people are to recommend your product (or a part of your product) to others. With in-app messages, you can send a quick survey to your website visitors to gauge their satisfaction. While we’ve provided instructions below, here’s a quick video explaining how to set up an NPS survey and conditional logic in your workflow to handle different responses. How it works If you want to gauge how people feel about your product, you can send them an NPS survey in your website to measure their satisfaction. Here, we’ll help you set up a web in-app message to survey your customers. In your campaign, we’ll help you group responses so you can follow-up with people depending on the rating they give you—detractors (0-6), passives (7-8), and promoters (9-10). Remember, while your campaign will send an NPS survey, users won’t see the survey until they: Go to a page on your website where you set your message to appear. Are identifiedThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously.. This is often automatic, but people might not see your message immediately if they’ve recently cleared their cookies and cache or visit your website in a private browsing window. 1. Setup your campaign Before you set up your Campaign Trigger, have a group of users in mind that you want to survey. Common NPS scenarios include: A segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. of users who’ve gotten to know your product—for example, people who’ve been users for at least 90 days. Users who’ve completed an event indicating that they use a particular feature you want feedback on. On the Campaigns page, click Create Campaign. Click Choose trigger on the canvas. For this example, we’ll trigger based on a Segment change. Choose your segment(s). Set your Campaign Frequency depending on how frequently you want to poll your audience. Make sure you don’t poll them too frequently! People might like your product, but they might not like being surveyed all the time. In this example, people can re-enter after a fixed interval of 26 weeks. Click your campaign’s name to find Goal. We’ll set ours to No Goal, since we want to gather feedback, not get people to join a segment or perform a specific event (outside of answering your survey). This isn’t to say that you don’t have a goal for your NPS survey! It simply isn’t the kind of goal that you can track with our goal settings. 2. Set up your message Drag an in-app message into your workflow. Select it and give it a name like Web NPS. Click Add Content to set up your survey. We have a template for NPS surveys that you can use, or you can create your own. Customize your survey’s placement and display settings: Change the placement of the survey on the page using the Position settings. In general, we recommend that you set your survey as an Overlay and set a Message Max-Width! This makes sure your survey doesn’t interrupt your user’s experience and limits your message to websites! Set an expiration period for the survey. When you send a message, it waits for people to visit the page(s) on your website. If they don’t visit the right page(s) before the message expires, they won’t see your survey. Set Page rules determining the pages of your site where the survey will appear. Customize your survey’s appearance: Customize the heading text of the survey. Change the colors to match your website’s theme. Select elements in your message to change their display settings. Select the buttons in the survey. Where the Tracked Name has a number, 0-10, add the corresponding NPS value: detractor for scores 0-6, passive for 7-8, and promoter for 9-10. This is how we’ll group responses later! Select the various parts of your message and set up your colors, fonts, and so on. When you’re done, click Save and then click Done.  Send yourself a test! Click Send Test in the upper right corner of the screen to send yourself a test message. This will help you see how your survey looks and behaves on your website. You can use tests to adjust your survey’s appearance and behavior before you send it to your users. Finally, select the message again and click Settings. Change the sending behavior to Send Automatically. 3. Handle different kinds of responses If people are detractors, you might want to alert your customer success or tech support teams to follow up with the user. They might need help with your product! If people are promoters, you might want to ask them for a testimonial or a review. To handle these different kinds of responses, we’ll add a Wait until action to our workflow. In the resulting branches, we can add actions to notify our team or send messages to to the user. Drag a Wait until block to the canvas, under your in-app message. Click Conditions and add your conditions. You’ll repeat the process below for detractor, passive, and promoter responses. Under Add conditions, select Message. Select In-app message. In the next box, select your NPS survey. Remember, we called our message Web NPS. Set the remaining boxes to: is clicked on response containing. In the final box, type detractor, passive, or promoter. Repeat this process for each response type. Click Max. Time and set the maximum time that you want to wait for responses. This should equal the expiration period for your in-app message. You only want to wait until the message expires! Click Save. In your workflow, add actions for the different kinds of responses. You might: Send slack notifications to your team about detractors or promoters. Send a follow-up email thanking people for their responses. Use the Create or Update Person action to store people’s responses as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you’re done, click Start Campaign to go live! 4. Calculate scores As people respond to your survey, you’ll see their responses in the Metrics tab of your campaign. You’ll need to add up your detractors (0-6) and promoters (9-10) and calculate the percentage of each group. Then, subtract the percentage of detractors from the percentage of promoters to get your Net Promoter Score. (NPS promoters / Total responses) x 100 − (NPS detractors / Total responses) x 100 = Net Promoter Score Before you calculate your score, make sure you’ve achieved statistical significance! You’ll want to make sure you have a large enough sample size to make your score meaningful. For example, if you expect 1000 people to see your campaign but only have 10 responses, you probably don’t have enough data to make a meaningful calculation. Advanced: Send data to your reporting tool to gather results You can send message events to your analytics platform using outgoing integrations or reporting webhooks to send your survey responses to your reporting tool of choice. This way, you can analyze your survey results alongside other data you collect about your users. For example, you might send your survey responses to Mixpanel where you can group and analyze NPS scores over time. Go to Integrations. Select your reporting tool, like Mixpanel or Amplitude. Pick the Advanced version of the integration. Set up your integration. You’ll need to give us credentials to send data to your reporting tool. We’ve provided an example for Mixpanel below. In the Configure data step, you’ll see that we already capture message events from your workspace. But you can also select other data sources for your analytics platform. Click Save integration and you’re all set! Now you can send data into Customer.io and downstream to your reporting tool! You can even set up Actions to handle responses to NPS messages in your reporting tool differently—for example, if you want to send them with a specific event name that helps you categorize them --- ## Promotions and offers URL: https://docs.customer.io/journeys/web-promotion/ During certain times of the year, like Black Friday or Cyber Monday, you might want to display a banner or modal message on your website that highlights a promotion! With in-app messages, you can announce your promotion and drive sales, increase subscriptions, and so on. Read on for instructions on how to build messages like this welcome offer. How it works In this recipe, we’ll broadcastA single message sent to a group of people. You can choose to create an A/B test too. an in-app message telling people that we’re running a promotion. Our message will persist until a user dismisses it or the promotion ends. We’ll also set up our message so that it only appears on relevant pages and in the right spot. For example, we don’t want our promotion to block an Add to Cart button, and we might not want it to appear on the checkout page if a promotion is already applied. flowchart LR a{Is the website visitor identified?} a-->|yes|b{Are they on the right page?} b-->|yes|c{Have they already closed the message?} c-->|no|d(Display promotion) a-...->|no|e(Do not display promotion) b-.->|no|e c-.->|yes|e Example promotions you can copy We’ve set up a few examples that you can use for inspiration or even use as templates. If you to start your message from one of these examples, just click the code icon in the editor and paste the example code below into the editor! Banner Example Banner Example <x-base> <x-message background="#111827"> <x-heading-2 margin="0px" :font-size="36" font-family="'Readex Pro', sans-serif" color="#ffffff">Black Friday <span style="color: #facc15">Linguist</span> Bonanza!</x-heading-2> <x-row :gap="8" :layout='[50,25,"auto"]'> <x-column> <x-paragraph margin="0px" :font-size="18" font-weight="700" color="#ffffff">Unlock your language potential with <span style="color: #facc15">20% OFF</span></x-paragraph> <x-paragraph margin="0px" :font-size="18" font-weight="700" color="#ffffff">Valid Nov 28 - Dec 2 | First 500 customers only</x-paragraph> </x-column> <x-column> <x-heading-2 text-align="right" margin="0px" :font-size="29" font-family="'Roboto', sans-serif" color="#ffffff">20% OFF</x-heading-2> <x-paragraph text-align="right" margin="0px" :font-size="18" font-weight="700" color="#ffffff">for individuals &amp; teams</x-paragraph> </x-column> <x-column> <x-cta margin="0px" padding="12px 32px" border-radius="8px 8px 8px 8px" hover-background="#eab308" font-family="'Roboto', sans-serif" color="#111827" background="#facc15">Claim Offer</x-cta> </x-column> </x-row> </x-message> </x-base> Upgrade Offer Upgrade Offer <x-base> <x-message width="auto" box-shadow="0 2px 4px 0 rgba(0, 0, 0, 0.1)" margin="8px" padding="24px 24px" border-radius="8px" background="#f1ebff" font-family="Helvetica" :line-height="1.5" color="#666666" > <x-heading-1 font-family="'Alata', sans-serif" text-align="center" margin="0px" :font-size="36" :line-height="1.25" color="#0d1216" >Black Friday Special</x-heading-1 > <x-paragraph color="#0d1216" :font-size="21" text-align="center">Get our Plus plan at Starter plan price for a full year!</x-paragraph ><x-box border-radius="8px 8px 8px 8px" background="rgba(255, 255, 255, 0.8)" padding="0px 24px 0px 0px" margin="0px 0px 16px"><x-paragraph margin="16px 0px 0px 24px" color="#0d1216">Unlock premium features for your design projects:</x-paragraph ><x-list color="#0d1216"><li>Unlimited design projects</li><li>Collaborate with your entire team</li><li>Access to premium templates and assets</li><li>Advanced design tools for both graphic and product design</li></x-list></x-box> <x-cta background="linear-gradient(90deg,#00c4cc,#7d2ae8) #000000" align="center" text-transform="none" font-weight="700" font-family="Helvetica" :full-width="true" border-radius="8px" padding="18px 0px" hover-background-color="#0046a0" background-color="#0057c4" behavior="dismiss" >Upgrade to Plus</x-cta ><x-paragraph margin="14px 0px 0px" :font-size="14" color="#0d1216" text-align="center">Offer valid from Nov 28 through Dec 2 </x-paragraph> </x-message> </x-base> Fintech Coupon Fintech Coupon <x-base> <x-message width="auto" box-shadow="0 2px 4px 0 rgba(0, 0, 0, 0.1)" margin="8px" padding="16px 16px 32px" border-radius="8px" background="#f0f0f2" font-family="Helvetica" :line-height="1.5" color="#666666" ><x-image align="right" behavior="dismiss" width="26px" src="https://storage.googleapis.com/cio-in-app-templates/assets/close-padding.603304e2.png" /> <x-heading-1 text-align="center" font-family="Arial, sans-serif" margin="0px" :font-size="17" :line-height="1.25" color="#230b59" >BLACK FRIDAY EXCLUSIVE</x-heading-1 ><x-paragraph margin="0px" text-align="center" color="#4840bb" font-weight="700" :font-size="40" font-family="Arial, sans-serif">$20 BONUS</x-paragraph> <x-paragraph text-align="center" color="#230b59">Deposit $200 or more into your account and receive an instant $20 bonus!</x-paragraph ><x-paragraph font-weight="700" :font-size="18" text-align="center" :line-height="1.5" color="#230b59">⏱︎ Offer ends in: <span id="countdown"></span></x-paragraph ><x-paragraph margin="8px 0px 16px" text-align="center" :font-size="14" color="#230b59">Valid only on November 29, 2023</x-paragraph > <x-cta hover-background="#2b23a2" background="#4840bb" align="center" text-transform="none" font-weight="700" font-family="Helvetica" :full-width="true" border-radius="8px" padding="18px 0px" hover-background-color="#0046a0" background-color="#0057c4" behavior="dismiss" >Claim your bonus</x-cta > <x-watermark /> </x-message> <script> // Set the date we're counting down to var countDownDate = new Date("November 29, 2024 23:59:59").getTime(); // Update the count down every 1 second var x = setInterval(function() { // Get today's date and time var now = new Date().getTime(); // Find the distance between now and the count down date var distance = countDownDate - now; // Time calculations for days, hours, minutes and seconds var days = Math.floor(distance / (1000 * 60 * 60 * 24)); var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); var seconds = Math.floor((distance % (1000 * 60)) / 1000); // Display the result in the element with id="demo" document.getElementById("countdown").innerHTML = days + "d " + hours + "h " + minutes + "m " + seconds + "s "; // If the count down is finished, write some text if (distance < 0) { clearInterval(x); document.getElementById("countdown").innerHTML = "EXPIRED"; } }, 1000); </script> </x-base> Travel Promo Travel Promo <x-base> <x-message width="auto" box-shadow="0 2px 4px 0 rgba(0, 0, 0, 0.1)" margin="0px" padding="0px" border-radius="8px" background="#f0f0f2" font-family="Helvetica" :line-height="1.5" color="#666666" ><x-box padding="16px" background="url('https://userimg-assets.customeriomail.com/images/client-env-122460/1729037025116_iceland-beach_01JA9A6G19AMMF7QDRV30T8WYK.jpg') center center / cover no-repeat"><x-image align="right" behavior="dismiss" width="26px" src="https://storage.googleapis.com/cio-in-app-templates/assets/close-padding.603304e2.png" /><x-heading-1 text-align="left" font-family="Arial, sans-serif" margin="0px" :font-size="17" :line-height="1.25" color="#ffffff" >BLACK FRIDAY EXCLUSIVE</x-heading-1 ><x-paragraph margin="0px" text-align="left" color="#ffffff" font-weight="700" :font-size="48" font-family="'Cabin', sans-serif">40% Off</x-paragraph><x-paragraph font-weight="400" font-family="'Cabin', sans-serif" margin="0px" :font-size="20" text-align="left" color="#ffffff">Treat yourself to an all-inclusive experience.</x-paragraph ><x-paragraph font-family="'Cabin', sans-serif" margin="0px 0px 270px" text-align="left" :font-size="14" color="#ffffff">Offer available Nov 29 - Dec 2.</x-paragraph ><x-cta hover-background="#2b23a2" background="linear-gradient(rgb(38, 104, 147) 0%, rgb(40, 39, 140) 100%) #4840bb" align="center" text-transform="none" font-weight="700" font-family="Helvetica" :full-width="true" border-radius="50px 50px 50px 50px" padding="18px 0px" hover-background-color="#0046a0" background-color="#0057c4" behavior="dismiss" >Book Now</x-cta ></x-box> </x-message> </x-base> Welcome Offer Welcome Offer <x-base> <x-message box-shadow="2px 2px 10px 0 rgba(0, 0, 0, 0.2)" border-radius="16px 16px 16px 16px" padding="0px" background="#ffffff"> <x-row :gap="0" :layout='[50, 50]'> <x-column background="url('https://userimg-assets.customeriomail.com/images/client-env-122460/1729186391320_person_1_01JADRMSF0AE6T7EN1D03KNQ99.jpg') center center / cover"> <x-spacer></x-spacer> </x-column> <x-column padding="0px 16px"> <x-image margin="8px 0px 0px" align="right" behavior="dismiss" width="16px" src="https://userimg-assets.customeriomail.com/images/client-env-138234/1715273992530_Close-Button-Forest_01HXF4R2DGQ70XVN3G37A71RVR.png" /> <x-heading-2 font-family="'Rethink Sans', sans-serif" font-weight="700" color="#0b353b" :line-height="1.2">Claim your welcome offer</x-heading-2> <x-paragraph :line-height="1.2" font-weight="400" font-family="'Rethink Sans', sans-serif">Get 20% off on your first month</x-paragraph> <x-cta margin="8px 0px 32px" font-weight="700" font-family="'Rethink Sans', sans-serif" align="left" text-align="center" :font-size="13" color="#e4ffce" background="#0b353b">Unlock my discount</x-cta> </x-column> </x-row> </x-message> </x-base> Before you begin This recipe assumes you’ve already set up your website to identify people and receive in-app messages. If you haven’t done that, you’ll need to: Set up your website with our JavaScript source integration. If you haven’t done this yet, we have a helpful video to get you started! IdentifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. your web visitors. People can’t see your message until you know who they are! If you’ve already set up our JavaScript integration, identification is often automatic—based on a cookie. But people might not see your message immediately if they’ve recently cleared their cookies and cache or visit your website in a private browsing window. 1. Create your in-app broadcast Go to Broadcasts and click Create Broadcast. Give your broadcast a name. We’re calling ours Promotional Broadcast. Select Everyone in workspace as your audience and click Next. We want to send our promotion to everyone!  Is your promotion limited to a particular product or region? For this recipe, we’re sending our promotion to everyone. But, if your promotion is limited to a particular region, product, or other criteria, you can set up a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. to target your promotion more effectively. Set a Goal that helps you track engagement with your promotion. To do this, you’ll need to have a way to track people that redeem your promotion. If the promotion isn’t something you can track—like a coupon code people use or a specific URL people visit—then you might not be able to set a goal in Customer.io. See Advanced: Use an event as a goal for more information. In the Content step, pick In-App and click your message. Now we’re ready to set up our in-app message. 2. Set up your in-app message In the content step, you can start from our Promotion template, or you can start from scratch. If you’re new, we recommend starting from the template. Our example is a short promotion letting people know that we have a sale on all products. Click the bar above the message that shows Priority, Display, and Expiration. Under Expiration, select Persist message until it expires or is dismissed. This will keep your message on the page until someone dismisses it or the promotion ends. Set the expiration period for the message. Our promotion runs from the last Friday in November through Sunday December 1st 2024 in the eastern time zone (EST), so we set our message to expire on Monday at midnight (12 AM). Set Page rules to determine where your message will appear. We want our promotion to appear on our home page and our product pages, but not our /checkout pages, so we set page rules where the page equals /, contains /product/, or /category/. Set Display settings. By default, our promotional template is a modal message that appears in the center of the screen. Because our message is persistent, we probably don’t want to interrupt the user flow or cover up important information on the page! So, we’re changing our message to an Overlay that appears in the bottom-right of the page. Now we’re ready to work on our message content! Add a Close Button to the top-right of your message. Add a Heading to your message. If you have a call to action, add a Button to your message. If you’re promoting a specific product or category, you might use your button to link people to a page where they can use your promotion. Set an Action for your button. We’re going to set our button to link to our /black-friday page. Set a Tracked Name for your button. This is the value we record when someone clicks your button. We’re going to set our tracked name to black-friday-promotion. (Optional) Add an image to your promotion if you have a specific product to highlight or a visual element that you want to include.  Make your image clickable! Your in-app image can act like a button. Just add a Behavior and a Tracked Name, so people can interact with your image. You might give it the same action and tracked name as a button so people can click in a much larger area in your message. When you’re done, click Send Test and send yourself a test message! Open your website and make sure your message looks good on your website and that your buttons and links work as expected. Click Save and then click Next in the upper-right to go to the Review step of your broadcast. 3. Review and schedule your broadcast Remember, we want to send our message over the 2024 Black Friday weekend in the US, but we don’t want to spend our holidays writing a broadcast! So, we’re writing our broadcast ahead of time and we’re going to schedule it to send when the promotion starts. Click Schedule at the bottom of the screen to set up your message’s delivery. Set the Date when you want to send your message. In our case, we’re scheduling for Friday, November 29th 2024. We’re going to schedule our message to go live at midnight in the US Eastern time zone, so we’re setting our Time to 12:00 AM and the Time Zone to Eastern Time. Click Schedule to set up your message to send on the day of your promotion. Now you’re all done! You can check the status of your promotion in the Broadcasts page. You can make changes to your broadcast if you need to, right up until it sends. --- ## Promote upcoming event URL: https://docs.customer.io/journeys/in-app-event-promotion/ Add a banner or modal message to your website that highlights upcoming events, like webinars, to increase registrations and engagement wih your product. How it works This recipe expands on our recipe Create an email campaign to announce registration for events! It uses a feature of ours called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., which are non-people entities you can relate to people. In this case, objects are events that people want to register for. Below you’ll learn how to set up an in-app message to promote an upcoming event on your website. Before you begin This recipe assumes you’ve already set up your website to identify people and receive in-app messages. If you haven’t done that, you’ll need to: Set up your website with our JavaScript source integration. We have a helpful video to get you started! IdentifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. your web visitors. People can’t see your message until you know who they are! If you’ve already set up our JavaScript integration, identification is often automatic—based on a cookie. But people might not see your message immediately if they’ve recently cleared their cookies and cache or visit your website in a private browsing window. 1. Create your in-app message Go to your campaign. Drag an In-App Message into your workflow. Click the message then Add Content. Choose a template to start from. You can start from our Banner template, a previous message, or you can start from scratch. In this example, we’ll click Drag-and-drop editor then Templates > Start from scratch. We’re using this code, which you can copy/paste into your own message! <x-base> <x-message width="auto" background="#0057c4" :line-height="1.4" color="#FFFFFF" font-family="Helvetica" :font-size="16" padding="16px" > <x-row :gap="8" :layout='[8,"auto",30]'> <x-column padding="0px"> <x-image align="left" width="auto" src="https://userimg-assets.customeriomail.com/images/client-env-122460/1730245378130_sketcherio_logo_01JBDAJG922ACE0MPQG37Q8WVJ.png" /></x-column> <x-column vertical-align="middle" padding="0px"> <x-paragraph margin="8px 0px" :font-size="16">Register for <strong>Using Sketcher for Client Feedback</strong>&nbsp;live on Nov 16 to discover how to efficiently invite clients for reviews and manage feedback.&nbsp;&nbsp;</x-paragraph> </x-column> <x-column> <x-row :gap="8" :layout='[50, 50]'> <x-column> <x-cta width="100%" border-color="#ffffff" border-style="solid" :new-tab="true" tracked-response-name="register" behavior="openUrl" font-weight="700" :font-size="14" color="#0057c4" background="#ffffff">Register</x-cta> </x-column> <x-column> <x-cta width="100%" padding="10px 16px" behavior="dismiss" border-style="solid" border-color="#ffffff" font-weight="700" :font-size="14" color="#ffffff" background="">Close</x-cta> </x-column> </x-row> </x-column> </x-row> </x-message> </x-base> 2. Configure settings Click the bar above the message that shows Priority, Display, and Expiration. Under Expiration, select Persist message until it expires or is dismissed. This will keep your message on the page until someone dismisses it or the promotion ends. Set the expiration period for the message. Our event occurs on November 16, so we set our message to expire on Nov 16 when the event starts. Set Page rules to determine where your message will appear. We want our promotion to appear on our home page and our product pages, but not our /checkout pages, so we set page rules where the page equals /, contains /product/, or /category/. Set Display settings. Because our message is persistent, we probably don’t want to interrupt the user flow or cover up important information on the page! So, we’re changing our message to an Overlay that appears in the top center of the page. 3. Modify your content The following instructions help you modify the template above to match your brand. If you’re building an in-app message from scratch, check out these instructions. Click the Message breadcrumb below the preview and change the background content fill to match your brand color. Click the Image on the preview and replace it with a file you uploaded to your workspace or provide an external link. Click the Paragraph on the preview and modify the text to fit your event details. Click the Register button and add your registration Link and modify the styles as you see fit. Click the Close button and modify the styles as you see fit. Always include a button that lets people dismiss your message. 4. Test and start your campaign In your in-app message editor, click Send Test to check it out! Open your website and make sure your message looks good and that your buttons and links work as expected. Click Save Changes then Done. Click the message again and change the in-app from Queue Draft to Send automatically. When you’re ready, review your campaign then click Start Campaign to go live!  If you have multiple events to promote, you can add time delays between multiple in-app messages, too! --- ## Milestones and achievements URL: https://docs.customer.io/journeys/web-achievements/ Add a banner or modal message on your website that highlights customers' milestones to encourage them to purchase. Consider what milestone or achievement you’d like to celebrate. In this recipe, we’ll create a campaign to notify customers of their one year anniversary using your product. How it works In this recipe, we’ll trigger a campaign based on the date a person started using your product. When they reach their one year anniversary, we’ll send them an in-app message to congratulate them and offer a discount code for their loyalty. Our message will persist until a user dismisses it. Before you begin This recipe assumes you’ve already set up your website to identify people and receive in-app messages. If you haven’t done that, you’ll need to: Set up your website with our JavaScript source integration. We have a helpful video to get you started! IdentifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. your web visitors. People can’t see your message until you know who they are! If you’ve already set up our JavaScript integration, identification is often automatic—based on a cookie. But people might not see your message immediately if they’ve recently cleared their cookies and cache or visit your website in a private browsing window. 1. Create a date-triggered campaign Go to Campaigns and click Create Campaign. Enter a name, like “One Year Subscribers.” Add a description so other team members can understand the campaign at a glance. Click Choose trigger then select Important date. Choose a person’s attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that reflects the date they started using your product. For this recipe, it’s signed_up. Click After the date and specify 364 days. Change the time of day they should enter this campaign. You can set a specific time zone or set a user’s time zone. Set the frequency to once so people enter the campaign once, a year after they signed up for your product. Click the campaign’s name then go to Messages. Send your message to people with the appropriate subscription preference. Click Goal. Consider what should cause people to convert. In this example, we’ll say people convert when they use the discount code they can claim from the message. Let’s assume we’re sending the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. purchase-with-one-year-discount, so we’ll count a conversion when someone performs this event within 1 week of being sent any delivery from this campaign. Click Exit. If you set a goal, you might have people exit the campaign early if they match the conversion criteria. Next, we’ll set up our in-app message. 2. Create an in-app message Drag an In-App Message into your Workflow. Click it then Add Content. You can create an in-app message from scratch, from a template, or from an in-app you’ve already made. In this example, we’ll click Drag-and-drop editor then Templates > Start from scratch. We’re using this code, which you can copy/paste into your own message! It celebrates people’s one year anniversary and links them to claim a discount code. <x-base> <x-message width="auto" box-shadow="0 2px 4px 0 rgba(0, 0, 0, 0.1)" margin="0px" padding="0px" border-radius="8px" background="#f0f0f2" font-family="Helvetica" :line-height="1.5" color="#666666" ><x-box margin="0px" padding="16px 24px 16px 200px" background="url('https://userimg-assets.customeriomail.com/images/client-env-122460/1730248249031_cupcake_01JBDDA3VM69VJTQ1ZT7Y50P3A.png') left center / cover no-repeat"><x-image align="right" behavior="dismiss" width="26px" src="https://storage.googleapis.com/cio-in-app-templates/assets/close-padding.603304e2.png" /><x-heading-1 text-align="left" font-family="Arial, sans-serif" margin="0px 0px 8px" :font-size="28" :line-height="1.2" color="#ffffff" >Happy Anniversary!</x-heading-1 ><x-paragraph :line-height="1.3" font-weight="400" font-family="'Cabin', sans-serif" margin="0px 0px 160px" :font-size="17" text-align="left" color="#ffffff">It's been one year since you first joined.&nbsp; See below for a special reward to celebrate this today!</x-paragraph ></x-box><x-box align="left" padding="0px 16px" background="#ff7d00"><x-cta font-weight="700" padding="10px 16px" :new-tab="false" tracked-response-name="redeem" behavior="openUrl" align="left" background="">Surprise! For the next 7 days, get 50% off a premium subscription. <u>Claim this gift</u> →</x-cta></x-box> </x-message> </x-base> 3. Modify settings In the in-app message editor, click the bar above the message that shows Priority, Display, and Expiration. Under Expiration, set the expiration period for the message. We’ll say a relative date of 30 days after being sent. Check Persist message until it expires or is dismissed. This will keep your message on the page until someone dismisses it. Set Page rules to determine where your message will appear. We want our promotion to appear everywhere until it’s dismissed, including checkout, so these subscribers know they can use a discount code. We’ll set a single page rule Web contains *, where the asterisk respresents all pages on your website. Set the Priority. Consider whether this should appear before other in-app messages for a customer. We’ll set this to Medium, seeing it as higher priority than say, a reminder to register for an event, but lower than a message encouraging them to upgrade. Set Display settings. Because our message is persistent, we probably don’t want to interrupt the user flow or cover up important information on the page! So, we’re changing our message to an Overlay that appears in the top-right of the page. 4. Modify the content The following instructions help you modify the template above to match your brand: Click the Close button and modify the styles as you see fit. Always include a button that lets people dismiss your message. Click the Image of the cupcake on the preview and replace it with a file you uploaded to your workspace or provide an external link. Click the Heading on the preview to change the text to fit what you’re celebrating. Click the Paragraph on the preview to modify the text. Click the orange Box on the preview to change the color to match your brand. Click the Button at the bottom of the preview to add your redemption Link and modify the styles as you see fit. If you started from scratch using our drag-and-drop editor, here’s how you can build your message: Drag a Close Button from the left components menu to the top-right of your message. Add a Heading to your message. Add a Button to your message for people to claim their discount. Set the Action Behavior for the button. Set a Tracked Name for your button. This is the value we record when someone clicks your button. (Optional) Add an Image to your promotion if you have a specific product to highlight or a visual element that you want to include.  Make your image clickable! Your in-app image can act like a button. Just add a Behavior and a Tracked Name, so people can interact with your image. You might give it the same action and tracked name as a button so people can click a much larger area of your message. 5. Review and start your campaign In the in-app message editor, click Send Test to check it out! Open your website and make sure your message looks good and that your buttons and links work as expected. Click Save Changes then Done. Click the message again then click Settings. Change the sending behavior from Queue Draft to Send automatically. When you’re ready, click Start Campaign to go live! --- ## Feature adoption URL: https://docs.customer.io/journeys/web-adoption/ Add a banner or modal message on your website to highlight a new feature! With in-app messages, you can announce features to drive adoption, increase subscriptions, and more. How it works In this recipe, we’ll trigger a campaign based on a segment of users who use your product regularly. We’ll send them an in-app message about the new feature and how to get started. We’ll also set up our message so that it only appears on relevant pages and in the right spot. For example, we don’t want our message to block an Add to Cart button. Before you begin This recipe assumes you’ve already set up your website to identify people and receive in-app messages. If you haven’t done that, you’ll need to: Set up your website with our JavaScript source integration. We have a helpful video to get you started! IdentifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. your web visitors. People can’t see your message until you know who they are! If you’ve already set up our JavaScript integration, identification is often automatic—based on a cookie. But people might not see your message immediately if they’ve recently cleared their cookies and cache or visit your website in a private browsing window. 1. Create a segment of active users Go to Segments and click Create Segment. Add a name like “Recent Users,” and click Create Data-driven Segment. Click the dropdown “Add condition or group.” Select Attribute from the dropdown. With this recipe, let’s assume we save a last_login on people’s profiles. Enter last_login as the attribute name. Then specify it’s a timestamp after a relative date of 30 days ago. This means anyone who logged in in the last 30 days will join this segment. Click Save Changes. Now you’re ready to create a campaign to drive feature adoption! 2. Create a segment-triggered campaign Go to Campaigns and click Create Campaign. Enter a name, like “Feature adoption: feature-name.” If helpful, add a description so other team members can understand the campaign at a glance. Click Choose trigger then select Segment change. Choose the segment you made to target active users. Set the frequency of entry. We want this campaign to reach out to people once. Click the campaign’s name then go to Messages. Send your message to people with the appropriate subscription preference. Click Goal. Consider what should cause people to convert. In this example, we’ll say people convert when they perform the eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. adopt-feature within 1 week of being sent any delivery from this campaign. Click Exit. If you set a goal, you might have people exit the campaign early if they match the conversion criteria. Now you’re ready to set up your in-app message! 3. Create an in-app message Drag an In-App Message into your Workflow. Click it then Add Content. You can create an in-app from scratch, a template, or from an in-app you’ve already made. Check out these samples to code a flip card, glowing button, and more! Here’s an example of an in-app you could make. It introduces a feature with a video and links people to enable it. <x-base> <x-message border-radius="8px 8px 8px 8px" padding="24px" background="#ffffff"> <x-row :gap="8" :layout='["auto",10]'> <x-column> <x-heading-2 :font-size="18" color="#0b353b" margin="0px 0px 16px" text-align="left">New! Dive right into building your workflow ✨<br></x-heading-2> </x-column> <x-column> <x-image margin="0px" align="right" behavior="dismiss" width="16px" src="https://userimg-assets.customeriomail.com/images/client-env-138234/1715273992530_Close-Button-Forest_01HXF4R2DGQ70XVN3G37A71RVR.png" /> </x-column> </x-row> <x-box> <x-paragraph color="#3f4e50" margin="0px 0px 16px" text-align="left">We've simplified the way you create campaigns.&nbsp; You'll now be able to add your trigger, goal, exit and build your workflow in one place.&nbsp; <strong>Opt-in to start using the new builder.</strong></x-paragraph> </x-box> <x-video src="https://customer-1.wistia.com/medias/hsyc6s5jtm" /> <x-cta action="enable-builder" tracked-response-name="opt-in" behavior="performAction" border-style="solid" border-color="#e4ffce" margin="16px 0px" font-weight="700" font-family="'Rethink Sans', sans-serif" align="center" text-align="center" :font-size="14" color="#e4ffce" background="#0b353b">Enable the new builder</x-cta> </x-message> </x-base> 4. Configure settings In the in-app message editor, click the bar above the message that shows Priority, Display, and Expiration. Under Expiration, set the expiration period for the message. Let’s say we want this to appear for 7 days after being sent. Set Page rules to determine where your message will appear. We want our message to appear when people log in and when people visit settings so we’ll target web pages that contain /dashboard or /settings. Set a Priority in case a recipient is sent multiple in-app messages. We’ll set ours to Medium as it should appear before some messages but after others. Set Display settings - a modal message that appears in the center of the screen. Finally, let’s set our Message Max-width. We’ll set it to 600px so the video in the message can be more prominent. 5. Modify content The following instructions help you modify the template above to match your brand: Click the Close button and modify the styles as you see fit. Always include a button that lets people dismiss your message. Click the Heading on the preview to change the text to match your feature. Click the Paragraph on the preview to modify the description. Click the Video and replace with your own uploaded file or provide an external link. Click the Button: Enable the new builder. In the image above, the button is set to perform a custom action. This requires developer resources, but would enable the feature for the recipient if they clicked it! You can also change the behavior to something that does not require developer resources, like opening a URL. 6. Test and start your campaign In the in-app message editor, click Send Test to check it out! Open your website and make sure your message looks good and that your buttons and links work as expected. Click Save Changes then Done. Click the message again then click Settings. Change the sending behavior from Queue Draft to Send automatically. When you’re ready, click Start Campaign to go live! --- ## The visual editor URL: https://docs.customer.io/journeys/new-in-app-editor/ Draft in-app messages in your campaign or broadcast workflow. Our in-app editor it easy to build, similar to the way you build emails in Customer.io. And, like our email editor, you can even switch to a full HTML-code view to edit tags and classes directly.  We’re actively developing this feature! This feature is new, and we’re continuing to improve on it. If you run into a problem or have feedback for us, please get in touch so that we can build the best possible feature for you! How it works Our in-app editor lets you drag-and-drop components like text, media, and buttons to build in-app-messages. You can style each component so that your message looks and feels like a part of your app or website. If you’re HTML-savvy, you can even switch to a full HTML-code view to edit your message’s HTML code directly. The old editor experience If you enabled in-app messaging with Customer.io before July 8, 2024, you’ll see two editor experiences—Drag-and-drop (new) and Standard (legacy). You’ll want to use the Drag-and-drop editor to develop new messages. For messages you already created before July, 8 2024, you can use the Legacy editor to pick and populate your message. In many cases, you’ll want to rebuild your messages in the drag-and-drop editor to take advantage of the new features and improvements we’ve made. Building your in-app message Messages are already set up so that you can start adding components right away—headings, text, buttons, and so on. In general, most messages should contain a heading, text, and at least one button—a call to action that you want your audience to perform or a clear way to dismiss your message. Text and headings are fairly self-explanatory, but buttons need an Action. An action determines what happens when someone interacts with a button or a part of your message. Some components, like the Close Button already have an action set up for you. See buttons and actions below for more information. You can drag combinations of 2, 5, or 11 buttons into your layout, but you’re not limited to these numbers of buttons. You can click the row to customize the number of buttons and the width of each button. Beyond that, you can even use the row and column components to customize the layout of your message. Styling your message Click on any component in your message to see and change its settings on the right side of the editor. The settings for each component are common CSS properties like font-size, color, background-color, and so on. You can switch to the HTML-view of your message using the code button if you’re comfortable working directly in HTML. The first thing you’ll notice is that our HTML looks a little different from what you’re used to. We use extended HTML components to support our drag-and-drop editor. For example, our Heading 1 component is called x-Heading-1, not h1. In the code view, you can apply CSS style and class attributes. Because your message is essentially an iframed HTML document, you can include a complete list of styles or import a style sheet and apply classes to your components if you don’t want to repeat styles across different components in your messages. You can also use standard HTML elements in your message (as opposed to our extended components). But, keep in mind that our drag-and-drop editor relies on these extended components. You won’t be able to style or drag standard HTML elements with the drag-and-drop editor. Edit multiple components at once You can select multiple components and edit their shared styles. This way you can apply identical settings like margins across components, rather than spending time individually modifying them. To make edits across multiple components, click and drag your cursor to select multiple components on the canvas. Then edit your settings in the Properties panel. After selecting multiple components, you can also move them around the canvas. Hover over your selection to find the drag handle . Then click and hold to move them around. Top-level message styles and settings You can set your message’s background color, font settings, border color, and other settings by selecting the message frame itself. Message styles cascade from parent containers to child elements. So, for example, if you set a font color on the message frame, all components in your message will inherit that font color unless you change the setting on a specific component. That’s because the message frame is the top-level container for your message.  Ignore the settings icon unless you start from scratch In the top-right corner, you’ll see a icon. While you can click this to reveal top-level settings like font colors, background color, and so on, our default templates override these settings. If you start with one of our templates, these settings won’t have any effect on your message! Advanced properties: custom CSS and styles While you can style all the aspects of your message itself, you may simply want to write your own stylesheet and apply CSS classes to components in your message—so you don’t have to manually adjust styles for each component. You’ll find CSS Class and CSS Style settings under Advanced when you select a component. Both work like standard HTML attributes. Where the contents of each field are the classes you want to apply. Remember that classes are separated by spaces and styles are separated by semicolons: CSS Classes: style1 style2 anotherStyle CSS Styles: font-weight: bold; font-family: Roboto; Advanced: using custom fonts You can use custom fonts in your in-app messages by including @font-face declarations in a style tag within the <x-head> element. You’ll need to reference fonts via URL; you can’t upload fonts to use directly in your message. Add a <x-head> element at the top of your message (before <x-base>) Include a <style> block with your @font-face declarations Create CSS classes in the <style> block that use your custom fonts Apply these classes to your message components. You can apply classes in code using the class attribute or in the CSS Class field in the component’s Advanced settings. Here’s an example that shows you how to set up and use custom fonts: <x-head> <style> /* Define your custom fonts */ @font-face { font-family: "MyFont"; src: url("https://font.example.com/MyFont.ttf") format("truetype"); font-style: normal; } @font-face { font-family: "MyFont-Bold"; src: url("https://font.example.com/MyFont-Bold.ttf") format("truetype"); font-style: normal; } /* Create CSS classes to use your fonts */ .heading { font-family: "MyFont-Bold", sans-serif !important; } .paragraph { font-family: "MyFont", sans-serif !important; } </style> </x-head> <x-base> <x-message> <x-heading-1 class="heading">Custom Font Heading</x-heading-1> <x-paragraph class="paragraph">This text uses a custom font family.</x-paragraph> <x-cta behavior="dismiss">Continue</x-cta> </x-message> </x-base> Buttons and actions An Action is something you want your message to do when your audience clicks or taps a component. Button components have default actions, because they’re obvious for people to interact with. Your buttons typically carry a call to action. But you can add actions to other components, like images. For each action, you can set two values: Behavior (Required) determines what the action does Tracked Name the Tracked Response that we record whens someone clicks or taps a button. This value appears as action_name in events sent to Customer.io. You’ll notice that we have a dismiss action and other actions. All actions dismiss your message. Other actions both dismiss your message and do something else; for example, the Open URL action both opens a web page and dismisses your message. Behavior What it does Dismiss Dismisses the message. This is the default action for the Close Button Open URL Opens a URL in a new tab. Open Deep Link Opens a deep link in your app. Perform Action Trigger a custom action—a function already set up in your app. Show step Shows a different step within the same message. See Multi-step messages for more information. Tracked Name and Responses When you set up an action, you’ll see a Tracked Name field. We use this value to aggregate responses to your message, so you can see how everybody responds to your message. It’s important that you provide a meaningful Tracked Name for each action so it’s easy to understand your metrics. This value also appears as an action_name in events associated with your message, and you can use this value to trigger follow-ups or other campaigns based on how people interact with your messages. For example, if you send a survey where people rate your app from 1 to 5, you might set the Tracked Name values from 1 to 5. If people rate your app as a 5 out of 5, you might thank them for the good review; if people rate your app as a 1 out of 5, you might ask them for feedback! Add steps to your message A single in-app message can have multiple steps! You might do this to introduce people to your app (like an onboarding flow) or to thank users after they fill out a survey, etc. Click Steps in the upper left corner. Click Add step to add a new step to your message. You can then use the Show step action to determine when to show each step—so people can progress through the different steps in your message. See Multi-step messages for more information. Rows and columns In most cases, your message is a single column—content flows top to bottom. But, when you drag buttons into your message, we create a row component with columns for each button. While we have options for 2, 5, and 11 buttons, you can add or remove columns from a row to set the number of buttons you want. You can also set the widths of columns to customize your layout. You can also delete individual buttons and replace them with other content if you simply want to use the row and columns to customize the layout of your message. To customize a row or columns inside it, drag a set of buttons into your message and click the space between or around buttons to select the row itself. You can then set the number of columns and the width of each column. Video The in-app editor supports video. We don’t host videos ourselves. You’ll provide the URL to a video hosted on YouTube, Vimeo, Wistia, Loom, and other video hosting services that provide an embeddable URL. If you use an unsupported platform, we’ll show a black preview and link to the video. We don’t have any constraints on video size or dimensions, but you’ll see the best results when you use a video that’s optimized for your audience’s medium. For example, if you send your message to mobile devices, you might want to use a smaller or shorter videos so it better fits your mobile audience’s needs and plays well whether they’re on cellular or WiFi connections.  We don’t support YouTube shorts You can’t link to a video with /shorts in the path. You’ll need to link to a standard, full-length video (that typically contains /watch in the URL). Advanced: trigger an event from your message If you use our code editor, you can write your message in HTML and include <script> tags in your message. If you use our JavaScript client, you can send events in Customer.io when people interact with your message (the analytics.track function in the example below). For example, you might add onClick handlers to elements in your message to trigger events when someone rates your app in a survey. You could then use this event to trigger a follow up campaign based on their rating. After you trigger an event, you’ll still need to track the interaction and dismiss the message. That’s what the message.performAction and message.dismiss functions do in the example below. These are regular actions you can perform on any message with the JavaScript SDK. You’ll find a complete list of message functions below. <script> // attach to button as `onClick="submitFeedback(event, this)"` function submitFeedback(event, el) { event.preventDefault(); let rating = el.getAttribute('aria-label'); const payload = { NPS_campaign: '{{message.name}}', NPS_rating: rating, NPS_method: 'inapp_cio', } //trigger event to workspace analytics.track('Completed NPS Survey', payload); //track click message.performAction('trackClick', {name: rating}); message.dismiss(); } </script> Advanced: set and call message actions with JavaScript You can use the following functions in your message’s JavaScript corresponding to button action behaviors listed above. All functions take an options object. This object just carries a name property (corresponding to the Tracked Name of the action) and, in the case of message.openUrl, the _target. message.dismiss({name: "action name"}): Dismisses the message. message.openUrl(url, {name: "page/screen name", target: '_blank'}): Opens a URL in a new tab. message.openDeeplink(url, {name: "page/screen name"}): Opens a deep link in your app. message.performAction('myCustomAction', {name: "action name"}): Triggers a custom action. message.isClientWeb(): Returns true if the message appears in a web browser. Lets you perform custom handling if you intend to send a message to both your website and mobile app. Advanced: accessibility settings When you change the settings for a component, you’ll see Advanced at the bottom right. This often includes accessibility settings, which help people who navigate your app with screen readers or other assistive technologies. You should keep these users in mind as you develop messages! Language sets the lang attribute for an element. This tells browsers and screen readers what language content is in. It can help screen-readers understand how to pronounce text and can help when a user needs to translate your message in their browser. It takes a two letter or four letter language code, like en or en-US. This does not affect how we send and deliver localized messages. Text Direction determines the direction of text in your message. This is important for languages that read right-to-left. Label sets a label attribute for your message or component. These describe fields or buttons in your message for folks using assistive technologies. For example, the label might let your users know that they’re reading an in-app message! Role sets the role attribute for your message or component. This tells assistive technologies what kind of element they’re looking at. For example, a role of button tells a screen reader that a component is a button. This is particularly important when an element doesn’t match its default role. For example, if a div acts like a button, you should set its role to button. Advanced: embed a form in your message While in-app messages support buttons that can represent responses from your customers, what if you want to capture more detailed feedback like text responses? You can embed a form in your message directly. Take Typeform for example: if you want to add a Typeform to your message, you can click to switch to the HTML view and paste your form inside the x-message element. This renders the form inside your message—when you send it; your form will not appear in the Customer.io preview. We’ve included a basic example below. You’ll need to make sure that your button contains a way to dismiss it—like a Close Button. But otherwise, it’s pretty easy to embed a form inside your in-app message! <x-base> <x-message> <x-heading-1>👋 Got a second {{customer.first_name}}?</x-heading-1> <!-- typeform goes here --> <div data-tf-live="41D6VFDDJ694EPE4XMZT652C0C"></div><script src="//embed.typeform.com/next/embed.js"></script> <x-cta behavior="dismiss">Dismiss</x-cta> </x-message> </x-base> Now, keep in mind that there are a few limitations with a form that you set up this way: You won’t be able to attach up a message.dismiss() event to your form in Customer.io. That means that your customers will have to close the message themselves after they submit their responses—unless you can handle this action with your form provider. The preview won’t show in the Customer.io editor. You should test your message in your app to make sure it looks and works as you expect it to. Form responses aren’t fed into CIO automatically like other actions in your messages. You’ll have to use your form provider to see responses. If you want to see form data in Customer.io, you can pass a webhook through your form provider or use a custom script to send the response to Customer.io. Advanced: dark mode We’re planning to add dark mode support in the future, so you don’t have to write code. But in the meantime, if you’re comfortable with HTML and CSS, you can add a dark mode palette to your message today using the prefers-color-scheme CSS media query. Below is an example message showing custom styles for people who use dark mode. We customize this message for dark mode by: Adding a container class to the x-message component and a button class to the x-cta component. Adding a style block and media query for dark mode to the top of the message, directly under the <x-base> tag: <style>@media (prefers-color-scheme: dark) { }</style>. Adding custom styles for the classes from step 1 in the media query. In dark mode, we set the background of the message to black (#000000) and the text color to white (#FFFFFF). We also reverse the colors for the button class so that the buttons stand out appropriately. You can test this out! Just copy and paste the message below into a test message in your workspace. As you switch between dark mode and light mode on your computer, you’ll see the message change to reflect your color scheme choice. <x-base> <style> @media (prefers-color-scheme: dark) { .container { background:#000000; color:#FFFFFF; } .button { background:#FFFFFF; color:#000000; } } </style> <x-message class="container" color="#000000" background="#ffffff"> <x-heading-2>Dark Mode Example</x-heading-2> <x-paragraph> This example uses CSS to adjust message colors for dark mode. </x-paragraph> <x-cta class="button" behavior="dismiss">Close message</x-cta> </x-message> </x-base> Advanced: auto-dismiss a message To automatically dismiss a message after a time delay, switch to the code view in your message and add the following script near the end of your message right before the </x-base> closing tag. The example below sets a 5 second time delay (5000 milliseconds) before calling the message action message.dismiss() to close the message. <script> setTimeout(() => {message.dismiss();}, 5000); </script> --- ## In-app component reference URL: https://docs.customer.io/journeys/in-app-components/ In-app messages use an HTML-like syntax with components similar to HTML elements beginning with `x-`. We've separated these components from standard HTML. How it works In-app messages use an HTML-like syntax with components similar to HTML elements beginning with x-. In HTML, you typically pass CSS styles to components in a style attribute. You can still do that with our in-app code editor, but each component also exposes a number of top-level attributes that makes it easy to set things like fonts, colors, and so on. These attributes correspond to common CSS properties, like font-size and color, so all the styles should make sense to you if you’re familiar with CSS. For example, in our paragraph component, you can set the font-size attribute to change the size of the text or you can set style="font-size...". They’re functionally the same! <!-- this --> <x-paragraph :font-size="14" color="red"> <!-- is the same as this --> <x-paragraph style="font-size:14px;color:red">  Format top-level attributes that take number or boolean values like :attribute-name See the component above: color="red" contains a string value, but :font-size="14" is an integer so it starts with a colon. Message composition While you can typically mix and match components in your message, there is a basic structure that most messages should follow. Messages need a base component, x-base, that sets the overall style of your message. Inside the base, you’ll typically have a x-message component that frames your message and sets the background color. If you want to write your message in HTML, we’ve provided a base structure below as well. You should use this structure to avoid unexpected interactions in your message (like zooming, scrolling, and so on). Components Components <x-base> <x-message> message content goes here </x-message> <x-watermark /> </x-base> <script> // any custom JavaScript goes here </script> Raw HTML Raw HTML <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> <title>Message Title</title> <style></style> </head> <body> message content goes here </body> </html> Components at a glance In-app messages use extended HTML components—described in detail on this page—to support our drag-and-drop editor. You can use standard HTML elements, but you won’t be able to drag them or manipulate them in our editor! Component Code Type Description Base <x-base> Layout Required: The base component for your message. The base contains an x-message component. Box <x-box> Layout A container for grouping and styling components Button (CTA) <x-cta> Button A call to action button that also closes your message. Row and columns <x-row> <x-column> Layout Creates a row containing up to 11 columns. Heading <x-heading-1> — <x-heading-6> Text Headings for your message, like h1 - h6 Horizontal Rule <x-hr> Layout A simple line to break up sections of a page HTML <x-html> Code Add custom HTML to your message. Image <x-image> Media An image in your message List <x-list> Text An ordered or unordered list Message <x-message> Layout Required: The frame of your message, containing all the other child components of your message. This component controls the background color and set padding between your message and where it appears in your app—including a box shadow. Paragraph <x-paragraph> Text A block of text Spacer <x-spacer> Layout Adds space between components, independent of padding/margins. Video <x-video> Media A video in your message Base The x-base component is the equivalent of a blank page. Apply any basic settings, like fonts and colors, for the message here. Your message will inherit all text styles set on the x-base component. This is the first component in your message, and you should only use it once, wrapping all other components inside it. Attribute Type title string lang string dir enum One of: ltr, rtl, auto background string color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number text-align enum One of: left, center, right class string style string Box Use the <x-box> component to group and style components that should, for instance, stand out from the rest of the content, like a footer or an offer. It’s similar to an HTML section. All components inside the x-box component will inherit its text styles. You can also use x-box to add semantic meaning to a group of components by using the role and label settings. Adding accessibility in this way is an advanced feature; only use this if you understand the impact as this can be negative when used incorrectly. Attribute Type background string width string height string padding string margin string border-radius string border-style enum One of: none, solid, dashed, dotted border-width string border-color string box-shadow string align enum One of: left, center, right opacity number color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number text-align enum One of: left, center, right lang string dir enum One of: ltr, rtl, auto label string role enum One of: article, region, navigation class string style string Buttons: call to action (x-cta) Buttons in in-app messages are called x-cta, short for call to action. When someone clicks or taps a button, it’ll perform a behavior: dismiss, openUrl, performAction. All behaviors dismiss the message, but the openUrl, and performAction behaviors expose extra fields determining what happens when someone uses the button. For example, the openUrl action includes an href or deep-link attribute for the link you want to open. If you don’t want to perform an action or track clicks on a button, you can add a close button to your message. Attribute Type background-color string behavior string, one of dismiss, openUrl, openDeepLink, performAction border-radius string border-style enum One of: none, solid, dashed, dotted border-width string border-color string box-shadow string width string height string padding string margin string align enum One of: left, center, right color string font-size number font-family string font-weight string One of: 300, 400, 700 text-align enum One of: left, center, right opacity number hover-color string hover-background-color string hover-opacity number hover-box-shadow string hover-border-radius string line-height number text-transform string One of: none, capitalize, uppercase, lowercase text-decoration string One of: none, underline, line-through class string style string Open a link or deep-link The openUrl behavior takes a person to a browser URL or a deep link. When you set behavior to openUrl, you’ll set an href or deep-link determining what URL to open and new-tab determining if the URL opens in a new tab or not. openURL doesn’t automatically dismiss a message. We’re working to add this option to the editor in the future. In the meantime, you can manually configure buttons to close the message after opening a link. Attribute Required Description deep-link if not href string, the deep link you want to open href if not deep-link string, the URL you want to open new-tab boolean, if true, the link opens in a new tab Browser link Browser link <x-cta behavior="openUrl" href="https://example.com/sfgiants" :new-tab="true" border-radius="8px" font-family="Helvetica" font-weight="700" :full-width="true" tracked-response-name="2" hover-background-color="#0046a0" background-color="#0057c4" padding="24px 0px" > CTA button </x-cta> Deep Link Deep Link <x-cta behavior="openUrl" deep-link="my-app://sfgiants" :new-tab="true" border-radius="8px" font-family="Helvetica" font-weight="700" :full-width="true" tracked-response-name="1" hover-background-color="#0046a0" background-color="#0057c4" padding="24px 0px" > CTA Button </x-cta> Perform a custom action A custom action is a behavior that you’ve programmed in your app. When someone interacts with your message, it’ll call this custom behavior. For example, if your call to action is to change a setting, you might set a custom action to enable the setting. However, to perform custom actions, you must have added a function using the action name and listen for that function name. Otherwise, the button will dismiss your message and nothing will happen. <x-cta behavior="performAction" action="myCustomAction" border-radius="8px" font-family="Helvetica" font-weight="700" :full-width="true" tracked-response-name="3" hover-background-color="#0046a0" background-color="#0057c4" padding="24px 0px" > CTA Custom Action Button </x-cta> Close Button The close button is actually an x-image component with a close icon. It’s used to dismiss an in-app message without taking any action. Most actions also automatically close your message, so you don’t need to include a close button in your message. The only button that doesn’t automatically close your message is the Custom Action. Rows and columns A row contains a group of column elements. It helps add structure to your layout—like cells in a table, columns in a grid, or components in a flexbox. The most common use case for rows and columns are buttons. When you drag a group of buttons into your layout, it’s actually an x-row component that contains a series of x-column components and a button in each column. Don’t use x-row to create a single column layout. If you just want to separate a single element or column from the rest of your layout, use x-box instead. It’s important that the number of columns you define in the layout property of your x-row matches the number of x-column components you add to your layout. For example, if you set your row’s column count to four, then your code needs to reflect your columns in the format :layout="[25,25,25,25]". In this case, each column is 25% of the row’s width. The default behavior of columns is to scale down to a narrower layout. If you want columns to stack on smaller viewports, you can set the break-point property to the screen size when you want columns to stack.  Use buttons to start a row of columns The x-row and x-column elements can be hard to set up manually. When you want to set up columns, you can drag a row of buttons into your layout. This’ll start you off with a row, columns, and all the properties you need for both—so you don’t have to start from scratch! Row attributes Attribute Type layout String containing array of numbers adding to 100, ex :layout='[50,50]' gap number width string padding string margin string align enum One of: left, center, right background string opacity number border-radius string border-style enum One of: none, solid, dashed, dotted border-width string border-color string box-shadow string break-point number fallback enum One of: single, multi color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number text-align enum One of: left, center, right class string style string Column attributes Attribute Type padding string background string opacity number border-radius string border-style enum One of: none, solid, dashed, dotted border-width string border-color string box-shadow string class string style string Headings There are six heading components, x-heading-1 to x-heading-6, representing the six levels of HTML headings h1 to h6. All headings support the same attributes; they simply have different default styles. Attribute Type color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number text-align enum One of: left, center, right text-transform string One of: none, capitalize, uppercase, lowercase text-decoration string One of: none, underline, line-through margin string lang string dir enum One of: ltr, rtl, auto class string style string HTML Use the x-html component to add custom HTML, like embedded forms, to your message. Horizontal rule Use the x-hr component to separate content with a horizontal rule. It creates a simple line that can visually break up sections of a page. Attribute Type background-color string height number width string align enum, One of: left, center, right margin string class string style string Image The x-image component adds an image to your message. When you choose a file, you can either select from assets you’ve already uploaded or upload an image. Attribute Type src string href string alt string width string margin string align enum One of: left, center, right border-radius string border-style enum One of: none, solid, dashed, dotted border-width string border-color string box-shadow string opacity number hover-opacity number hover-box-shadow string hover-border-radius string background-color string color string font-family string font-size number font-weight string One of: 300, 400, 700 letter-spacing number line-height number text-align enum One of: left, center, right text-transform string One of: none, capitalize, uppercase, lowercase text-decoration string One of: none, underline, line-through class string style string srcset string sizes string List The x-list component adds an ordered or unordered list to your message. It’s similar to an HTML ul or ol elements. Use the list-style to determine the list decorators (bullets, circles, roman numerals, etc). Attribute Type color string element enum One of: ul, ol (defaults to ul) font-family string font-size number font-weight string One of: 300, 400, 700 line-height number list-style-type enum One of: none, disc, circle, square, decimal, decimal-leading-zero, lower-roman, upper-roman, lower-alpha, upper-alpha text-align enum One of: left, center, right text-transform string One of: none, capitalize, uppercase, lowercase text-decoration string One of: none, underline, line-through class string style string Message The x-message component is the background of your message. Where the x-base contains items like your message’s fonts and other base styles, the message provides a background color and is where you’ll set padding between your message and where it appears in your app. It’s also common to apply a box-shadow to this component to make it appear like it’s floating above your app. Tooltip attributes When you set display-type to tooltip, the following attributes control how the tooltip is anchored and positioned. See tooltip messages for more information. Attribute Type Description display-tooltip-target string A CSS selector for the element the tooltip anchors to—e.g. #my-button or [data-tooltip="feature"]. display-tooltip-position enum One of: top, bottom, left, right The position of the tooltip relative to the target element. Defaults to top. General attributes Attribute Type background string border-color string border-radius string border-style string border-width string box-shadow string color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number margin string padding string text-align enum One of: left, center, right width string lang string dir enum One of: ltr, rtl, auto class string style string Paragraph The x-paragraph is basically a <p> tag in HTML in that’s it’s a block-level component that contains text. Attribute Type color string font-family string font-size number font-weight string One of: 300, 400, 700 line-height number text-align enum One of: left, center, right text-transform string One of: none, capitalize, uppercase, lowercase text-decoration string One of: none, underline, line-through margin string lang string dir enum One of: ltr, rtl, auto class string style string Spacer We recommend you use margin or padding properties where possible to add space between components. However, if for any reason those properties don’t fulfill your needs, you can use the x-spacer component. The x-spacer component allows for either vertical or horizontal spacing. Attribute Type size number class string style string Video The x-video component adds an externally-hosted video to your message. You’ll provide the URL to your video. We support videos from Vimeo, Loom, Youtube, and Wistia. If you use an unsupported platform, we’ll show a black preview and link to the video. There aren’t any particular constraints on video size or dimensions, but you’ll see the best results when you use a video that’s optimized for your audience’s medium. For example, if you send your message to mobile devices, you might want to use a smaller or shorter videos so it better fits your mobile audience’s needs and plays well whether they’re on cellular or WiFi connections.  We don’t support YouTube shorts You can’t link to a video with /shorts in the path. You’ll need to link to a standard, full-length video (that typically contains /watch in the URL). Attribute Type src string alt string margin string width string align enum One of: left, center, right border-color string border-radius string border-style enum One of: none, solid, dashed, dotted border-width string box-shadow string opacity number class string style string --- ## Legacy in-app editor URL: https://docs.customer.io/journeys/legacy-in-app-editor/ If you built messages before July 8, 2024, you'll find them under **Content** > **In-App Messages**. You can edit these messages, but we recommend that you create new messages with our new editor. We'll eventually deprecate the editor that we talk about on this page!  The legacy in-app editor is deprecated You will not be able to create new messages in the legacy editor after October 1, 2025. We’re sunsetting the legacy in-app editor January 31, 2026. If you haven’t made the switch yet, now’s the perfect time to start using the new in-app editor, designed to help you build better in-app experiences. How it works In-app message templates built in our older template editor include all of your message content, including the text areas you can customize when you create a message in Customer.io. Any of the messages that you set up with this editor contain basic components like text and images, wrapped in actions that make them tappable. These items are then contained in blocks, lists, and grids that organize the layout of your message. For example, a button in your message that takes a person to a deep link is an action (deep link), containing a block (defining the shape of the button), containing a text component (button text). Unlike messages you create with our new editor, messages built in this older editor also rely on in-app branding settings (fonts, colors, etc) that are only used in this editor. Create a message... and use it in a campaign In-app branding Branding rules determine the colors, fonts, and padding options used in the old template editor. They do not apply to messages you create in the block-based editor, which supports custom CSS classes and reusable styles. Go to the Branding tab under Content > In-App Messages to set or change your in-app branding rules.  Branding changes take effect immediately Changes to your branding settings affect all of your messages, even messages that are in-flight! In-App message JSON You can use the Show Code option to expose the JSON representation of your message. Messages created in this editor are an array of objects, where each object represents a part of your message. Some items, like actions and blocks, contain nested components. You might use the code editor to rearrange your message by moving components in and out of blocks, or moving them up or down in the array. Each component has a type and a gist object, both are required and set automatically by the editor. type determines the type of widget you’re using in the editor—textWidget, imageWidget, actionWidget, etc. gist is an object containing a unique id for the widget. Edit an in-app message While we have a newer, easier to use editor, you might still want to edit messages that you created in our older in-app message editor. You can create new messages in this editor, but we strongly recommend that you use our new editor for new messages. Go to the Messages tab under Content > In-App Messages and select the message you want to edit. Add or move around components in your message. By default, a component only shows required fields. Use $properties.<var> to add variables to your message that you can supply in Customer.io. For example, $properties.name creates a field called Name in Customer.io Click to reveal additional properties for the component. Click Actions in the upper-right of any component to change the order of components, wrap them in blocks, actions, etc. As you create and update your message, we’ll show how it will appear in your app or on your website. Customizing messages with variables You can customize text in your message by adding $properties.<variableName> to text field. You can include as many variables as you like, and even simply set a variable name for an entire block if you want to give message creators control over the text that they send in messages. You can use the same variable multiple times if you want to duplicate variable text in your message. When you create and populate your messages, you can use 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 use your audience’s attributes or event data—like a person’s name or the items they left in their cart. In the example below, we use a variable in an in-app message called $properties.name to add a field called name when you use the message in a campaign or broadcast. Organizing your message When you add a component or container, we show required settings by default. Click to show additional options. We organize message items into blocks, content, and layout. In general, blocks make it easy to build your message with premade components, and the content and layout components give you more granular control to customize your message. blocks are premade groups of components that you can use to build your message. content are content components for your message, like text, images, etc. layout components help you organize the visible parts of your message horizontally, vertically, blocks that let you set padding, etc. in-app messages are flexible: you can nest some containers inside each other. In this section, we’ll show a few common arrangements that you’ll use to create buttons, carousels, and so on. Blocks Message Frame A message frame is a group of components you can use to establish an area with rounded corners. Nest content components within the frame as you see fit. Close Button A close button is a group of components you can use to do just that - close an in-app message. The action is automatically set to close the message, but you can also adjust the action and styling as you see fit. Button A button is a group of components you can use to add an actionable area to your message. The action is automatically set to close the message, but you can also adjust the action and styling as you see fit. A button consists of four items, nested in this order: Button padding: a block that determines the padding around the outside of the tappable area. Button action: this determines what happens when someone taps the button. Button style: a block that defines the tappable area and controls the border color, the border radius/roundness of the button, and the color of the button. Text: the text for the button. You could substitute this for a different content component: icon, image or markdown for button text. Click to style your blocks and content. You can modify the background color or center your button text, for instance. Remember, if you want to personalize the text for your buttons when you create your message, use the syntax $properties.fieldname. When you create your message in Customer.io, you’ll see a fieldname that you can personalize with 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}}. or otherwise change so that it’s relevant to your message! Two Button Row A two button row is a group of components that places two buttons, side-by-side in the same row. Set the action, styling, and text as you see fit. You might use this to create an action button and a button that dismisses your message. Rounded Image A rounded image is a group of components that displays an image within a circular viewport. You can adjust the rounding through Corner Radius within the image component. Surveys The Survey components make it easy to set up surveys for your recipients and automatically track responses in Customer.io. You can use these components to ask for feedback, ratings, or other information from your audience. With these components, it’s easy to set up the options or ratings available to your audience. See microsurveys for more information about setting up surveys and tracking responses in Customer.io. Links You can set up a link by adding an action component and then adding a text component inside the action. The action determines what happens when someone taps the text—whether the link goes to a website, somewhere in your app, a mail application, etc. Content Text A text component represents any text in your message—headings, body, footers, etc. Use $properties.<variableName> to add a customizable variable to your message. When someone uses your message in a campaign or broadcast workflow, they’ll see a field called <variableName> that they can personalize for your audience. Press to expose options to style your text. JSON JSON { "type": "textWidget", "text": "$properties.name", "style": "bodyText", "color": "black", "textAlign": "left", "maxLines": 1, "overflow": "ellipsis" } Schema Schema maxLines integer The maximum lines of text you want to display. Text over this limit is controlled by the overflow property. If unset, the message displays an unlimited number of lines. overflow string Determines how to handle text that overflows the maxLines limit (if set). By default, we cut off overflowing text with ellipsis (...).Accepted values:ellipsis,fade,clip style string The style of text you want to display. You can only set values here that are defined under Content > In-App Messages. text string Required The text you want to display. textAlign string How you want to align this text.Accepted values:center,right,left,start,end,justify type string Required Defines the widget type.Accepted values:textWidget Markdown You can use the markdown component to style text using markdown syntax. Markdown # Heading 1 ## Heading 2 ### Heading 3 **bold** __bold__ `code` [text](link) 1. go to concert 2. scream-sing your favorite song 3. return home overjoyed and exhausted - carrots - broccoli - raspberries To render markdown using your branding settings, select the styles for headings, text and links below the markdown field. Begin by specifying the color so that they appear on the canvas. Check the preview on the right to see if you’ve chosen your desired fonts and colors. Image Upload an image for your message. Click the downarrow to reveal additional properties determining the width of your image, how it should fit the bounds of the message, whether it should fade in, etc. JSON JSON { "type": "imageWidget", "image": "$properties.imageUrl", "fit": "cover", "height": 50, "width": 50, "cornerRadius": 25, "fadeInDuration": 200 } schema schema fadeInDuration integer The durration for the image to fade in, in milliseconds, similar to the fadeIn CSS transition property. fit string Determines how the image fits your message. Defaults to cover.Accepted values:none,fitWidth,cover,contain,scaleDown,fill,fitHeight type string Required Defines the widget type.Accepted values:imageWidget Icon The Icon widget displays an icon from an icon font. You must load fonts in the assets section of your app configuration to show icon fonts. JSON JSON { "type": "IconWidget", "color": "black", "font": "IconFont-One", "size": 18, "value": "\e012" } schema schema size integer The pixel size of the icon. type string Required Defines the widget type.Accepted values:iconWidget value string Required The value of the icon that you want to use. For example, for font-awesome, you’d use the name of the icon. Action An Action determines what happens when someone taps one or more components in your message. You might use an action to send someone to a link in your app, an external link, etc. When you add an action, you’ll determine the action and then add components inside the action. We support the following actions: Close message: Dismisses the in-app message. Link to web page: Sends the message recipient to a web page in their default browser. Link to web page (new tab) or mobile app (deep link): Sends a person to a deep link in your app or a webpage (if you use Universal Links). You’ll need to know the deep link format and screen you want to send a person to. Show another message: Change the content of your message when someone interacts with it. Custom action: Set up an action handled directly by your app. See the section below for more information. If you use the Show Code option, the action is the link. You can set a behavior key determining how to handle the action. See the schema below for available values. JSON JSON { "type": "actionWidget", "action": "myApp://homepage", "behaviour": "push", "component": {} } schema schema action string Required The link or place you want to send a person. This is either a deep link in your app, a web address, a mailto link, or a way to close the message (gist://close). behaviour string push: pushes a new route into the navigation stack. system: offloads the action onto the operating system. Actions like mailto:support@bourbon.sh will open the default email client. back: pops the navigation stack one step back. retain: retain replaces the current view with a new route. Accepted values:push,system,back,retain component object Required The component a person taps to perform the action defined in this widget. type string Required Defines the widget type.Accepted values:actionWidget Track Clicks The Track Clicks box tells Customer.io to track when someone taps the action—similar to the way tracked links work in emails. This setting is enabled by default. If you disable this setting, we won’t track when someone taps the action. Tracked responses are based on the action name, so make sure that you give your actions names that will make sense to you when you look at metrics later. Custom actions Unlike other actions, which do something predefined by our SDKs, you can also set up “Custom Actions”—this is custom code that you want to execute when someone interacts with your message. For example, if you send an in-app message requesting that your audience opts-into push notifications, you might execute custom code to trigger the operating system’s native opt-in prompt. Custom actions give you the flexibility to handle your audience’s responses to messages in ways that fit your app uniquely. Our SDKs expose an in-app event listener called messageActionTaken. You’ll set up your app to listen for this event and the custom actionName or actionValue that you set. When it occurs, you’ll execute custom code in your app. See the in-app pages for our SDKs to learn more about in-app event listeners and set up your first custom action. Dismissing messages with custom actions All of our SDKs expose methods to listen for the actions people take when they interact with your messages and methods to dismiss the message. If you use custom actions, you’ll need to make sure that your app or website does the following things to execute custom actions and dismiss messages appropriately: Listen for the custom action (either by event.actionName or event.actionValue). Perform your custom action—this is your own code. Use the dismissMessage method to stop showing the message to your audience. See relevant documentation for our SDKs for more information about event listeners and dismissing messages. Carousel A carousel is content that users can swipe without dismissing your message. You might use a carousel to showcase new features, provide a tutorial, etc. You’ll use a Fixed horizontal scroll widget containing at least two components. You can even wrap items in the carousel in actions, so that each item in your carousel performs a different action. Layout  Click Show Code to move components in and out of lists or grids You might want to try different list and grid layouts as you create your message. Rather than re-creating components in different list and grid containers, you can use the Show Code option and copy the components array into different containers to play with different layouts! Block A block is a group of components that you apply a design to. You might group components to set a background image, apply rounded corners, etc. The only required property is the list of components you want to nest inside the block. JSON JSON { "type": "blockWidget", "safeInsets": false, "padding": ["m","m","m",""], "backgroundColor": "white", "borderColor": "black", "borderWidth": 1, "borderRadius": 10, "height": 150, "backgroundImage": "$properties.backgroundImage", "flex": 1, "components": [] } schema schema backgroundColor string The background color for your block. You must set a value defined under Branding > Colors. backgroundImage string Set a background image for the block borderColor string The border color for your block, if you set a border width greater than 0. You must set a value defined under Branding > Colors. borderWidth integer The width of the border for this block in pixels. flex integer The single digit syntax for the CSS flex property. The value you use here determines the propotional amount of space the block consumes in a parent container. padding array of [ strings ] Defines padding for the block, based on the values set under Branding > Padding. As with the CSS padding property, values in the array represent top, right, bottom, and left padding. safeInsets boolean Based on the env safe-area-inset-* CSS properties. Set to true to ensure that the block can’t overflow the defined screen or the defined area of your message. Defaults to false. type string Defines the widget type.Accepted values:blockWidget The Flex property The flex property is an integer that defines the proportion of two or more blocks in a horizontal list. Using a banner example, we can show the same banner with three different pairs of flex values. 1:1 3:1 9:1 Horizontal List A horizontal list lays out components horizontally, letting you set the alignment of the items in the list. Nest content components within this layout to horizontally align them. Wrap these components in blocks for additional styling. The flex property of blocks can help you establish the ratio of space between each block in a horizontal list. JSON JSON { "type": "fixedHorizontalListWidget", "mainAxisAlignment": "start", "crossAxisAlignment": "center", "components": [{},{}] } schema schema type string Defines the widget type.Accepted values:fixedHorizontalListWidget Vertical List While components in your message are typically displayed top-to-bottom, a vertical list helps you align these items. You can set up vertical lists to control the vertical layout of your message. JSON JSON { "type": "fixedListWidget", "mainAxisAlignment": "start", "crossAxisAlignment": "center", "components": [{},{}] } schema schema Carousel A carousel is a list of items you can scroll hortizontally. One component displays at a time, and users can swipe left to right to see different components. A carousel takes up the entire width of the parent list, block, or grid. JSON JSON { "type": "fixedHorizontalListScrollWidget", "height": 150, "components": [{},{}] } schema schema height integer The height of the widget in pixels. type string Defines the widget typeAccepted values:fixedHorizontalListScrollWidget Grid A grid is like a horizontal list, except that you can control the padding between elements and the aspect ratio of items in the grid. This allows you to customize your layout with tall, skinny grids or short, long grids. By default, a grid’s aspect ratio is 1.0. JSON JSON { "type": "fixedGridWidget", "itemPadding": "xs", "columns": 2, "childAspectRatio": 1.5, "components": [{},{}] } schema schema childAspectRatio number The aspect ratio for items in the grid. Defaults to 1.0 columns integer Required The number of columns in your grid. itemPadding string The padding between items in your grid. type string Required Defines the widget type.Accepted values:fixedGridWidget Conditional Use a conditional to show or hide parts of your message based on a true/false condition. There are three fields to complete when using a conditional component: Condition: The function that you want to evaluate as “true” or “false”. The condition field supports: >, <, == & in. If you don’t specify an operator, the condition will check if the property is null. If True: The components that you want to display when the condition is true. If False: The the components that you want to display when the condition is false. When building your message, entering 1 == 1 is an easy way to force the condition to true and 1 == 0 will force it to false. When you’re set with the design, replace the condition with $properties.condition_field == MATCH-VALUE. This adds a field called Condition Field to your in-app messages which you can then configure with static or dynamic values from the campaign or broadcast. If the value matches, the True section will show to the recipient. Otherwise, the False section will show. --- ## Get Started URL: https://docs.customer.io/journeys/whatsapp-get-started/ You can set up WhatsApp using your Facebook Business Account or through Twilio if you use Twilio to send SMS messages. We'll help you figure out which option is right for you and get you set up. How it works WhatsApp is similar to SMS in that you can send messages to your audience using their phone numbers. But you’ll need WhatsApp-approved phone numbers to send messages; these are separate from the phone numbers you use to send SMS messages. After you’ve enabled WhatsApp support, you’ll set up templates for your messages. Unlike SMS and other channels, you won’t write message content directly in Customer.io; WhatsApp messages are based on templates that you create in WhatsApp (or through Twilio), and then populate in Customer.io. So, before you can send WhatsApp messages, you’ll need to: Set up WhatsApp support in Customer.io through either your Facebook Business Account or your Twilio account. Create templates and have them approved by WhatsApp. Select a template when you go to send a WhatsApp message. Should I set up WhatsApp through Twilio? Whether or not you set up WhatsApp through Twilio, you’ll need a Facebook Business Account to send WhatsApp messages. If you don’t have one, that’s where you’ll start. Beyond that, if you already have your own Twilio account that you use to send SMS messages through Customer.io, it might be easier to set up WhatsApp support using your Twilio account. Twilio can help you get WhatsApp-approved phone numbers, and they’ll help you create and get WhatsApp-approved templates approved by WhatsApp (Meta). However, while Twilio can act as a concierge for your WhatsApp setup, using a third party makes it difficult to understand what parts of the process are handled by Twilio and what parts are handled by Customer.io. If you don’t have a Twilio account—or you want to make sure that your WhatsApp setup isn’t dependent on a third party—you can connect Customer.io directly to your Facebook Business Account to send WhatsApp messages. Removing Twilio from the equation can make it easier to understand and troubleshoot WhatsApp support. Set up WhatsApp support Before you can send WhatsApp messages, you must have a WhatsApp-approved phone number that you’ll use to send messages. If you haven’t gotten a WhatsApp-approved phone number, you might want to do that before you set up WhatsApp support in Customer.io. If you’re going to send WhatsApp messages through Twilio, see Set up WhatsApp support using Twilio below. Go to > Workspace Settings > WhatsApp. Click Connect WhatsApp Business. Enter your Facebook Business Account credentials and follow the prompts. As a part of this process, you’ll enter your business information and the sender number you want to use to send messages. When you’re done, you’ll be set up to send messages from Customer.io. Though you’ll need to create WhatsApp-approved templates in your Facebook Business Account before you can send messages. Set up WhatsApp support using Twilio Twilio WhatsApp support is tied to our SMS functionality. If you want to send WhatsApp messages using your Twilio account, you’ll need to set up SMS in Customer.io first and then ask Customer.io to enable WhatsApp support for your account and workspace.  Why does Customer.io have to enable WhatsApp support for me? Twilio’s WhatsApp approval process may take a few weeks to complete. We manage WhatsApp support on a customer-by-customer basis to make sure that your account and workspaces are fully prepared to send WhatsApp messages before we enable the feature. To send SMS and MMS messages in Customer.io, you’ll need to have a Twilio account and the Sender phone number. This might be a regular phone number, short code, or an alphanumeric ID. Twilio can lease these numbers to you. Or, if you have a paid Twilio account with Alphanumeric Sending enabled, you can send messages from an Alphanumeric ID instead of a Twilio phone number. Set up a Twilio account if you don’t already have one. We recommend using a trial account to get started. Set up a Twilio-specific Sender if you don’t already have one. You can’t use your own phone number to send SMS; you need to purchase a number from Twilio. If you already set up your sender number, select it when you compose messages. In your Customer.io workspace, go to > Workspace Settings > SMS and add your Twilio Account SID and Auth Token. You’ll find these values in your Twilio dashboard. Complete the WhatsApp approval process. You must have an approved WhatsApp Business Account to send WhatsApp messages. Twilio facilitates the approval process and provides instructions to help you enable WhatsApp messaging. In Twilio, create the content templates you want to use to send messages. Contact win@customer.io to enable WhatsApp in your Customer.io account. After we enable WhatsApp support in your account, you’ll see an option in the SMS editor to send a message to your WhatsApp audience. Link in WhatsApp messages You might want to track and shorten links that you add to WhatsApp messages. In general, if you track links, you should also use our link shortening feature; when you track a link, it becomes a much longer URL that can take up considerable space in your message if you don’t shorten it. Link tracking You can enable link tracking by either: Enabling link tracking at the campaign or message level. If you want to track individual links, you can use the {% cio_link url:"https://example.url" %} tag to track links in your messages. Link shortening Link shortening settings depend on how you set up WhatsApp support—directly through your Facebook Business Account (WhatsApp in your workspace settings) or through Twilio (SMS in your workspace settings). Native WhatsApp support Native WhatsApp support You’ll find link shortening settings in Workspace settings > WhatsApp. When you enable link shortening, we’ll automatically shorten links to a URL like https://a.cx.io/abcde12345 (or https://e.cx.io/abcde12345 if you’re in our EU data center). Shortened links are between 26 and 36 characters: 8 for the https:// prefix. 7 for the domain (a.cx.io or e.cx.io). 11-16 characters for the path (the slash and random characters /lnk.abc123). We start with a 10 character path and increase the length until we find a path that isn’t in use. Shortened links expire after 90 days. Add a custom short link domain Custom short link domains help you brand your links and make them easier for your audience to recognize. You can add up to 10 custom short link domains per workspace, which are shared across your SMS and WhatsApp channels; any domain you add is available for both channels, and you can set the default domain for each channel. As a part of this process, you’ll need to add CNAME and TXT records to your domain to verify that you own it and grant Customer.io permission to use it. Your custom short link domain cannot be the same domain you use for email link tracking. Go to your > Workspace settings > SMS or WhatsApp settings. Click Add custom domain. Enter your custom domain and click Add domain. Go to your domain registrar and add the appropriate records to your domain. The Add custom domain dialog contains the values you’ll need to copy to your domain host. CNAME record: points your custom domain to Customer.io’s servers. TXT record: verifies that you own the domain. The record name uses the format _cio.yourdomain.com. It can take up to 24 hours for these records to propagate, though it usually happens much faster. Until then, you’ll see that your custom domain is pending. In many cases, you can wait a minute or two and refresh the page to see that your custom domain is active. If you want to use multiple domains or override the default for individual messages, see use multiple short link domains. WhatsApp via Twilio WhatsApp via Twilio If you send WhatsApp messages through Twilio, your WhatsApp messages share link shortening settings with SMS messages. You’ll find link shortening settings in Workspace settings > SMS. Here you can set up link shortening including a custom short link domain. See link shortening for more about your short link setup. --- ## Create content templates URL: https://docs.customer.io/journeys/whatsapp-content-templates/ WhatsApp messages are based on templates approved by Meta. So, before you can send WhatsApp messages, you'll need to create templates in Twilio and submit them for approval. How it works WhatsApp requires that you use templates to ensure that messages comply with their policies and to prevent spam. So, before you can send WhatsApp messages, you’ll need to create templates and have them approved by Meta/WhatsApp. The process to create and use templates depends on whether you connected your Facebook Business Account directly to Customer.io or set up WhatsApp support as a part of your SMS setup through Twilio. This template in Twilio Becomes this message in Customer.io If you set up WhatsApp support in Customer.io using your Facebook Business Account, you can either create templates in your Facebook Business Account or within your Customer.io workspace using Design Studio. If you set up WhatsApp support through Twilio, you’ll create templates in Twilio and select them in Customer.io when you go to send a message.  It can take up to 24 hours for Meta to review your templates While Meta (WhatsApp’s parent company) typically reviews templates with an algorithm and approves or rejects them within minutes, some templates go through a manual review process that can take up to 24 hours. Make sure that your template is approved before you try to use it in Customer.io. Personalize WhatsApp content templates When you create templates, you’ll add variable fields to your message with {{1}}, {{2}}, etc. When you go to send a message, you’ll pick a template and populate these variables—either with static text or using 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 personalize messages. Create a content template  Streamline your workflow by creating WhatsApp templates in Design Studio! If you integrated with your Facebook Business Account directly, you can create and submit your templates for approval from Design Studio—our newest message editor! Learn more in our Design Studio WhatsApp guide. If you manage WhatsApp support in Customer.io using your Facebook Business Account, you’ll create templates in your Facebook Business Account. See Meta’s documentation about creating and managing content templates if you want to do this programmatically. After you create a template and it’s approved by Meta, it’ll appear in Customer.io when you go to send a message. Go to Meta Business Suite, select your business portfolio, and go to your WhatsApp Manager. Select the account that you want to create the message template for. Click > Manage message templates and click Create message template. Set your template’s category, name, and languages. For category, choose from marketing, utility or authentication. You can hover over the template types to view details for each template. Name: Enter name of the template in lowercase letters, numbers, and underscores only. Language: Set your template’s language code. Follow the steps below to create your template depending on whether you want to make a Marketing or Utility template or an Authentication template. Set up a Marketing or Utility template Add a sample of your message based on the values you plan to populate from Customer.io. This helps WhatsApp understand what kind of message you plan to send during the review and approval process. Make sure your message includes representative examples and does not include any actual customer information. (Optional) Add a Header. Set a title or choose the type of media you want to use for your header. Enter the Body of your message in the language you’ve selected. You can edit text formats, add emojis or include variables. Where you use variables, you can populate them with data from Customer.io. (Optional) Add a Footer to the bottom of your message template. (Optional) Add Buttons to your message. You can select from the dropdown menu to create buttons your audience can use to respond to your message or take action. If you don’t want to add any buttons, select None. (Optional) Add a Call to action to your message. Here you can combine up to 10 buttons in a button list so your audience can take action; these are independent of other buttons. The types of actions include things like “Call phone number” and “Visit website.” Note that you can combine a call-to-action and a quick reply as one button. (Optional) Add Quick replies to your message. Create up to 3 buttons that let your customers respond to your message. Click Submit. WhatsApp will review your template and approve it or reject it. You’ll see your template’s status in the WhatsApp Manager. When they approve your template, you can begin sending messages with it. Set up an Authentication template Set the Code delivery method for how your customers input the code into your app. Enter the Message content for your authentication template. The message content is fixed as shown in the preview. You can optionally add a security recommendation statement or code expiration time in your message. Click Submit. WhatsApp will review your template and approve it or reject it. You’ll see your template’s status in the WhatsApp Manager. When they approve your template, you can begin sending messages with it. Create a content template in Twilio When you create a content templates, follow Twilio’s Guidelines to make sure that WhatsApp approves your template. In Twilio, go to Develop > Messaging > Content Template Builder and click Create New. Give your template a name and select the language you’ll use in your template. Use the full name of the language, not a language code. For example, use English not en-US. Select your content type and then click Create. Text messages are relatively simple. Other content types are based on the basic message types supported by WhatsApp. Note that some templates, like the List Picker are restricted to conversational messages. Add your template content. Available fields change based on the content type you selected. Use the {{1}}, {{2}}, etc. syntax to add variables to your content template. Follow Twilio and Meta’s rules to ensure that your template gets approved. Meta might disapprove of your template if your variables are not sequential, if variables are the start or end of your message body, and so on. Click Save with Samples and give names to your variables. These are the titles that’ll show up in Customer.io, so make sure they’re descriptive enough for your team members to understand what they’re for. Click Submit for WhatsApp approval. Troubleshooting a template that doesn’t appear in Customer.io If you don’t see a template when you set your message in Customer.io: 1. Check the template’s approval status Meta needs to approve templates before they appear in Customer.io. To check a template’s approval status, go to the service you use to manage your WhatsApp templates (Meta Business Suite or Twilio). Native WhatsApp support Native WhatsApp support Go to Meta Business Suite and select your business portfolio. In the left panel, go to WhatsApp Accounts and select your account. Scroll down and click WhatsApp Manager. Click Manage message templates and find your template. In the Status column, check the template’s status. Active: The template is approved and ready to use. In Review: Meta is still reviewing it—this typically takes minutes but can take up to 24 hours. Rejected: Meta did not approve the template. Review the rejection reason, edit the template, and then resubmit it. WhatsApp via Twilio WhatsApp via Twilio Check the template’s status in the Twilio Content Template Builder. An approved template shows a green WhatsApp Approved badge. Review the rejection reason and edit the template as needed. 2. Verify that you’ve connected the correct WhatsApp Business Account to Customer.io If your template is approved but still doesn’t appear, Customer.io may be connected to a different WhatsApp Business Account than the one where you created the template. Go to > Workspace Settings > WhatsApp and note which account is connected. Go to the Meta Business Suite for the account that’s connected to Customer.io and confirm your template exists under that same account. If they don’t match, either reconnect Customer.io to the correct account, or create the template under the account that’s connected to Customer.io. --- ## Send a WhatsApp Message URL: https://docs.customer.io/journeys/send-whatsapp/ After you've [enabled WhatsApp support](/journeys/whatsapp-messages/) and created [WhatsApp-approved templates](/journeys/whatsapp-content-templates/), you can send WhatsApp messages in any campaign or broadcast. Set up your WhatsApp message  If you created your WhatsApp template in Design Studio, check out our Design Studio docs! The process for sending a WhatsApp message made with Design Studio is simpler than with other templates. Learn more. Add a WhatsApp message block to your campaign or broadcast workflow. Click Add Content. Select a WhatsApp approved sender in the From field. If you only have one sender phone number, it’s already selected for you.  We don’t know which of your Twilio senders are WhatsApp approved! If you send WhatsApp messages using Twilio, and you select a sender that is not WhatsApp approved, your messages will fail. When you add sender numbers in Twilio, you might want to add “WhatsApp” to the sender names. This’ll help you identify which senders are for WhatsApp messages. In the Template field, search for the template you want to use for your message. If you don’t see the template you’re looking for, you may not have created it yet or it may not have been approved by WhatsApp. Fill in the templated variables for your message. You can use 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}}. in these fields to personalize your message. Click Save. Localizing WhatsApp messages WhatsApp supports different languages, but only one language per template. If you want to translate your WhatsApp messages, you’ll need to create a separate template for each language. Then, in Customer.io, you’ll need to set the right template for each language variant. You can create multiple templates in WhatsApp with the same name but different languages. Business-initiated vs user-initiated sessions The kinds of messages you can send through WhatsApp change depending on whether you or your audience initiate the conversation. These are called business-initiated and user-initiated (or conversational messaging) sessions respectively. In general, WhatsApp messages sent through Customer.io are business-initiated. User-initiated conversations can be more flexible than business-initiated messages, providing a 24-hour window during which you can send free-form replies and use special templates, like location-based or list-based messages, that aren’t available in business-initiated conversations. But Customer.io doesn’t support incoming messages natively. If you want to respond to user-initiated conversations, you’ll need to set up Twilio to handle incoming messages; you’ll use keywords in Twilio to send webhooks to Customer.io that trigger outgoing campaigns based on the messages you receive. --- ## WhatsApp metrics and reporting URL: https://docs.customer.io/journeys/whatsapp-metrics/ Customer.io tracks metrics for your WhatsApp messages so you can understand how your audience interacts with them. You can view WhatsApp metrics in campaign dashboards and the delivery log. WhatsApp message metrics The following metrics apply to WhatsApp messages. Some metrics, like Opened, behave differently for WhatsApp than for email because they rely on WhatsApp-specific signals like read receipts. Metric Description Drafted We generated the message but haven’t sent it yet. The message requires manual action if you use Queue Draft. Sent The message left Customer.io and was passed to the delivery provider. Delivered The delivery provider confirmed that the message reached the recipient. Opened The recipient read the message. For WhatsApp, this is based on read receipts, not a tracking pixel. If a recipient turns off read receipts in their WhatsApp settings, we can’t track this metric. Clicked The recipient tapped a tracked link in the message. Failed The message didn’t leave Customer.io. This typically happens when a required variable is missing or a template isn’t approved. Converted The recipient completed your conversion goal.  Read receipts and the Opened metric WhatsApp’s Opened metric relies on read receipts, which recipients can turn off in their WhatsApp privacy settings. If read receipts are off, Customer.io won’t receive an open signal for that message. This is different from email, where open tracking uses a tracking pixel. Reply tracking Customer.io doesn’t track replies for WhatsApp messages. This means that you can’t use replies as a conversion goal or typically respond to inbound messages through Customer.io without a webhook-based workflow. --- ## Frequently Asked Questions URL: https://docs.customer.io/journeys/faq-whatsapp/ Answers to common questions about WhatsApp messages in Customer.io. How much does it cost to send WhatsApp messages? Customer.io doesn’t charge you for WhatsApp messages. However, both Meta and Twilio (if you use Twilio) charge for WhatsApp usage. See Billing for WhatsApp messages for more information. Meta charges per message using a tiered pricing model based on the template type (marketing, utility, authentication, or service), recipient country, and message volume. See Meta’s WhatsApp pricing page for details. Twilio As of February 2026, Twilio charges $0.005 per WhatsApp message. If you send WhatsApp messages through Twilio, you’ll pay this amount in addition to Meta’s fees. See Twilio’s WhatsApp pricing for details. How do I know the difference between SMS and WhatsApp deliveries? If you set up WhatsApp support in Customer.io using your Facebook Business Account, you’ll see a whatsapp type in the Delivery Logs. If you send WhatsApp messages using Twilio, you’ll see a twilio type in the Delivery Logs. WhatsApp messages sent through Twilio are nearly identical to SMS messages in Customer.io. You can see the difference between Twilio SMS and WhatsApp messages using the to and from fields in the Delivery Logs. WhatsApp messages prefix these values with whatsapp:. Otherwise, all of the reporting metrics that apply to SMS messages also apply to WhatsApp messages. See WhatsApp metrics and reporting for more information. Does Customer.io know if a Twilio sender is WhatsApp-approved? No. Unfortunately, if you send WhatsApp messages using Twilio, we cannot identify WhatsApp senders separately from your SMS senders. You can only obtain this information from the WhatsApp Senders page in your Twilio console. Because Customer.io can’t tell the difference WhatsApp and SMS senders, you might want to add “WhatsApp” or “SMS” to your sender names in Twilio before you sync sender identities from Twilio to Customer.io. When, when you sync (or re-sync) your sender identities from Twilio to Customer.io, you’ll be able to tell the difference between SMS and WhatsApp senders in Customer.io. A phone number doesn’t appear as a sender If you don’t see your phone number listed as a WhatsApp sender in Customer.io: Native WhatsApp support Native WhatsApp support Make sure that you’ve registered the phone number you want to use as a WhatsApp sender in your Facebook Business Account. If you haven’t, you’ll need to register it before you can use it to send WhatsApp messages. Go to > Workspace Settings > WhatsApp and confirm that setup completed successfully. You should see your connected phone number listed there. If setup didn’t complete, click Connect WhatsApp Business and go through the setup flow again, making sure to complete all steps including granting the required permissions. WhatsApp via Twilio WhatsApp via Twilio Go to Twilio and check your WhatsApp senders page to confirm that your sender is listed as WhatsApp-approved. If you don’t see your sender, or it isn’t listed as WhatsApp-approved, you’ll need to register it. If your sender is correctly set up in Twilio, go to > Workspace Settings > WhatsApp and click Sync from Twilio.  Add ‘WhatsApp’ to your sender names Customer.io doesn’t know which of your Twilio senders are WhatsApp-approved. If you don’t include “WhatsApp” in your sender names, it may be hard to tell which of your sender phone numbers are for WhatsApp messages and which are for SMS messages. Do you support Messaging Service SIDs? No. You must provide a single sender ID when you send a message. Why do I get a 21212 error? This error means that your message’s From address uses a Messaging Service SID. We don’t support Messaging Service SIDs for WhatsApp messages today. You’ll need to provide a single sender ID when you send a message. A Messaging Service SID is essentially a pool of senders that Twilio uses to load-balance messages. See Twilio’s documentation for more information about registering WhatsApp senders with Twilio. Do you support conversational messaging? Yes, but you’ll need to set up specialized campaigns to support it. Conversational messaging are one-to-one, free-form messages between you and your customers. Per WhatsApp requirements, your audience must initiate the session by texting you first. When your audience begins a session, you can send free-form replies for up to 24 hours. To detect the start of a session, you’ll need to set up a workflow in Twilio to handle the incoming message. The process would be similar to our unsubscribe keyword process. How do I disconnect my WhatsApp account? You can’t revoke WhatsApp access from within Customer.io directly. To disconnect your WhatsApp account, you need to remove Customer.io from Meta’s Business Manager. Go to business.facebook.com. Go to Settings > Partners and remove Customer.io from your Business Portfolio. Go to Settings > Integrations > Connected Apps and remove Customer.io. After you complete these steps, Customer.io no longer has access to your WhatsApp Business Account.  You can reconnect your WhatsApp account to Customer.io in the future. Go to > Workspace Settings > WhatsApp and click Connect WhatsApp Business to set up the connection again. --- ## Get Started URL: https://docs.customer.io/journeys/line-get-started/ Get started using LINE to send messages to your customers as part of your triggered campaigns! Set up LINE Before you connect LINE to Customer.io, you need: A LINE Official Account with the Messaging API enabled A Messaging API channel in the LINE Developers Console If you haven’t done these things yet, follow LINE’s getting started guide to create an account and enable their Messaging API. Get your channel access token To connect LINE to Customer.io, you’ll need your LINE API key. This is the channel access token for your Messaging API channel. Go to the LINE Developers Console and select your provider. Select your Messaging API channel. Go to the Messaging API tab and scroll to the Channel access token section. Click Issue to generate a token and copy it. Connect LINE to Customer.io Go to Integrations > Add Integration and select LINE. Paste your channel access token into the LINE API key field. Set up LINE webhook to track metrics You’ll set a webhook in your LINE account to send metrics back to Customer.io—things like message deliveries, opens, and so on. Go to the LINE Developers Console and select your Messaging API channel. Go to the Messaging API tab and find the Webhook settings section. Set the Webhook URL to https://track.customer.io/inbound/line and make sure that the Use webhook setting is enabled. This webhook also handles follow and unfollow events. When someone follows your LINE Official Account, we automatically create a profile in Customer.io with their LINE ID. When someone unfollows, we update their profile accordingly. Backfill existing LINE users If you already have followers on your LINE Official Account and want to import them into Customer.io, your account must be Verified or Premium in LINE. Contact your Customer.io account manager and we’ll help you backfill your existing audience. Now you’re ready to send messages using LINE! LINE IDs: Identifying your audience The LINE ID is not the same as a user’s display name or the typical ID that people use to become searchable by friends. A user’s LINE ID is a unique identifier that they get per provider, where a provider essentially represents your business or a subsidiary of your business. That means that, in most cases, people will have a single LINE ID per workspace—though they could have multiple LINE IDs if you serve multiple businesses or providers from the same Customer.io workspace. When someone follows or unfollows your LINE account, we automatically create a profile in Customer.io for them with their LINE id. You should send messages to new followers inviting them to go to a website or app that you own, where people can self-identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously.. This will let you associate their LINE ID with their profile in your workspace and act as a bit of a two-factor opt-in path for your audience. If you don’t do this, you’ll have line IDs in Customer.io that are effectively anonymous; by itself, a LINE ID doesn’t tell us much about the user. That’s not necessarily a problem—you can still send these people messages. But people with just a LINE ID likely won’t have meaningful attributes that you’d normally use to set up behavioral campaigns or personalize messages. flowchart LR a[User follows or unfollows your LINE account] --> b[We create a profile in Customer.io for them] b --> c[User receives LINE message] c --> d[User visits your website or app] d --> e[User self-identifies e.g. logs in or fills a form] e --> f[Your app sends LINE ID + identity to Customer.io] f --> g[LINE ID linked to person's profile] Merging users with multiple LINE IDs While people can have multiple IDs in LINE, or could generate new IDs over time, they’ll only have one line_id attribute in your workspace. We have some logic in place to make sure that we store and use the correct, canonical line_id for any given recipient. When you identify a user with a LINE ID, we’ll automatically look up the user’s LINE ID to see if it belongs to a profile in your workspace. If the person already exists, we’ll merge the profile with the new LINE ID to the existing profile. When you send a message, we’ll send to the correct profile based on the LINE ID. flowchart LR a[Person has multiple LINE IDs] --> b[Customer.io looks up the correct LINE ID] b --> c[Customer.io merges the profile with the correct LINE ID] c --> d[Person receives the correct LINE message]  Merging new LINE IDs will temporarily increase your profile count When you identify a new LINE user, Customer.io must create a new profile before we can merge it with the existing person. This adds a person to your workspace temporarily, which will increase your profile count for the month affecting your count of billed profiles for the month. --- ## Send messages URL: https://docs.customer.io/journeys/line-send-messages/ After you've set up LINE, you can send messages to your audience as a part of campaigns or broadcasts. Send a LINE message As a part of your campaign or broadcast workflow: Drag LINE into your workflow. Click the message and click Add Content. If you store your audience’s LINE ID in an attribute other than line_id, set the correct attribute in the To field. Add text and image content to your message. Click Save Changes. What kinds of messages can I send? While LINE supports a variety of message types, we support basic text and image messages. You can send text and an image in the same message, but your text and image appear to the user as separate messages. We don’t charge you by the message, so this doesn’t affect your usage or bill! How do you set the LINE ID? By default, the LINE ID—the way you identify your LINE audience—is the line_id attribute. If you store this attribute in differently, you’ll set it in the To field using our standard 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}}. syntax for attributes—{{customer.line_id}}. LINE metrics We track metrics for your LINE messages so you can understand how they performed across your audience. See LINE metrics and reporting for the full list of metrics and how they work. --- ## LINE metrics and reporting URL: https://docs.customer.io/journeys/line-metrics/ Customer.io tracks metrics for your LINE messages so you can understand how your audience interacts with them. You can view LINE metrics in campaign dashboards and the delivery log. LINE message metrics The following metrics apply to LINE messages. Note that metrics like Delivered depend on the webhook you set up in your LINE account. Metric Description Attempted We’re trying to send the message to LINE. You may see this status if we need to retry your message. Sent The message left Customer.io and was passed to LINE. Delivered LINE confirmed that the message reached the recipient, reported through your LINE webhook. Failed The message didn’t leave Customer.io. This typically happens when a required variable is missing or there’s a configuration issue. Clicked The recipient tapped a tracked link in the message. Converted The recipient completed your conversion goal.  We don’t support reply tracking for LINE messages This means that you can’t use replies as a conversion goal or respond to inbound messages. --- ## Get Started URL: https://docs.customer.io/journeys/slack/ Customer.io's Slack Action helps your teams work better together by passing information directly into Slack triggered by user behavior in your app, all customized to your needs. What you need to get started It’s very easy to set up Slack in Customer.io. All you’ll need are your Slack login details, if you’re not logged into your team already. Enable Slack in Customer.io Before you can send Slack messages, you need to enable the Slack action in Customer.io—essentially inviting Customer.io to your Slack workspace as a bot. When you configure Slack in Customer.io, you need to provide Slack credentials with the “bot” scope, letting Customer.io post to your Slack workspace. From your workspace dashboard, go to Workspace Settings and click Get Started next to Slack Message. From here, you can enable or disable Slack actions. Click Enable to get started. If you aren’t logged in to Slack, log in. If you have multiple workspaces or teams, you’ll need to select the workspace that you want to post Slack messages to. Select a channel. This channel you select isn’t used. You’ll set the channel you want to send messages to whenever you add a Slack message to your campaign workflow. Slack simply requires that you select a channel when you set up an integration.  Posting to a private channel If you want to post messages to a private channel, you must invite your bot to a channel with the /invite @Customer.io command. Click Allow. Your workspace will show that Slack actions are enabled. Set up your campaign You’re ready to start sending Slack messages! Once you create your campaign, head to the workflow. You’ll see Slack in the sidebar, so drag and drop it into your workflow builder. Once you drag it in, the edit panel will be open. You can name your message, add content, and change its settings: Compose your message In the composer, there is a lot you can do to customize your Slack message. For a full list, see Slack’s documentation. You can use a default {{customer.channel}} Liquid tag, or you can customize it. We’ve chosen to customize it, to send all of these notifications to a channel called #new-signups. We’ve also added some text and the {{customer.username}} Liquid tag, to let us know who signed up. Mention users To mention specific users in your slack message, follow this format: Hey <@U012AB3CD>, thanks for submitting your report. You’ll need to retrieve the user’s member ID via Slack’s UI or APIs. The ID is converted to the user’s display name in Slack. Embed links You can also choose to embed a link in your slack message using markdown: <https://www.example.com|The message you want hyperlinked>. Send a test This is the last step; we want to make sure that our Slack message is working. To do that, use the Send test modal. This will send a real Slack message, so be sure you choose the right test channel or user to send it to! Success!! Your test message should then appear in whichever channel you chose to send it to! --- ## Translate your messages URL: https://docs.customer.io/journeys/localization-getting-started/ Start here to learn how to translate messages and send them to the right recipients. Across any workflow, you'll create your message in a default language then translate it to all language variants you support. You'll handle translations within message editors and won't need to create branches in your campaigns to send the right language variant. How it works To localize messages, you’ll follow this process: Set a language attribute for your workspace so we know how to determine a recipient’s language preference. Create messages with language variants. When you add or update people, you can set an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. containing their language preference. Then, when you set up messages, you add translations matching your users’ language attributes. You can add language variants to any message channel: email, SMS, Whatsapp, push, and in-app. When your message sends, we match people’s language attribute to your translations. If we don’t find a match, we’ll send them the Default message, as indicated at the top of the editor. graph LR A[Add language attribute] --> B[Set up multi-language message] B --> C[Send message] C --> D{Does a person's language attribute match a language variant?} D -->|no| H[Person gets default message] D -->|yes, lang=es| E[Person gets Spanish message] D -->|yes, lang=fr| F[Person gets French message] D -->|yes, lang=de| G[Person gets German message] Set up a multi-language message After you define your language attribute, you can start adding translated content. When you create a message with multiple languages, you begin by drafting your Default message. People receive the Default message when their language preferences don’t exist or don’t match one of the translation options. The Default message is also the foundation of each of your translations. If you leverage our agent to translate your content, it translates your Default message. To create a message and add translations, follow these steps: Draft your Default message. Click Add language and check the languages you’ll want to include. In Design Studio, you need to click next to the email’s name to find this. Select Auto-translate with AI if you want to use our agent. Remember to review the results for accuracy. Otherwise, you can learn more about working with translation vendors and how to export/import content below. Send yourself a test to make sure each version renders as expected. Save your changes. Translate with AI You can translate your default message with AI when you add a language variant. This is available across all of our message channels. Remember to review your translations for accuracy; generative AI can make mistakes.  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. What our AI auto-translates The Auto-translate tool will translate the following: Body text Subject lines Preheader text It will not auto-translate the following: Images, but it will translate alt text Email layouts in the rich text or code editors Liquid: Static text, attribute values, filter values, etc. Snippets: Rather, you should add liquid conditionals based on people’s language preferences to the snippet file. Text in custom components: However, our AI tool will translate the text if you detach the component first, and then translate the message. Learn more about detaching a component from its source file. Regenerate translations with AI We do not automatically update translations when you change the default template. For most editors, if you made a change to your default message and want translations to reflect the updates, you’ll need to delete the translations and then re-add the language variants. Sync translations in Design Studio In Design Studio, if you made changes to your default message and want translations to reflect the updates, you can refresh your translations from your default message. Unlike other editors, you don’t have to delete and then re-add the variants. Click the language dropdown, and choose Refresh from Default. Select which languages you want to regenerate from your default template. By default, we’ll auto-translate with AI, but if you uncheck this option, we’ll overwrite the template with the default message. Click Refresh selected. This overwrites your selected language variants with the latest translations. Retry failed AI translations Sometimes a translation fails, like when there’s an issue connecting with our AI service. If a translation fails, you can click Retry in the failure notification. Languages we don’t auto-translate This feature can auto-translate most languages we support, but check out the exceptions in our locale list. If you select a locale that is not officially supported by our Auto-translate feature, you’ll see that we won’t translate it: In this example, both locales for Turkmen (tk) and Uzbek (uz) are selected, as indicated by the bubbles below the locale list. We will auto-translate Uzbek, but not Turkmen. After you click Add, new tabs appear for both languages. However, only Uzbek will show a translation. Turkmen would show a copy of the default message and still need to be translated by another service or vendor. Below are some tips for working with a translation vendor. Translating liquid When we send your message, we render liquid based on the values stored on attributes, so make sure you store values in native languages. For instance, if you store the most frequent purchase on a person’s profile like "frequent_purchase":"soap" and you want to include that in the message, the word “soap” would appear, no matter their language preference. Whether you’re adding languages with our auto-translate feature or through a vendor, make sure you translate any copy within your liquid statements as well as default filter values. For instance, we don’t auto-translate the fallback in {{customer.first_name | default:"there"}}. To add a simple greeting with a fallback, you’d translate the value in default: Bonjour {{ customer.first_name | default:"ami" }} You could also include a single liquid conditional across your language variants so the fallback renders based on a language variable. The text between conditions is not automatically translated: {% if customer.first_name %} {{customer.first_name}} {% elsif customer.language == 'fr' %} ami {% else %} there {% endif %} How to work with a translation vendor Export email to HTML for vendors When you set up a message with multiple languages, you may need to send content to a translation vendor. To simplify this process, we suggest that you draft, test, and finalize your Default message first. flowchart LR A[Create Default message] -.->F{Test message} D[Export and send to translator] -->|Get translations back| E[Add languages] E --> G[Send message] F-.-> |Tests pass|D F-.-> |Tests fail|A We recommend you send HTML to a translation vendor, unless your vendor requests a different format. How you export to HTML depends on the editor you’re using: Design Studio: If you built the email using our component syntax (like <x-paragraph>), switch to the code editor, and copy/paste the code into a shareable file. If you built the email from standard HTML (like <p>), click next to the email name, select Export, then copy/paste the HTML and save it as a file you can share externally. Drag-and-drop editor: Click Actions > Export to HTML. Rich text editor: Click HTML underneath the email header to copy/paste the source code. Then save this to a file to share externally. Code editor: Copy your HTML and save to a separate file. Add translations from vendors To add a translation to your message: Click Add language. In Design Studio, you need to click next to the email’s name to find this. Select one or more languages to add. Make sure Auto-translate with AI is not selected. Add in your translated content: Design Studio Design Studio If you exported the component syntax (like <x-paragraph>), switch to the code editor, and replace the code with the returned translation. If you exported standard HTML (like <p>), paste in the translated text. Drag-and-drop editor Drag-and-drop editor Paste translated text into the blocks of your message. Rich text editor Rich text editor Click HTML then paste in your translated HTML. You may need to reformat text after pasting. Code editor Code editor Paste in the translated HTML. A/B tests with localized messages You can A/B test messages with translations in newsletters, but not API-triggered broadcasts or campaigns. If you add languages to a message in a campaign workflow, you can’t A/B test that message—even if you delete the languages later. To set up A/B tests for newsletters with translations, you’ll follow the same process as you would without translations. Just make sure whatever changes you make to each test are duplicated across all language variants. For instance, if you’re testing different subject lines between variant A and B, make sure the subject is translated accordingly. If you set up languages for each variant in your test, we’ll split users across the tests before we check their language attribute. So, while your audience is split according to your settings, they might not be split evenly across languages variants. Right-to-left text formatting When editing emails or in-app messages, you can expand Advanced options for paragraphs and other elements that contain text and use the Text Direction setting to control for right-to-left or left-to-right text. Mixing LTR and RTL text When creating messages that mix right-to-left (RTL) languages like Arabic with left-to-right (LTR) languages like English in our email and in-app message editors, you may need to control the text direction to ensure proper display. You can control text direction inline using Unicode directional formatting characters: RLE (Right-to-Left Embedding): &#x202B; - Place before Arabic text to embed it in a right-to-left context. PDF (Pop Directional Formatting): &#x202C; - Place after Arabic text to return to the previous directional context. Welcome to Customer.io! &#x202B;‫مرحبا بك في كستمر.آي.أو‬&#x202C; Let's get started. Delete a language When composing a message, you can remove languages that you add by mistake or delete them to re-add new versions. Click the tab for the language you want to remove. In Design Studio, this is a dropdown menu. Click . Confirm the action. If some people have this language as their preference, they will receive the default message moving forward. Supported languages and locales You must format the values of your language attribute as two-letter language codes with an optional two-letter region code separated by a dash, like en or en-US. Language attribute values are not case sensitive, but language-region codes must be separated by a dash. For example, both es-MX and es-mx represent Spanish formatted for speakers in Mexico. These codes come from the ISO-3166-1 (alpha 2) and IETF standards respectively. If you’re looking for a language or locale that we don’t support, let us know! Any code with an asterisk (*) is not officially supported by our auto-translate feature. You’ll have to use another service to get an accurate translation. Code Language/Locale af Afrikaans af-ZA Afrikaans (South Africa) am-ET Amharic (Ethiopia) ar Arabic ar-AE Arabic (U.A.E.) ar-BH Arabic (Bahrain) ar-DZ Arabic (Algeria) ar-EG Arabic (Egypt) ar-IQ Arabic (Iraq) ar-JO Arabic (Jordan) ar-KW Arabic (Kuwait) ar-LB Arabic (Lebanon) ar-LY Arabic (Libya) ar-MA Arabic (Morocco) ar-OM Arabic (Oman) ar-QA Arabic (Qatar) ar-SA Arabic (Saudi Arabia) ar-SY Arabic (Syria) ar-TN Arabic (Tunisia) ar-YE Arabic (Yemen) arn-CL* Mapudungun (Chile) as-IN Assamese (India) az Azeri az-Cyrl-AZ Azeri (Cyrillic) (Azerbaijan) az-Latn-AZ Azeri (Latin) (Azerbaijan) ba-RU* Bashkir (Russia) be Belarusian be-BY Belarusian (Belarus) bg Bulgarian bg-BG Bulgarian (Bulgaria) bn-BD Bengali (Bangladesh) bn-IN Bengali (India) bo-CN* Tibetan (Peoples Republic of China) bi* Bislama br-FR* Breton (France) bs-Cyrl-BA Bosnian (Cyrillic) (Bosnia and Herzegovina) bs-Latn-BA Bosnian (Latin) (Bosnia and Herzegovina) ca Catalan ca-ES Catalan (Catalan) co-FR Corsican (France) cs Czech cs-CZ Czech (Czech Republic) cy-GB Welsh (United Kingdom) da Danish da-DK Danish (Denmark) de German de-AT German (Austria) de-CH German (Switzerland) de-DE German (Germany) de-LI German (Liechtenstein) de-LU German (Luxembourg) dsb-DE* Lower Sorbian (Germany) dv Divehi dv-MV Divehi (Maldives) el Greek el-GR Greek (Greece) en English en-029 English (Caribbean) en-AT English (Austria) en-AU English (Australia) en-BE English (Belgium) en-BZ English (Belize) en-CA English (Canada) en-DE English (Germany) en-ES English (Spain) en-FR English (France) en-GB English (United Kingdom) en-GH English (Ghana) en-IE English (Ireland) en-IN English (India) en-JM English (Jamaica) en-LU English (Luxembourg) en-MY English (Malaysia) en-NL English (Netherlands) en-NZ English (New Zealand) en-PH English (Republic of the Philippines) en-SG English (Singapore) en-TT English (Trinidad and Tobago) en-US English (United States) en-ZA English (South Africa) en-ZW English (Zimbabwe) es Spanish es-AR Spanish (Argentina) es-BO Spanish (Bolivia) es-CL Spanish (Chile) es-CO Spanish (Colombia) es-CR Spanish (Costa Rica) es-DO Spanish (Dominican Republic) es-EC Spanish (Ecuador) es-ES Spanish (Spain) es-GT Spanish (Guatemala) es-HN Spanish (Honduras) es-LA Spanish (Latin America) es-MX Spanish (Mexico) es-NI Spanish (Nicaragua) es-PA Spanish (Panama) es-PE Spanish (Peru) es-PR Spanish (Puerto Rico) es-PY Spanish (Paraguay) es-SV Spanish (El Salvador) es-US Spanish (United States) es-UY Spanish (Uruguay) es-VE Spanish (Venezuela) et Estonian et-EE Estonian (Estonia) eu Basque eu-ES Basque (Basque) fa Persian fa-IR Persian (Iran) fi Finnish fi-FI Finnish (Finland) fil-PH Filipino (Philippines) fo* Faroese fo-FO* Faroese (Faroe Islands) fr French fr-BE French (Belgium) fr-CA French (Canada) fr-CH French (Switzerland) fr-FR French (France) fr-LU French (Luxembourg) fr-MC French (Principality of Monaco) fy-NL Frisian (Netherlands) ga-IE Irish (Ireland) gd-GB Scottish Gaelic (United Kingdom) gl Galician gl-ES Galician (Galician) gsw-FR* Alsatian (France) gu Gujarati gu-IN Gujarati (India) ha-Latn-NG Hausa (Latin) (Nigeria) he Hebrew he-IL Hebrew (Israel) hi Hindi hi-IN Hindi (India) hr Croatian hr-BA Croatian (Latin) (Bosnia and Herzegovina) hr-HR Croatian (Croatia) hsb-DE* Upper Sorbian (Germany) hu Hungarian hu-HU Hungarian (Hungary) hy Armenian hy-AM Armenian (Armenia) id Indonesian id-ID Indonesian (Indonesia) ig-NG Igbo (Nigeria) ii-CN* Yi (Peoples Republic of China) is Icelandic is-IS Icelandic (Iceland) it Italian it-CH Italian (Switzerland) it-IT Italian (Italy) iu-Cans-CA* Inuktitut (Syllabics) (Canada) iu-Latn-CA* Inuktitut (Latin) (Canada) ja Japanese ja-JP Japanese (Japan) ka Georgian ka-GE Georgian (Georgia) kk Kazakh kk-KZ Kazakh (Kazakhstan) kl-GL* Greenlandic (Greenland) km-KH Khmer (Cambodia) kn Kannada kn-IN Kannada (India) ko Korean ko-KR Korean (Korea) kok* Konkani kok-IN* Konkani (India) ky Kyrgyz ky-KG Kyrgyz (Kyrgyzstan) la Latin lb-LU Luxembourgish (Luxembourg) lo-LA Lao (Lao P.D.R.) lt Lithuanian lt-LT Lithuanian (Lithuania) lv Latvian lv-LV Latvian (Latvia) mi-NZ Maori (New Zealand) mk Macedonian mk-MK Macedonian (Former Yugoslav Republic of Macedonia) ml-IN Malayalam (India) mn Mongolian mn-MN Mongolian (Cyrillic) (Mongolia) mn-Mong-CN Mongolian (Traditional Mongolian) (Peoples Republic of China) moh-CA* Mohawk (Canada) mr Marathi mr-IN Marathi (India) ms Malay ms-BN Malay (Brunei Darussalam) ms-MY Malay (Malaysia) mt-MT Maltese (Malta) nb* Norwegian (Bokmål) nb-NO* Norwegian, Bokmål (Norway) ne-NP Nepali (Nepal) nl Dutch nl-BE Dutch (Belgium) nl-NL Dutch (Netherlands) nn-NO* Norwegian, Nynorsk (Norway) no Norwegian nso-ZA* Sesotho sa Leboa (South Africa) oc-FR* Occitan (France) or-IN Oriya (India) pa Punjabi pa-IN Punjabi (India) pl Polish pl-PL Polish (Poland) prs-AF* Dari (Afghanistan) ps-AF Pashto (Afghanistan) pt Portuguese pt-BR Portuguese (Brazil) pt-PT Portuguese (Portugal) qut-GT* Kiche (Guatemala) quz-BO* Quechua (Bolivia) quz-EC* Quechua (Ecuador) quz-PE* Quechua (Peru) rm-CH* Romansh (Switzerland) ro Romanian ro-RO Romanian (Romania) ru Russian ru-RU Russian (Russia) rw-RW* Kinyarwanda (Rwanda) sa* Sanskrit sa-IN* Sanskrit (India) sah-RU* Yakut (Russia) se* Sami (Northern) se-FI* Sami (Northern) (Finland) se-NO* Sami (Northern) (Norway) se-SE* Sami (Northern) (Sweden) si-LK Sinhala (Sri Lanka) sk Slovak sk-SK Slovak (Slovakia) sl Slovenian sl-SI Slovenian (Slovenia) sm Samoan sma-NO* Sami (Southern) (Norway) sma-SE* Sami (Southern) (Sweden) smj-NO* Sami (Lule) (Norway) smj-SE* Sami (Lule) (Sweden) smn-FI* Sami (Inari) (Finland) sms-FI* Sami (Skolt) (Finland) so Somali so-DJ Somali (Djibouti) so-ET Somali (Ethiopia) so-KE Somali (Kenya) so-SO Somali (Somalia) sq Albanian sq-AL Albanian (Albania) sr Serbian sr-Cyrl-BA Serbian (Cyrillic) (Bosnia and Herzegovina) sr-Cyrl-CS Serbian (Cyrillic) (Serbia and Montenegro (Former)) sr-Cyrl-ME Serbian (Cyrillic) (Montenegro) sr-Cyrl-RS Serbian (Cyrillic) (Serbia) sr-Latn-BA Serbian (Latin) (Bosnia and Herzegovina) sr-Latn-CS Serbian (Latin) (Serbia and Montenegro (Former)) sr-Latn-ME Serbian (Latin) (Montenegro) sr-Latn-RS Serbian (Latin) (Serbia) sv Swedish sv-FI Swedish (Finland) sv-SE Swedish (Sweden) sw Kiswahili sw-KE Kiswahili (Kenya) syr* Syriac syr-SY* Syriac (Syria) ta Tamil ta-IN Tamil (India) te Telugu te-IN Telugu (India) tg-Cyrl-TJ Tajik (Cyrillic) (Tajikistan) th Thai to* Tonga (Tonga Islands) th-TH Thai (Thailand) tk-TM* Turkmen (Turkmenistan) tn-ZA* Setswana (South Africa) tr Turkish tr-TR Turkish (Turkey) tt* Tatar tt-RU* Tatar (Russia) tzm-Latn-DZ* Tamazight (Latin) (Algeria) ug-CN Uyghur (Peoples Republic of China) uk Ukrainian uk-UA Ukrainian (Ukraine) ur Urdu ur-PK Urdu (Islamic Republic of Pakistan) uz Uzbek uz-Cyrl-UZ Uzbek (Cyrillic) (Uzbekistan) uz-Latn-UZ Uzbek (Latin) (Uzbekistan) vi Vietnamese vi-VN Vietnamese (Vietnam) wo-SN* Wolof (Senegal) xh-ZA isiXhosa (South Africa) yo-NG Yoruba (Nigeria) zh Chinese zh-CHS Chinese (Simplified) zh-Hans Chinese (Simplified) zh-CHT Chinese (Traditional) zh-Hant Chinese (Traditional) zh-CN Chinese (Peoples Republic of China) zh-HK Chinese (Hong Kong S.A.R.) zh-MO Chinese (Macao S.A.R.) zh-SG Chinese (Singapore) zh-TW Chinese (Taiwan) zu-ZA isiZulu (South Africa) --- ## Set up your localization attribute URL: https://docs.customer.io/journeys/localization-attribute/ Before you can set up multi-language messages, you need to store your audience's language preferences as an attribute. Then you'll assign this in your language settings so we send the right translation to your recipients. How it works To set up your workspace to send translated messages, you’ll follow these steps: Store your audience’s language preferences as an attribute on their profiles. Assign this attribute in your language settings. You’ll indicate which attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. contains your audience’s language preferences in Workspace Settings > Language settings. This attribute must store values we support, meaning they’re either: A two-letter language code, like en for English A four-letter language and region code, separated by a dash, like en-US for English speakers in the United States We’ll match your customers’ attribute values to the corresponding translations of your messages. If a person’s language attribute doesn’t match one of the languages in your message, they’ll receive the Default message.  Localization matching is strict We don’t resolve or “fall back” from four-letter locale codes (like nl-BE) to two-letter codes (like nl). If a person’s localization attribute doesn’t match a language in your message exactly, they’ll receive the Default message. Make sure that you set your attributes exactly as you plan to use them in your messages. flowchart LR C[Multi-language message] --> D{Does a person's language attribute match a message?} D -->|no| H[Person gets Default message] D -->|yes, lang=es| E[Person gets Spanish message] D -->|yes, lang=fr| F[Person gets French message] D -->|yes, lang=de| G[Person gets German message] Set your language attribute To start translating messages, you’ll need to define how your workspace identifies people’s language preferences. To set your workspace’s language setting: Go to Settings > Workspace Settings. Find Language settings and click Get Started. Enter the name of the attribute representing your audience’s language. Remember you must store values in a certain format. If you store languages that don’t follow our standard, you can convert them with 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}}. or JavaScript. Convert language attributes to our standards If you already store your audience’s language preferences in a format that doesn’t fit ISO-3166-1 (alpha 2) or IETF standards, you can set up a campaign to reformat your language attribute. If you can, it’s best to also update your integrations so they send the correct format moving forward. Below is an example of a campaign you could create to reformat a language attribute with underscores instead of dashes between the language and region. In the workflow, you create a new attribute to store the reformatted preference, in case integrations continue to add or update people with a language preference that you need to reformat. Create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. containing people with your original language attribute. Create a campaign. Click the trigger on the canvas, and click Attribute or Segment as the trigger type. Choose the segment you just created from the dropdown. In your Workflow, drag Create or update person onto the canvas. Give the action a Name, then click Add Details. Click Add attribute. Type the name of the language attribute you set in Language settings. Select Liquid or JavaScript, and write an expression to convert or map languages. We’ve provided some examples below. Under Sample Data, click next to the original attribute. Set the value to Remove attribute. This removes the original language attribute and removes the person from the segment you set up in the first step. Use liquid to convert language values When you use the Create or Update Person action, you can set and modify attributes using 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}}.. The liquid expression you use to map your old language attribute to your new one depends on your current language format, but we’ve provided some ideas below. If you store languages with an underscore instead of a dash (e.g. en_US instead of en-US), you can replace the underscore easily with replace: // assume input en_us {{ customer.language | replace: "_", "-" }} // outputs en-us If you store languages without a delimiter, you can add one by slicing your original language attribute: // assume input enUS {{ customer.language | slice: 2 }}-{{customer.language | slice: 3, 2}} // output Or, if you store languages as full names, you may need to map some of them. You can do this with condtitions: // assume languages are english, french, and german {% if customer.language == "German" %} de // output for german {% else %} {{customer.language | slice: 2, 2}} // for french and english, use the first two letters of language code {% end %} Use JavaScript to convert language values When you use the Create or Update Person action, you can set and modify attributes using JavaScript. The liquid expression you use to map your old language attribute to your new one depends on your current language format, but we’ve provided some ideas below. If you store languages with an underscore instead of a dash (e.g. en_US instead of en-US), you can replace the underscore easily with replace: // assume `language` attribute formatted en_us return customer.language.replace("_", "-"); // outputs en-us If you store languages without a delimiter, you can add one by slicing your original language attribute: // assume `language` attribute formatted enUS return customer.language.slice(0, 2) + "-" + customer.language.slice(2); // output Or, if you store languages as full names, you may need to map some of them. You can do this with condtitions: // assume languages are english, french, and german if (customer.language == "German") { return de; // output for german } else { customer.language.slice(0, 2); // for french and english, use the first two letters of language code } Supported languages and locales You must format the values of your language attribute as two-letter language codes with an optional two-letter region code separated by a dash, like en or en-US. Language attribute values are not case sensitive, but language-region codes must be separated by a dash. For example, both es-MX and es-mx represent Spanish formatted for speakers in Mexico. These codes come from the ISO-3166-1 (alpha 2) and IETF standards respectively. If you’re looking for a language or locale that we don’t support, let us know! Any code with an asterisk (*) is not officially supported by our auto-translate feature. You’ll have to use another service to get an accurate translation. Code Language/Locale af Afrikaans af-ZA Afrikaans (South Africa) am-ET Amharic (Ethiopia) ar Arabic ar-AE Arabic (U.A.E.) ar-BH Arabic (Bahrain) ar-DZ Arabic (Algeria) ar-EG Arabic (Egypt) ar-IQ Arabic (Iraq) ar-JO Arabic (Jordan) ar-KW Arabic (Kuwait) ar-LB Arabic (Lebanon) ar-LY Arabic (Libya) ar-MA Arabic (Morocco) ar-OM Arabic (Oman) ar-QA Arabic (Qatar) ar-SA Arabic (Saudi Arabia) ar-SY Arabic (Syria) ar-TN Arabic (Tunisia) ar-YE Arabic (Yemen) arn-CL* Mapudungun (Chile) as-IN Assamese (India) az Azeri az-Cyrl-AZ Azeri (Cyrillic) (Azerbaijan) az-Latn-AZ Azeri (Latin) (Azerbaijan) ba-RU* Bashkir (Russia) be Belarusian be-BY Belarusian (Belarus) bg Bulgarian bg-BG Bulgarian (Bulgaria) bn-BD Bengali (Bangladesh) bn-IN Bengali (India) bo-CN* Tibetan (Peoples Republic of China) bi* Bislama br-FR* Breton (France) bs-Cyrl-BA Bosnian (Cyrillic) (Bosnia and Herzegovina) bs-Latn-BA Bosnian (Latin) (Bosnia and Herzegovina) ca Catalan ca-ES Catalan (Catalan) co-FR Corsican (France) cs Czech cs-CZ Czech (Czech Republic) cy-GB Welsh (United Kingdom) da Danish da-DK Danish (Denmark) de German de-AT German (Austria) de-CH German (Switzerland) de-DE German (Germany) de-LI German (Liechtenstein) de-LU German (Luxembourg) dsb-DE* Lower Sorbian (Germany) dv Divehi dv-MV Divehi (Maldives) el Greek el-GR Greek (Greece) en English en-029 English (Caribbean) en-AT English (Austria) en-AU English (Australia) en-BE English (Belgium) en-BZ English (Belize) en-CA English (Canada) en-DE English (Germany) en-ES English (Spain) en-FR English (France) en-GB English (United Kingdom) en-GH English (Ghana) en-IE English (Ireland) en-IN English (India) en-JM English (Jamaica) en-LU English (Luxembourg) en-MY English (Malaysia) en-NL English (Netherlands) en-NZ English (New Zealand) en-PH English (Republic of the Philippines) en-SG English (Singapore) en-TT English (Trinidad and Tobago) en-US English (United States) en-ZA English (South Africa) en-ZW English (Zimbabwe) es Spanish es-AR Spanish (Argentina) es-BO Spanish (Bolivia) es-CL Spanish (Chile) es-CO Spanish (Colombia) es-CR Spanish (Costa Rica) es-DO Spanish (Dominican Republic) es-EC Spanish (Ecuador) es-ES Spanish (Spain) es-GT Spanish (Guatemala) es-HN Spanish (Honduras) es-LA Spanish (Latin America) es-MX Spanish (Mexico) es-NI Spanish (Nicaragua) es-PA Spanish (Panama) es-PE Spanish (Peru) es-PR Spanish (Puerto Rico) es-PY Spanish (Paraguay) es-SV Spanish (El Salvador) es-US Spanish (United States) es-UY Spanish (Uruguay) es-VE Spanish (Venezuela) et Estonian et-EE Estonian (Estonia) eu Basque eu-ES Basque (Basque) fa Persian fa-IR Persian (Iran) fi Finnish fi-FI Finnish (Finland) fil-PH Filipino (Philippines) fo* Faroese fo-FO* Faroese (Faroe Islands) fr French fr-BE French (Belgium) fr-CA French (Canada) fr-CH French (Switzerland) fr-FR French (France) fr-LU French (Luxembourg) fr-MC French (Principality of Monaco) fy-NL Frisian (Netherlands) ga-IE Irish (Ireland) gd-GB Scottish Gaelic (United Kingdom) gl Galician gl-ES Galician (Galician) gsw-FR* Alsatian (France) gu Gujarati gu-IN Gujarati (India) ha-Latn-NG Hausa (Latin) (Nigeria) he Hebrew he-IL Hebrew (Israel) hi Hindi hi-IN Hindi (India) hr Croatian hr-BA Croatian (Latin) (Bosnia and Herzegovina) hr-HR Croatian (Croatia) hsb-DE* Upper Sorbian (Germany) hu Hungarian hu-HU Hungarian (Hungary) hy Armenian hy-AM Armenian (Armenia) id Indonesian id-ID Indonesian (Indonesia) ig-NG Igbo (Nigeria) ii-CN* Yi (Peoples Republic of China) is Icelandic is-IS Icelandic (Iceland) it Italian it-CH Italian (Switzerland) it-IT Italian (Italy) iu-Cans-CA* Inuktitut (Syllabics) (Canada) iu-Latn-CA* Inuktitut (Latin) (Canada) ja Japanese ja-JP Japanese (Japan) ka Georgian ka-GE Georgian (Georgia) kk Kazakh kk-KZ Kazakh (Kazakhstan) kl-GL* Greenlandic (Greenland) km-KH Khmer (Cambodia) kn Kannada kn-IN Kannada (India) ko Korean ko-KR Korean (Korea) kok* Konkani kok-IN* Konkani (India) ky Kyrgyz ky-KG Kyrgyz (Kyrgyzstan) la Latin lb-LU Luxembourgish (Luxembourg) lo-LA Lao (Lao P.D.R.) lt Lithuanian lt-LT Lithuanian (Lithuania) lv Latvian lv-LV Latvian (Latvia) mi-NZ Maori (New Zealand) mk Macedonian mk-MK Macedonian (Former Yugoslav Republic of Macedonia) ml-IN Malayalam (India) mn Mongolian mn-MN Mongolian (Cyrillic) (Mongolia) mn-Mong-CN Mongolian (Traditional Mongolian) (Peoples Republic of China) moh-CA* Mohawk (Canada) mr Marathi mr-IN Marathi (India) ms Malay ms-BN Malay (Brunei Darussalam) ms-MY Malay (Malaysia) mt-MT Maltese (Malta) nb* Norwegian (Bokmål) nb-NO* Norwegian, Bokmål (Norway) ne-NP Nepali (Nepal) nl Dutch nl-BE Dutch (Belgium) nl-NL Dutch (Netherlands) nn-NO* Norwegian, Nynorsk (Norway) no Norwegian nso-ZA* Sesotho sa Leboa (South Africa) oc-FR* Occitan (France) or-IN Oriya (India) pa Punjabi pa-IN Punjabi (India) pl Polish pl-PL Polish (Poland) prs-AF* Dari (Afghanistan) ps-AF Pashto (Afghanistan) pt Portuguese pt-BR Portuguese (Brazil) pt-PT Portuguese (Portugal) qut-GT* Kiche (Guatemala) quz-BO* Quechua (Bolivia) quz-EC* Quechua (Ecuador) quz-PE* Quechua (Peru) rm-CH* Romansh (Switzerland) ro Romanian ro-RO Romanian (Romania) ru Russian ru-RU Russian (Russia) rw-RW* Kinyarwanda (Rwanda) sa* Sanskrit sa-IN* Sanskrit (India) sah-RU* Yakut (Russia) se* Sami (Northern) se-FI* Sami (Northern) (Finland) se-NO* Sami (Northern) (Norway) se-SE* Sami (Northern) (Sweden) si-LK Sinhala (Sri Lanka) sk Slovak sk-SK Slovak (Slovakia) sl Slovenian sl-SI Slovenian (Slovenia) sm Samoan sma-NO* Sami (Southern) (Norway) sma-SE* Sami (Southern) (Sweden) smj-NO* Sami (Lule) (Norway) smj-SE* Sami (Lule) (Sweden) smn-FI* Sami (Inari) (Finland) sms-FI* Sami (Skolt) (Finland) so Somali so-DJ Somali (Djibouti) so-ET Somali (Ethiopia) so-KE Somali (Kenya) so-SO Somali (Somalia) sq Albanian sq-AL Albanian (Albania) sr Serbian sr-Cyrl-BA Serbian (Cyrillic) (Bosnia and Herzegovina) sr-Cyrl-CS Serbian (Cyrillic) (Serbia and Montenegro (Former)) sr-Cyrl-ME Serbian (Cyrillic) (Montenegro) sr-Cyrl-RS Serbian (Cyrillic) (Serbia) sr-Latn-BA Serbian (Latin) (Bosnia and Herzegovina) sr-Latn-CS Serbian (Latin) (Serbia and Montenegro (Former)) sr-Latn-ME Serbian (Latin) (Montenegro) sr-Latn-RS Serbian (Latin) (Serbia) sv Swedish sv-FI Swedish (Finland) sv-SE Swedish (Sweden) sw Kiswahili sw-KE Kiswahili (Kenya) syr* Syriac syr-SY* Syriac (Syria) ta Tamil ta-IN Tamil (India) te Telugu te-IN Telugu (India) tg-Cyrl-TJ Tajik (Cyrillic) (Tajikistan) th Thai to* Tonga (Tonga Islands) th-TH Thai (Thailand) tk-TM* Turkmen (Turkmenistan) tn-ZA* Setswana (South Africa) tr Turkish tr-TR Turkish (Turkey) tt* Tatar tt-RU* Tatar (Russia) tzm-Latn-DZ* Tamazight (Latin) (Algeria) ug-CN Uyghur (Peoples Republic of China) uk Ukrainian uk-UA Ukrainian (Ukraine) ur Urdu ur-PK Urdu (Islamic Republic of Pakistan) uz Uzbek uz-Cyrl-UZ Uzbek (Cyrillic) (Uzbekistan) uz-Latn-UZ Uzbek (Latin) (Uzbekistan) vi Vietnamese vi-VN Vietnamese (Vietnam) wo-SN* Wolof (Senegal) xh-ZA isiXhosa (South Africa) yo-NG Yoruba (Nigeria) zh Chinese zh-CHS Chinese (Simplified) zh-Hans Chinese (Simplified) zh-CHT Chinese (Traditional) zh-Hant Chinese (Traditional) zh-CN Chinese (Peoples Republic of China) zh-HK Chinese (Hong Kong S.A.R.) zh-MO Chinese (Macao S.A.R.) zh-SG Chinese (Singapore) zh-TW Chinese (Taiwan) zu-ZA isiZulu (South Africa) --- ## Track metrics for translations URL: https://docs.customer.io/journeys/localization-metrics/ You can track message performance for each translation from your metric dashboards. To learn more about metrics, check out Campaign and newsletter metrics. Campaign metrics When you look at a running campaign, the Overview and Metrics tabs both show aggregated metrics for your messages, including all the languages they contain. The Metrics tab shows stats for each language under the name of the message block. Beside the message block name, you’ll see metrics aggregated across all languages. Newsletter metrics In the Overview tab of a newsletter, you’ll see metrics for each language. --- ## Link Tracking URL: https://docs.customer.io/journeys/link-tracking/ When link tracking is enabled, we track clicks to help you understand how people are engaging with your messages in campaigns, broadcasts, and transactional use cases. We track the number and percentage of recipients who clicked links in your messages: We also track which links they clicked: You can view clicked links on a person’s profile through the Activity tab by expanding a Clicked action entry:  Your links must include the protocol—HTTP or HTTPS We can’t track links if you don’t include the protocol. To generate secure HTTPS links, visit HTTPS Link Tracking. Emails For campaigns and API-triggered broadcasts, link tracking is enabled for emails in your workflows by default. While editing the workflow, tap the email you want to edit and a panel of options will appear on the left: For newsletters, you can enable/disable link tracking for the whole message on the Content tab during setup: Tracking personalized links in transactional emails To track personalized links in transactional messages, you must populate your links using liquid. Use attributes or trigger properties—like {{trigger.custom_url}}—to enter custom URLs. If you provide custom URLs directly in the body or subject of a transactional /send/email request, you should use data-cio-tag to group links together for reporting purposes: <a href="http://mydomain.com?token=123abc" data-cio-tag="YOUR-LINK-GROUP-NAME">CLICK HERE</a> If you enter fully personalized URLs (without liquid) in the body or subject of a transactional API request and enable link tracking, Customer.io will track a new link for each transactional message you send, cluttering link tracking metrics. Tracking links in other message types By default, link tracking is enabled for emails, SMS, and WhatsApp messages. It is not enabled for other channels by default. In the case of push and in-app notifications, this is because our SDKs report clicks back to Customer.io. Wherever link tracking is disabled, or whenever you want to track mailto links, you can use the 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}}. cio_link tag in the format{% cio_link url:https://example.com %} Beyond the url field, you can also add track and url_params to your link. By default, both are true: A true value for track means you would like Customer.io to track this particular link. A true value for url_params indicates you would like Customer.io to add your URL parameters to this particular link. (Workspace-wide URL parameters are configured in your workspace settings.) {% cio_link url:https://example.com track:true/false url_params:true/false %} This would become: http://e.customeriomail.com/e/c/eyJlbWFpb29pZCI6IlJLS2hBZ01BQVhKaVBtS2pKeGtWWllCME21N0Qzdz09IiwiaHJlZiI6Imh0dHBzOi8vZmFucy5jb20vaG91c2Utc7VsZXMvIi7ibGlua19pZCI7NTkwMTE5NzMsInBvc7l0779uIjoyMH0/e72cf17efa292d48f56d65535b3f7c45fe75d2365bf18caa1aebcaf6a2632c66  Enable link shortening for SMS and WhatsApp messages SMS and WhatsApp messages have character limits, so you should enable link shortening to make sure that your links fit in your messages. URL parameters can get in the way of links to telephone numbers. Learn more about disabling URL parameters for phone numbers. Tracking links in WhatsApp messages To track links in WhatsApp messages: Links must be in variable fields: You cannot add links directly to messages. Instead, you must create a variable field for your WhatsApp template and add the link when you compose your message. Use the cio_link liquid tag: When adding links in variable fields, you must use the cio_link liquid tag format: {% cio_link url:https://example.com %} We don’t support link tracking for things like Call-to-Action (CTA) buttons. Counting link clicks There are two ways of counting clicks in Customer.io. Which way is better depends on the information you need! Total clicks: For each tracked link in a given campaign or newsletter, you can find this in the “Top Clicked Links” metric in the campaign overview or newsletter report. Total Clicks is the number of times each link has been clicked, which includes multiple clicks on the same link from the same user. This is not a count of unique links.  Want to group links together in reports? You can use the tag data-cio-tag to track links of the same type—like all of your password reset links or all of your links to your users’ dashboards. This helps you gather usable metrics for links that include personalized variables. <a href="http://mydomain.com?token=123abc" data-cio-tag="YOUR-LINK-GROUP-NAME">CLICK HERE</a> Unique clicks: For a given tracked link, create a segment for the link you’d like to track, like this: Then, when you save this segment, you’ll see how many people it contains. This is the number of unique clicks. Automatically identify people who click tracked links By default (for workspaces created after July 12, 2021), Customer.io automatically appends a _cio_id parameter containing a person’s cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). to tracked linksA link in a message that logs when a person clicks it. You can gather metrics for tracked links and use them to determine your audience’s level of engagement.. If your tracked link sends people to a webpage containing our JavaScript library, which we recommend, you’ll need to append links with ajs_uid=cio_{{customer.cio_id}} to automatically identify people. See our JavaScript documentation for more information. If your tracked links send people to a webpage containing our legacy JavaScript snippet, the snippet automatically identifies people. Even if you don’t use our JavaScript Snippet, you can still take advantage of the _cio_id parameter in tracked links to identify people. If you integrate directly with our API or one of our libraries, you can identify people using cio_<_cio_id-param-value> rather than a person’s ID or email address. To change or disable this setting: Go to Workspace Settings > URL Parameters. If you already have URL parameters enabled, click Settings; otherwise, click Get Started. Toggle Add _cio_id URL parameter. This setting affects messages you send after you enable or disable it. It does not affect messages that you’ve already sent. Security and privacy Our tracked links are securely sent over HTTPS by default. But you should not enable link tracking for links that contain secure or private information such as password resets or time-limited downloads. If you want to track links with your custom subdomain using HTTPS, configure HTTPS link tracking.  If HSTS (HTTP Strict Transport Security) is enabled on your domain, you must configure HTTPS Link Tracking or your tracked links will not resolve correctly.  We surface links in activity logs and campaign metrics. This means your URLs are visible in your workspace, so to keep these private, we strongly recommend disabling link tracking for sensitive links. Disable tracking for specific links If you’d like to prevent tracking for specific links within emails, you’ll want to add class="untracked" to the anchor element, like this: <a href="http://mydomain.com" class="untracked">CLICK HERE</a> Design Studio Within the visual editor of Design Studio, you can disable link tracking for specific links by turning off the Track link toggle. Drag-and-drop editor If you’re using the drag-and-drop editor, you can remove tracking on specific text links by highlighting the text and then clicking Edit Link. From there you can add the untracked class under the Custom Attributes section. You can also disable link tracking on Buttons and Images in the drag-and-drop editor by clicking on the content block and then scrolling down to the Attributes section of the Content Properties menu. There you can add a class attribute with a value of untracked.  Need to disable link tracking and URL parameters on the same link? You can add both the disable-url-params and untracked classes to the same class attribute in the drag-and-drop editor. Just be sure to leave a space between the two class names. See the URL parameters page for help disabling URL parameters. Disable link tracking for individual messages If you’d like to disable tracking for all links in an email, SMS, or WhatsApp message, go to the workflow in your campaign or api-triggered broadcast. Click the email you’d like to disable tracking for and uncheck “Track opens and link clicks in this message” on the left. This will disable open and click tracking for that particular email only. If you want to disable tracking for a whole campaign, you will have to disable it in each email. You can also disable open tracking for all messages sent from your workspace. This prevents emails from tracking opens, even if the Track opens and link clicks in this message setting is enabled at the message level. Disable link tracking for a newsletter You can disable link tracking in a newsletter through the Content tab during setup: --- ## Checking Link Status URL: https://docs.customer.io/journeys/link-checking/ If you've got a lot of links in your message, you may need to make sure that none of them are broken without clicking each one individually! Here's how to check link health, and get all the link information you need in your messages. Checking Link Status in the Composer When composing your message, you may notice the “Review Links” button: When you click it, you’ll see a modal with all of your link information. You’ll see your link text, the URL, whether it is tracked or untracked, its UTM tags, and any errors associated with the link.  Note: If you use a link protocol other than http, https, ftp, mailto or tel you may see a message stating that your protocol is not supported. This is common with deep/universal links and just means that the link checker will not attempt to validate those links. If link tracking is enabled for your links, we will still be able to track links with alternate protocols. To check if your links are working as you expect, click the “Check Links” button at the bottom: At this point, you’ll see new information in the ‘Status’ column about how your links are behaving, and whether or not they’re healthy! Interpreting Status Codes A few general notes here: Any statuses starting with 2 and 3 are marked successful. A status of 301, for example, means that the link is still valid but has simply been redirected. In these 3xx cases, you may want to make sure that the redirect still goes where you want it to! Anything beginning with 4 and 5 will be marked unsuccessful. A 404 error means that that link has not been found, for example, and any 5xx code means that the server is incapable of performing the link request. In both of these 4xx and 5xx cases, you’ll want to troubleshoot your link! 999 error: Some sites (like LinkedIn) block automated requests from our link checker and return this (invalid server) error. If you see this error code, you can copy the link into your browser to check it; if your link behaves properly in your browser, you can ignore this error. If you see a status code you don’t recognize, there’s a far more comprehensive and detailed list from the W3C here that describes each one and its specific behavior. --- ## Adding URL parameters to links URL: https://docs.customer.io/journeys/url-parameters/ Adding URL parameters to your links enables you to better track and analyze customers' interactions with the messages you send from Customer.io. You can set up templated URL parameters and automatically add them to links in your emails. How it works While Customer.io supports link tracking, you might want to add URL parameters to links to track clicks from specific campaigns, messages, or other factors. You can manually add URL parameters to links in messages, but this can be tedious and error-prone. You probably have a repeatable set of parameters that you want to use for links in different messages. That’s where our URL parameters feature comes in. It makes it easy to automatically append a templated set of URL parameters to links in your messages. email: When you enable URL parameters, we automatically append them to all links in your emails. You don’t need to use the cio_link 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}}. tag. SMS and WhatsApp: URL parameters aren’t automatically appended. You must use the cio_link tag to add parameters to links in these messages. Push and in-app: Your SDK integration handles link tracking. You can use cio_link if you want to add URL parameters to links in these messages. {% cio_link url:"https://example.com" %} Enable URL parameters Go to Workspace Settings > URL Parameters to enable and set up URL parameters. When you enable URL parameters, they’re automatically appended to all links in your emails. For SMS and WhatsApp messages, you must use the cio_link liquid tag to add URL parameters to links. Default parameters Our default URL parameters are commonly used properties in analytics platforms like Google Analytics. The values help you tie clicks back to the messages and campaigns your audience interacts with. Parameter Default value Description utm_source customer.io The source of the link. utm_medium {{message.type}} The source “medium” for the link the user clicked, like email_action. utm_campaign {{campaign.name}} The campaign the message (and link) belonged to. utm_content {{message.name}} The name of the message that contained the link.  Disable campaign.name if you use URL parameters outside of campaigns Empty liquid variables aren’t valid, if you leave the utm_campaign set to campaign.name and try to use cio_link to include URL parameters on links in a broadcast, newsletter, or transactional message, your message will fail. Default URL parameters in action For example, imagine you have a campaign and message with the following details. Campaign name: Retention Campaign 1 [Marketing] [Internal] Email name: Hello World Email With our default URL parameters, you’d add a link like this: {% cio_link url:"https://example.com" %}. And we automatically append the URL parameters in your rendered email, like this: https://example.com/?utm_content=Hello+World+Email&utm_medium=email_action&utm_name=Retention+Campaign+1+%5BMarketing%5D+%5BInternal%5D&utm_source=customer.io Using URL parameters in SMS and WhatsApp messages URL parameters aren’t automatically appended to links in SMS and WhatsApp messages. To add URL parameters in these channels, you must wrap your links in the cio_link tag: {% cio_link url:"https://example.com" %} URL parameters can make links significantly longer. If you use URL parameters (or Customer.io’s link tracking feature) in these kinds of messages, make sure that you enable link shortening so that your links fit in your messages. How do I check that my URL parameters work? In an email, you can click Review Links to see your URL parameters in action. Add URL parameters to a link For emails, URL parameters are automatically appended to all links when the feature is enabled—you don’t need to do anything extra. For SMS and WhatsApp messages, use the cio_link tag with the url parameter to add URL parameters to a link: {% cio_link url:"https://example.com" %} You can also use the cio_link tag in emails if you want explicit control over URL parameters on specific links—for example, to disable URL parameters on a particular link. Add or remove URL parameters In your URL parameter settings, click Edit Parameters to add, remove, or edit your URL parameters. Any changes you make apply to links using the cio_link tag immediately.  You can shorten long campaign or newsletter names You can use the Liquid truncate filter to shorten long values in links, like this: {{ campaign.name | truncate: 15, "" }}. This example truncates your campaign name to 15 characters. Disable URL parameters for a link In emails, URL parameters are appended to all links automatically. To turn off URL parameters for a specific link, use the cio_link tag with url_params:false. {% cio_link url:"https://example.com" url_params:false %} If you use our drag-and-drop editor, you can add the disable-url-params class to any item that acts as a link—links, buttons, images, etc. URL parameters and link tracking Link tracking helps you understand which links your audience clicks and the percentage of your audience that clicks each link. URL parameters can help you track performance and engagement outside of Customer.io—in platforms like Google Analytics, for example. They also give you more control over the data you track—down to the campaign and message level. Liquid in URL parameters Here’s a list of common liquid values you might use in your URL parameters. Tag Output {{layout.id}} The numerical ID associated with the email layout you've used. **Only available for emails made with the code or rich text editors.** {{layout.name}} The name you've assigned to the email layout you've used. **Only available for emails made with the code or rich text editors.** {{campaign.id}} Your campaign's numerical ID. This can be found in your campaign URL. For example, the campaign ID here is 2000: https://fly2.customer.io/env/12345/v2/campaigns/2000/overview {{campaign.name}} This tag will output the name you've given your campaign or newsletter in the Customer.io interface. For example: "Q2 Anvil Onboarding Campaign \\\\\[Coyotes]" {{campaign.type}} This returns whether or not your campaign is behavioral (segment triggered), transactional (event triggered), or a newsletter {{delivery_id}} A URL-compatible base64 string that identifies a specific message created for an end-user. This is generated when the message is drafted or sent but will be set to "unsent" in test messages and composer previews. {{message.id}} The numerical ID associated with a message action in the workflow— i.e. an SMS action might have a {{message.id}} of 200 while every SMS it generates will have a different {{delivery_id}}. {{message.name}} The name you give your message in the workflow. E.g. "Welcome to ACME!" {{message.type}} This refers to a particular message's type. Possible values are: email_action, twilio_action, slack_action, webhook_action, attribute_update_action {{customer.id}} Whatever you're using to uniquely identify your customer—usually numeric*.  Your analytics provider (like Google) may prohibit you from sending personal information or have specific guidelines for tracking; make sure you’re aware of any restrictions. --- ## Track universal links and app links in email URL: https://docs.customer.io/journeys/universal-links/ Deep links take users to content inside your app. Apple's universal links are one type of deep link. Android's app links are another. With universal links and app links, you provide a standard web link and fallback should a user not have your app downloaded. To track them in Customer.io, you must add a special liquid tag, explained below. This article discusses how to set up universal and app links to direct email recipients to a page in your mobile app and track clicks on these links. These instructions typically apply to email You can use universal links and app links in other message channels, but the manual tracking steps we describe in this article are only viable in emails. For in-app messages, our web and mobile SDKs automatically track links, but make sure your app is set up to resolve deep links. For push, use the Opened metric to track links, which is the push equivalent to Clicked. For more information, visit our mobile SDK docs below: iOS universal links React Native - iOS universal links Flutter - iOS universal links Google’s documentation for setting up app links for Android For SMS, Enable link shortening in your workspace rather than using {% cio_link_id %}. Link shortening tracks clicks and shortens URLs to fit SMS character limits. Note that shortened links redirect through Customer.io, so iOS and Android won’t automatically open your app when people tap the link—they’ll land on your website first. Make sure your landing page resolves deep links to route people to your app. Set up universal and app links in email Follow the steps in our SDK docs to set up universal links for iOS, linked above. Follow the steps for Google’s documentation to set up app links for Android. Track universal and app links in email Customer.io wraps tracked links in a URL that points to a domain we control and can use to log your clicks. Once logged, we then redirect people to the URL you originally used for the link. This poses a problem for universal links and app links, however, because your tracked links no longer point to a path you have configured in your app site configuration files. To address this, we created a special liquid tag you can add to your universal and app links—{% cio_link_id %}. To log clicks on your universal and app links within Customer.io emails, you have to: configure all universal and app links as untracked in our system, then pass the clicked links to us programmatically for tracking 1. Configure universal and app links as untracked Per your app site configuration files for iOS and Android, you must format any link that takes users to content inside your app like the link shown in the image below. The link must contain these two components: link_id={% cio_link_id %} This gives us a way to send you the link id as a query parameter value so that you can pass the link to us for tracking. We’ve used cio_link_id in this example, but the parameter name can be anything your system will recognize as the link id token you need to send back to us. class="untracked" This ensures that we don’t wrap this link like we do for conventional links. You may add other classes, if needed, but you must include untracked to ensure that we don’t alter the link. After you send your email, universal and app links point to a URL that looks something like: http://yourwebsite.com/confirm/?link_id=eyJlbWFpbF...928bf (Note, we’ve truncated the link id token in the example because it is normally quite long.) 2. Pass universal and app links for tracking purposes Next, from within your app, retrieve the link_id token from the request URL. Pass it to Customer.io by sending a POST request from within your app to a URL that will look something like this one: https://<your-tracking-domain>/click/<link_id> Replace <link_id> with the value pulled from the link_id parameter. Replace <your-tracking-domain> with the sending domain you use in your workspace’s deliverability settings. You can locate your deliverability settings by going to Workspace Settings > Email > Show Records > Link Tracking. Then click the Configure button on the line for the message’s sending domain (i.e. the domain used in the FROM address of your message). If you configured a CNAME record in your domain’s DNS records as your custom link tracking domain in Customer.io, then you need to replace <your-tracking-domain> above with that domain. For example, if the custom link tracking domain in your settings says email.yourdomain.com AND we have verified your CNAME record for that domain, then you would perform an HTTP POST to URLs that are formatted like: http://email.yourdomain.com/click/eyJlbWFpbF...928bf so that we can log the click. If we have NOT verified your CNAME record for the sending domain, then you will replace <your-tracking-domain> above with e.customeriomail.com instead so that we can log the click. Note that the tracking domain you send this to must match the domain you configured in your deliverability settings discussed above. Each of our link ids are cryptographically signed by Customer.io using your tracking domain for security purposes, so the request must arrive on the same domain for validation. Reporting on universal and app links Assuming the token we receive is valid, we will respond with a 200 OK response and register a click for the associated message. You will see this in your account as a “Clicked Email” event in your activity log. There, the href value will contain (CIO--LINKID) instead of the long value, which indicates it was a universal or app link. You can also look at the “Top Clicked Links” section found on the relevant campaign’s Overview tab. Tracking universal links when the app isn’t installed When a user clicks a universal link but doesn’t have your app installed, the link falls back to opening in their web browser instead. In this scenario, you’ll need a different approach to track the click and resolve the cio_link_id parameter. You’ll either need to handle tracking on your server or in the browser. We’ve provided information about both approaches below. No matter which approach you choose, you should: Track links asynchronously to avoid impacting user experience Handle failures gracefully so failures don’t break your website functionality Test thoroughly across different scenarios (app installed vs. not installed) Consider user privacy and ensure compliance with applicable regulations Server-side tracking Client-side tracking with custom domain Benefits • Avoids CORS restrictions • More reliable than client-side approaches • Doesn’t block the user experience if implemented asynchronously • Handles tracking immediately when the page loads • No server-side infrastructure changes required Considerations • Requires server-side infrastructure to handle the tracking call • If implemented synchronously, network issues could slow down page loads • May still encounter CORS issues depending on your domain setup • Less reliable than server-side approaches • Network requests could impact page performance Option 1: Server-side tracking You can handle tracking on your web server when the linked-page loads: Parse the cio_link_id parameter from the URL query string when your webpage receives the request Make a server-to-server POST request to the Customer.io tracking endpoint: https://<your-tracking-domain>/click/<link_id> This avoids CORS issues since the request originates from your server Use asynchronous processing if possible (e.g., background job queues) to avoid blocking page load while waiting for the API response Option 2: Client-side tracking with custom domain Our JavaScript integrations don’t automatically handle universal-link tracking. Instead, you’ll need to implement your own call to the Customer.io tracking endpoint. Use the following steps to implement client-side tracking and avoid CORS issues: Set up custom link tracking domain following our domain authentication guide Configure CORS headers on your tracking domain to allow requests from your website domain Use JavaScript to parse the cio_link_id and POST to your tracking endpoint Example JavaScript approach: // Parse cio_link_id from URL const urlParams = new URLSearchParams(window.location.search); const linkId = urlParams.get('link_id'); // or whatever parameter name you used if (linkId) { // POST to your custom tracking domain fetch(`https://email.yourdomain.com/click/${linkId}`, { method: 'POST', // Additional headers as needed }).catch(err => { console.log('Link tracking failed:', err); // Handle gracefully - don't impact user experience }); } FAQs Deep links vs. universal and app links Deep links refer to any link that sends a user deeper into your app. Many email clients, including Gmail, automatically strip out app scheme deep links because they aren’t in the limited subset of approved protocols, preventing deep links from working. If you run into this problem, you should use universal or app links instead: Universal links, called app links in Android, let you direct users to your app with a regular web link (like https://yourdomain.com/profile/content) and fallback to your web browser if the user doesn’t have your app installed. This fallback provides an advantage over app scheme deep links (like myapp://profile/33138223345), which only point to your app and don’t provide a fallback for people who haven’t installed your app. Do not use {% cio_link_id %} in other tracked links We don’t support the use of {% cio_link_id %} inside non-universal, tracked links because it causes a circular dependency, where in order to generate the link_id we need to know the full value of the link, but in order to know the full value of the link we need to generate the link_id. --- ## HTTPS Link Tracking URL: https://docs.customer.io/journeys/track-https-links/ By default, [tracked links that use your custom subdomain](/journeys/link-tracking-custom-domain/) as specified with the CNAME record are basic HTTP links. You can enable HTTPS link tracking by configuring your link tracking and we'll automatically generate a valid SSL certificate and proxy links to our link tracking domain for you. How it works We generated a unique URL for any link you track. This link proxies through our link tracking domain and then to the ultimate destination. That’s how we track clicks! But for HTTPS link tracking, we need to use a valid SSL certificate to prove that the proxy is secure—so that your audience, browsers, and any security software understands that the link is safe. In this case, we’ll generate a record you can add to your DNS provider that points to our link tracking domain and we’ll generate a valid SSL certificate for you.  Upgrade to automatic HTTPS link tracking If you set up your domain before October 8, 2025, you may have configured your own SSL certificate and proxy through a service like Cloudflare or NGINX. You can now upgrade to automatic HTTPS link tracking, where we generate and manage your TLS certificate for you. Go to Settings > Workspace Settings > Email, select your domain, and click Upgrade to HTTPS. Then choose how you want to configure your DNS records: Automatic setup (recommended): We’ll configure your CNAME and TXT records automatically with your DNS provider. Manual setup: Add the DNS records to your domain yourself. Set up HTTPS link tracking Go to Settings > Workspace Settings > Email, and select your domain. Select your domain and click the Link Tracking tab. Update the hostname if you want to change the subdomain for tracked links. Copy the CNAME record to your DNS provider. If you use a provider like Cloudflare, make sure that the Proxy status is disabled. We’re going to generate the certificate and proxy the links for you. Return to Customer.io and click Verify domain. Like domain verification, this process can take up to 72 hours depending on your DNS provider but typically happens in minutes. If you see a note that we’re provisioning your certificate when you verify your domain, that means that the domain is verified but it’ll take up to 10 minutes for us to generate the certificate and proxy your links. What happens if I change my link tracking domain? Changing your link tracking domain will cause your existing tracked links to break. If you already configured a link tracking domain, you should also use that domain (like link.example.com) when you enable HTTPS link tracking. Failure to generate a certificate If your domain provider has a Proxy setting enabled for the link tracking domain’s CNAME record, we won’t be able to generate and handle the certificate for your links. Disable this setting and click Verify domain again. If you have CAA records that restrict the certificate authorities that can issue certificates for your domain, you’ll need to update your CAA record to let Google issue certificates for your domain. Allow Google as a certificate authority We issue SSL certificates through Google’s certificate authority. If your domain has CAA (Certificate Authority Authorization) records that restrict which certificate authorities can issue certificates for your domain, you must allow Google to issue certificates. If you have existing CAA records that don’t include Google, you’ll need to add these records to your DNS configuration, replacing YOURDOMAIN.com with your actual domain: YOURDOMAIN.com. IN CAA 0 issue "pki.goog" YOURDOMAIN.com. IN CAA 0 issuewild "pki.goog" Legacy HTTPS link tracking If you added your domain before October 7, 2025, you had to set up your own SSL certificate and proxy through a service like Cloudflare or tool like NGINX. We have instructions for some common services below. Set up HTTPS link tracking with Cloudflare Cloudflare automatically handles TLS certificate generation and proxying for you, making it easy to set up HTTPS link tracking. If your link tracking domain contains more than one subdomain (e.g. a.b.example.com), you’ll need to pay for Cloudflare’s Advanced Certificate Manager, on which you can specify the subdomain you need to cover. Or, if you have a Cloudflare Business or Enterprise plan, you can upload a custom SSL certificate with the required hostnames. When you set up link tracking with Cloudflare, you can set your SSL/TLS settings to either Full or Full (strict). Full mode is more flexible because you can use either e.customeriomail.com or track.customer.io in your CNAME. With Full (strict) mode, you must use track.customer.io because it requires that the certificate common name and alternate name have the same root domain, which would be “customer.io” in this case. See Cloudflare’s documentation for more information about SSL modes. In Cloudflare, go to Websites. If your domain is already present, skip to the next step. Otherwise, click Add Site and set up your Name servers and DNS records as directed by Cloudflare. Go to the DNS page and click Add record. In Customer.io, go to Settings > Workspace Settings > Email, and select your domain. Go to the Link Tracking tab, and copy the CNAME record information to your new record in Cloudflare. Make sure that the Proxy status is enabled (it’ll show Proxied). If your record looks like the image below, click Save. Go to the SSL/TLS tab and make sure that you’re using the Full mode. (Optional) If you want to record repeat opens/clicks, and you have a paid Cloudflare account, you can go to the Caching page and set your time to live (TTL) value to 10 seconds or less (effectively zero), which can help you record repeat opens/clicks. If you’re not on a paid plan, you can’t control your cache’s TTL settings. In your Customer.io Workspace Settings under Email, set up your link-tracking domain if you haven’t already. Enter your domain in the HOST NAME field and click Verify domain to re-validate the domain. You should now pass the HTTPS check and tracking links will use HTTPS by default.  If past messages already have white-labeled tracked links, changing your link tracking domain will cause those existing tracked links to break. When the domain is collapsed/closed, you can tell that HTTPS link tracking is enabled by looking at the LINK TRACKING section pictured below. Once enabled, your tracked links will now start with something like: https://link.example.com… Cloudflare WAF settings Cloudflare has Web Application Firewall (WAF) settings. Depending on the strength of your firewall, it may block our request to verify your domain. If you have problems, you may need to add a rule to your WAF ruleset to make an exception for our user-agent. Go to Security > WAF > Managed Rules. Click Add exception and add the following information: Field: http.user_agent Operator: Matches Value: Customer\.io\/.* Cloudflare Bulk redirects Make sure you don’t redirect requests away from the subdomain that you set up HTTPS link tracking for/on. In your Cloudflare configuration settings, you may need to disable the “include subdomains” option for Bulk Redirects. Setting up HTTPS Link Tracking with Amazon CloudFront Log into AWS and navigate to the AWS Certificate Manager. Import or request a new SSL certificate for the domain you want us to use for your tracked links (e.g. link.example.com).  To satisfy CloudFront… Your SSL certificate: must be in the US East (N. Virginia) Region (us-east-1) cannot be more than 2048-bit RSA (per CloudFront’s limitations must cover the subdomain you are using with us for your tracked links (e.g. link.example.com) If requesting a new certificate through AWS, they will send an email to the appropriate domain owners, requesting them to approve the certificate or you can verify ownership by adding a DNS record. Ensure that the certificate is approved and issued. Navigate to AWS CloudFront. Create a new distribution. Under the Origin section, set the fields as follows: Origin domain: track.customer.io (or track-eu.customer.io depending on your region) Protocol: HTTPS Only Minimum origin SSL protocol: TLSv1.2 Name: track.customer.io (or track-eu.customer.io depending on your region) Under the Default cache behavior section, set the fields as follows: Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE Cache key and origin requests: Legacy cache settings Headers: All Query strings: All Under the Settings section, set the fields as follows: Alternate domain names (CNAME): link.example.com (replace with your preferred link tracking domain) Custom SSL certificate: Choose the appropriate ACM certificate Click a Create distribution button. Wait for the distribution status to be “Enabled”. Add (or update) a CNAME record in your link tracking domain’s DNS settings for the domain you are configuring (e.g., link.example.com) and point it to the Domain name shown in CloudFront for your distribution. (e.g., CHANGEME.cloudfront.net). The host name and value for your CNAME record will be something like: CNAME record host name: link.example.com CNAME record value: CHANGEME.cloudfront.net Verify that your DNS record has propagated and is now pointing to your CloudFront distribution. You can do this by checking the CNAME value at a propagation checker like WhatsMyDNS. As an additional “sanity check” you can visit your link tracking domain, followed by /health (e.g., https://link.example.com/health). If your domain is properly pointing to our API, the response body will just be {}. Anything else means there is a problem with your configuration.  If anything in your proxy’s configuration modifies or misrepresents the referring host, your links may result in Invalid link security token errors—even if you get a {} response. Your proxy server MUST use your link tracking domain as the host header for the requests that are passed to our server. Finally, once you are sure that your distribution is properly pointing to our API, head back to Customer.io and go to your Workspace Settings for Email. If you haven’t already set up your link tracking domain (e.g. link.example.com), enter it now in the HOST NAME field and click the Verify domain button to re-validate the domain. You should now pass the HTTPS check and tracking links will use HTTPS by default.  If past messages already have white-labeled tracked links, changing your link tracking domain will cause those existing tracked links to break. When the domain is collapsed/closed, you can tell that HTTPS link tracking is enabled by looking at the LINK TRACKING section pictured below. Once enabled, your tracked links will now start with something like: https://link.example.com... Set up HTTPS link tracking with Fastly CDN Fastly is a content delivery network (CDN) and can proxy requests to Customer.io to support HTTPS tracked links. This process assumes you’ve already set up your domain in Fastly. If you haven’t done that, you’ll want to do that first in your Fastly dashboard under Security > TLS Management > Domains. In Fastly, go to CDN and click Create a CDN Service. Give your service a name. Enter the domain you want to use for your tracked links (e.g. link.example.com). Under Add an origin, enter track.customer.io or track-eu.customer.io (depending on your account region). Disable the Override default host setting. We match the host header to your branded tracking domain for security purposes. If Fastly overrides the host, we’ll think that the link poses a security risk and return an Invalid Security Token error! Click Activate. It may take a minute or two for your CDN to finish activating. Then your links should be proxied through Fastly and use HTTPS by default. If you haven’t already set up your link tracking domain in Customer.io, go to Settings in the upper right > Workspace Settings > Email. On your domain, go to Actions , click Edit and go to Link Tracking to set your domain. Then click Verify domain. If you already set up your CDN, and the Override default host setting enabled, you can edit the Override host setting for your CDN and set it to your domain or leave it blank. Setting up HTTPS Link Tracking with NGINX Alternatively you can use your own server to serve HTTPS tracked links. The following instructions will guide you through setting up NGINX, however it’s possible to use other server software to accomplish this. Request a new SSL certificate for the domain you want us to use for your tracked links (e.g. link.example.com). Place the certificate chain into the file named /etc/pki/tls/certs/link.example.com.crt Place the private key into the file named /etc/pki/tls/private/link.example.com.key Create the file /etc/nginx/conf.d/link.example.com.conf, with the following content - ensuring that the host header is set to the Host Name specified in your link tracking settings in Customer.io (e.g. link.example.com). The proxy_pass URL should match your region (track.customer.io or track-eu.customer.io):  Use https://track-eu.customer.io if you’re in our EU data region If you use the wrong regional URL in the proxy_pass field, we won’t be able to validate your link-tracking domain in later steps. server { listen 80; listen 443 ssl; server_name 'link.example.com'; ssl_certificate '/etc/pki/tls/certs/link.example.com.crt'; ssl_certificate_key '/etc/pki/tls/private/link.example.com.key'; location / { proxy_pass 'https://track.customer.io'; proxy_set_header 'Host' 'link.example.com'; } } Update your DNS record to change the CNAME record for link.example.com to send traffic to your NGINX server. If you’re specifying the IP address of your server this will need to be an A record instead of a CNAME record. CNAME or A record host name: link.example.com CNAME or A record value: IP Address of your NGINX server As an additional “sanity check” you can visit your link tracking domain, followed by /health (e.g., https://link.example.com/health). If your domain is properly pointing to our API, the response body will just be {}. Anything else means there is a problem with your configuration.  If anything in your proxy’s configuration modifies or misrepresents the referring host, your links may result in Invalid link security token errors—even if you get a {} response. Your proxy server MUST use your link tracking domain as the host header for the requests that are passed to our server. In your Customer.io Workspace Settings under Email, set up your link-tracking domain if you haven’t already. Enter your domain in the HOST NAME field and click Verify domain to re-validate the domain. You should now pass the HTTPS check and tracking links will use HTTPS by default.  If past messages already have white-labeled tracked links, changing your link tracking domain will cause those existing tracked links to break. When the domain is collapsed/closed, you can tell that HTTPS link tracking is enabled by looking at the LINK TRACKING section pictured below. Once enabled, your tracked links will now start with something like: https://link.example.com… --- ## Overview of subscription options URL: https://docs.customer.io/journeys/subscriptions-overview/ We provide native unsubscribe functionality for email, SMS, WhatsApp, and push notifications. In this article, you'll also learn about non-native subscription options for push, SMS, as well as in-app messages. Native unsubscribe links for emails, SMS, WhatsApp, and push In Customer.io, you can use our default global unsubscribe functionality or set up a subscription center to manage which messages your customers receive. We recommend you create a subscription center to give your customers the option to receive messages they’re interested in without opting-out of all of your messages. flowchart LR c{Is unsubscribed true?} c-....->|yes|f[Person is only eligible for in-app or Slack messages] c-->|no|g{Is the subscription center enabled?} g-->|yes|d{Is the person subscribed to the topic?} d-->|yes|ch{Is the person subscribed to the channel?} ch-->|yes|e[Person receives message according to preferences] ch-->|no|chblock[Person does notreceive message] d-->|no|chblock g-...->|no|i[Person receives message] Unsubscribe globally Out-of-the-box, Customer.io provides global unsubscribe functionality where your customers can subscribe or unsubscribe from email, SMS, WhatsApp, and push notifications. This is great for getting started, but as you grow, you may want to give your customers more control over the messages they receive with subscription preferences. When someone clicks an unsubscribe link in an email, they’ll see this page and they’ll be unsubscribed from email, SMS, WhatsApp, and push notifications. Subscription preferences We recommend you set up a subscription center so your customers can control the messages they receive. The subscription center gives people two dimensions of control: topics and channels. Both work the same way—you set them up in Workspace Settings > Subscription Center, configure each as opt-in or opt-out, and provide a name and description. Click Preview at the top right of the landing page to see what our subscription center looks like to anyone who receives your messages. A person can still globally unsubscribe if the subscription center is enabled. They just click Unsubscribe from all or uncheck every preference and click Save. Read on to learn how to: Set up subscription preferences with topics and channels Add unsubscribe URLs to your messages Track subscription preferences Brand your unsubscribe page Topics Topics control what kind of content people receive—for example, “Product Updates” or “Marketing Offers.” Unsubscribing from a topic means your customers no longer receive emails, push, or SMS for that topic. Channels Channels control how people receive messages—for example, email, SMS, push, or WhatsApp. Channel preferences let people opt in or out of specific messaging channels, independent of their topic subscriptions. For example, a person might want to receive your “Product Updates” topic by email but not SMS. You must explicitly add each channel you want people to manage—enabling a messaging channel in your workspace doesn’t automatically add it to the subscription center.  Channel preferences apply per channel type, not per sender Channel preferences control whether a person receives messages on a channel type as a whole. For example, setting email to false stops all email—you can’t set preferences for individual email addresses or phone numbers. If you need per-sender control, you might align your sender addresses with topics and use topics to control preferences instead. How topics and channels work together Topics and channels are independent layers of subscription control, but they combine as an AND—a person must be subscribed to both the topic and the channel to receive a message. If either one is opted out, the message is blocked. For example, say a person is subscribed to your “Product Updates” topic but has opted out of the push channel. If you send a push notification for Product Updates, that person won’t receive it—even though they’re subscribed to the topic. They’d still get Product Updates by email or SMS, assuming those channels are subscribed. Topic Channel Result Subscribed Subscribed Message sends ✅ Subscribed Unsubscribed Message blocked ❌ Unsubscribed Subscribed Message blocked ❌ Unsubscribed Unsubscribed Message blocked ❌ There’s no per-topic-per-channel preference—topics and channels are independent. A person can subscribe to all topics but opt out of push, or subscribe to push but opt out of a specific topic. The flowchart at the top of this page shows this logic visually. Different messaging channels handle subscription checks differently: Channel Subscription behavior Email, SMS, push, WhatsApp Respects global unsubscribe, topic preferences, and channel preferences In-app Ignores global unsubscribe and topic preferences by default; respects channel preferences if you configure in_app as a channel Slack Ignores global unsubscribe and topic preferences; respects channel preferences if you configure slack as a channel Webhooks Bypasses all subscription checks Unsubscribe links You can use the following 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 generate links that unsubscribe people from email, SMS, and push: {% unsubscribe %} {% unsubscribe_url %} {% manage_subscription_preferences_url %} {% unsubscribe %} and {% unsubscribe_url %} both work if you’re using our global unsubscribe functionality or subscription topics: {% unsubscribe %} generates “Unsubscribe” which a person can click to unsubscribe from your messages. If you’re using the subscription center, people can unsubscribe from the specific topic in your message or manage other topics. If not, this page lets your audience unsubscribe from all messages. {% unsubscribe_url %} generates an unsubscribe link but lets you customize the text your audience sees. To hyperlink within the code and rich text editors, place {% unsubscribe_url %} in the href attribute of an anchor <a> tag in the HTML: When using the drag and drop editor, place {% unsubscribe_url %} in the settings for the link you create: {% manage_subscription_preferences_url %} only works if your subscription center is enabled: {% manage_subscription_preferences_url %} generates a link to the subscription preferences page. This is where a person can manage their subscription preferences for all topics and channels. With our standard links - the two tags above - we would first prompt a person to unsubscribe from the topic of the message; this link allows your recipients to go straight to the subscription preferences page. You might do this when you announce a change to preferences, for instance. Subscription attributes Our unsubscribed attribute tracks a person’s global subscription status when the subscription center is enabled and disabled. We also track topic and channel preferences through profile attributes. unsubscribed We track global subscription statuses through a profile attribute named unsubscribed. It’s a field that takes boolean values - true or false. Setting this field to true means the person is unsubscribed from email, SMS, and push. Setting this field to false means the person is subscribed to them. A person can globally unsubscribe when the subscription center is enabled or disabled. If the subscription center is enabled, a person must go to the subscription preferences page and click Unsubscribe from all or they must uncheck all of their preferences and click Save. If the subscription center is disabled, a person clicks Unsubscribe. Go to Set unsubscribed attributes to learn more. Topic and channel preferences We track subscription preferences in their own table within the Overview tab of a person’s profile. Behind the scenes, both topic and channel preferences exist within a JSON object called cio_subscription_preferences. Topics are identified by numeric ID (so renaming a topic doesn’t affect preferences) and channels use the channel type name. { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": false }, "channels": { "email": true, "sms": true, "push": false } } } Both topics and channels use the same opt-in/opt-out pattern. If a topic or channel is configured as opt-in, people must explicitly subscribe before receiving messages. If it’s configured as opt-out, people are subscribed by default. Go to Manage subscription preferences to learn more about setting these attributes.  Channel preferences don’t replace topics—they add a second layer of control. If you only use topics today, everything continues to work the same until you configure channel preferences. --- ## Global unsubscribes URL: https://docs.customer.io/journeys/unsubscribes/ **A person's global subscription status applies to email, push, SMS, and WhatsApp.** When people click unsubscribe links in your messages, they unsubscribe from those message channels. Customer.io tracks messages that cause people to unsubscribe so you can better understand your message and campaign performance.  Check out our subscription center! This article covers global unsubscribes. Visit our subscription center documentation to learn how to give customers more granular control over the messages they receive. Our subscription functionality doesn’t apply to in-app messages; rather, you’ll want to learn how to target the right people with in-app messages. Learn more about how people can unsubscribe from SMS, WhatsApp, or push notifications as channels. How it works In your emails, you need to add 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 generate an unsubscribe link for your customers. Other channels have their own unsubscribe mechanisms, but unsubscribing from links in emails will also unsubscribe people from SMS, WhatsApp, and push notifications. When you create an email with our drag-and-drop editor, we automatically add a block with {% unsubscribe %}. When a person clicks this link, we send them to a page where they can unsubscribe: After they click Unsubscribe, we update their profile. We track subscription status through the unsubscribed attribute. The field takes a boolean value where true means that a person is unsubscribed from email, SMS, WhatsApp, and push notifications. The value false means a person is globally subscribed, or if you have subscription center topics, it means they will receive messages based on their topic preferences. flowchart LR c{Is unsubscribed true?} c-->|yes|f[Person does not receive email, SMS, WhatsApp, or push, but could receive in-app or Slack messages] c-->|no|g[Is the subscription center enabled?] g-->|yes|d{Is the person subscribed to the topic?} d-->|yes|e[Person receives message] d-->|no|h[Person does not receive email, SMS, WhatsApp, or push, but could receive in-app or Slack messages] g-->|no|i[Person receives message] A person who is globally unsubscribed will not receive email, SMS, WhatsApp, and push (unless you change the default settings on a campaign, broadcast, or message). If you want to send messages to unsubscribed people, make sure it’s a transactional use case and you respect your audience’s local laws. Then you can track which messages and workflows people unsubscribe from in our metrics dashboards. In campaigns and API-triggered broadcasts, go to Delivery Metrics. In newsletter broadcasts, go to Overview. Set unsubscribed attributes You can set global unsubscribed attributes manually in your workspace or programmatically through our Javascript snippet, API, and integrations. We recommend using a boolean (true/false) to manage subscription status, but the unsubscribed field will take string and integer values like True, "true" , "True", 1, and "1" too. If the value is false, absent, or any value not considered “true”, a person is globally subscribed, or if you enabled a subscription center, a person will receive messages based on their cio_subscription_preferences. flowchart LR c{Is unsubscribed any casing of 'true' or 1?} c-->|yes|f[Person does not receive email, SMS, WhatsApp, or push, but could receive in-app or Slack messages] c-->|no|g[Is the subscription center enabled?] g-->|yes|d{Is the person subscribed to the topic?} d-->|yes|e[Person receives message] d-->|no|h[Person does not receive email, SMS, WhatsApp, or push, but could receive in-app or Slack messages] g-->|no|i[Person receives message] Unsubscribe a person On a person’s profile, click Unsubscribe from the dropdown: Under Attributes, you’ll see the unsubscribed attribute update to true. You can also track these changes under Activity. Resubscribe a person If a person accidentally unsubscribes, they have the option to resubscribe. If they click the resubscribe button, we set the unsubscribed attribute to false and confirm that they are subscribed once again. Reference the unsubscribed attribute in liquid logic If you’re referencing this field in liquid logic, keep in mind that attribute values are case sensitive. For instance, you could create an if statement with the unsubscribed attribute in a message: {% if customer.unsubscribed == "true" %} You're unsubscribed from messages; you're only receiving this because you completed a transaction. {% else %} You're receving this because you completed a transaction. {% endif %} If your values have a different casing like “True”, then the else statement would render. If you want to normalize your data so all values have the same casing, you can use Create or update person actions in campaigns to set the attributes to the recommended true or false values. Override sending settings in your workspace We do not send email, SMS, and push to people who are unsubscribed. By default, both campaign and broadcast settings, as well as their individual messages’ settings, are set to send to “All subscribed.” However, you can choose to send to unsubscribed if it’s absolutely necessary. You should not send to unsubscribed people unless the message is transactional in nature, like a password reset. For campaigns, you can change the subscription preference in Messages > Subscription preferences: For API-triggered broadcasts, you can change the subscription preference in the Settings tab: For Newsletters, you’ll see a dropdown in the Recipients tab under Sending options: An individual message uses your campaign or broadcast settings by default. You can change it by selecting it in your workflow and going to Subscription Preference in the panel: Review more information on sending behaviour and subscription statuses.  Respect anti-spam laws Remember that emailing someone who has unsubscribed could breach anti-spam laws in your country. We reserve the right to terminate your account if you continue to breach these laws. --- ## The subscription center URL: https://docs.customer.io/journeys/subscription-center/ A subscription center helps you differentiate between the different types of messages you send and lets your audience decide what kinds of campaigns and messages they want to receive. If you’re looking for information on global subscriptions or the unsubscribed attribute, check out our overview on subscription options. How it works The subscription center lets people manage what messages they get through topicsA category of message, set within your workspace’s subscription center, that people can subscribe to or unsubscribe from. Topics let your audience determine the kinds of messages they want to get from you. (content types) and channel preferences (delivery methods). If a person is unsubscribed globally, from a topic, or from a channel, they won’t receive the message. Here’s how the subscription logic works: flowchart LR c{Is unsubscribed true?} c-....->|yes|f[Person is only eligible for in-app or Slack messages] c-->|no|g{Is the subscription center enabled?} g-->|yes|d{Is the person subscribed to the topic?} d-->|yes|ch{Is the person subscribed to the channel?} ch-->|yes|e[Person receives message according to preferences] ch-.->|no|chblock[Person does notreceive message] d-.->|no|chblock g-...->|no|i[Person receives message] Topics control what kind of content people receive, and channel preferences control how they receive it. Both the topic and channel must be subscribed for Customer.io to send a message.  Subscription preference settings do not filter customers out of campaigns or broadcasts. If a person meets trigger and filter criteria for a campaign or broadcast, they will begin a journey regardless of their subscription preferences. If they opted out of the subscription topic assigned in settings, they will not, however, receive email, SMS, or push notifications. Subscription preference settings only apply to message sending, not starting/stopping a journey or other actions in a workflow. Default subscription status When you add topics or channels, you decide whether people are automatically opted in or opted out. You can always check your subscription center in workspace settings to remind yourself of the default status of each topic or channel: Default status options: Not subscribed: people must explicitly subscribe before they’ll receive messages for this topic Subscribed: people receive messages by default and must unsubscribe if they don’t want messages from this topic You cannot change this setting after you’ve added the topic. The subscription center page in workspace settings shows the default status of each topic and channel. What people see when they click Unsubscribe After you enable subscription center topics, the unsubscribe links in your emails will send people to a page where they can unsubscribe from the message’s topic, not all messages. From there, they can go to the subscription preferences page and subscribe to, or unsubscribe from, any topic. When people click unsubscribe, they can unsubscribe from the topic: When people click "here" to manage preferences, they can check/uncheck their preferences: flowchart LR a[person clicks unsubscribe in email] a-..->|person clicks unsubscribe|c[person is unsubscribed from topic] a-->|person clicks here and goes to subscription center|d{Subscription preferences page} d-.->|person clicks Save Preferences|f[person's subscription preferences are updated] d-.->|person clicks Unsubscribe from all|g[Person is globally unsubscribed from email, SMS, and push] If you use {% unsubscribe %} or {% unsubscribe_url %} as your unsubscribe links, a person will follow this flow. You can also link people directly to your subscription preferences page using {% manage_subscription_preferences_url %}. Considering the example above, they would bypass the first page and go straight to the second. You can preview the subscription preferences page in Workspace Settings > Subscription Center by clicking Preview. Set up your subscription center Setting up the subscription center involves several steps. As a part of this process, you’ll: Customize your subscription center branding and language. This helps you make the subscription center look and feel like a part of your brand. Add topics and channels-these are the preferences your audience can choose from when they set their subscription preferences. Apply topics to your campaigns and broadcasts.  Map out your topics before you start You need to apply topics to campaigns and broadcasts before you can enable the subscription center. It may help to map out and/or tag campaigns and broadcasts before you create topics to help you get a handle on the topics you want to create and the things they apply to. Translate your topics and other custom copy to fit your audiences’ needs. (Optional) Backfill your audience’s subscription preferences using the cio_subscription_preferences attribute. Enable the subscription center. Add topics and channels When you add a topic or channel, you can determine whether your audience must opt into it or opt out of it. By default, a new topic requires people to opt out; workflows using a new topic with default settings will send messages to everybody (who isn’t globally unsubscribed) until they opt out-of the topic. To use channel-based subscriptions, you must add at least one topic. In Customer.io, go Settings > Workspace Settings. Go to Subscription Center. In the Subscriptions tab, click Add Topic or Add Channel. Set a Name and a Description. The name is how you’ll select your topic in the Subscription preference setting. Your audience will see both the Name and Description when they set their subscription preferences. Determine whether People are subscribed by default. Off: People must opt-into the topic to get messages. On: (Default) People will get messages for this topic unless they change their subscription preferences to opt out. Before you use the On setting, make sure that you have your audience’s consent or won’t otherwise violate their message preferences and send them messages that they might consider spam! Click Save. When you’re done adding topics and channels, you can customize your subscription center’s branding and translate your subscription center’s content. You’ll also need to set your topics in the Subscription preference setting for your campaigns and broadcasts. This is how you determine who receives messages from campaigns and broadcasts based on their preferences. Reorder topics and channels You can drag topics and channels to change the order in which they appear in your subscription center from the subscription preferences page. You can use the Preview button to see how your changes will look to your customers. When you save your changes, people will see the new order in your subscription center. Edit a topic or channel You can update the Name or Description for your topic or channel. This does not affect your Subscription preference settings or people’s subscription preferences. It only affects what people see when they change preferences using the subscription center. In Customer.io, go Settings > Workspace Settings. Go to Subscription Center and click the item you want to update. Change the Name or Description. When you’re done, click Save. Delete a topic or channel You can’t delete a topic or channel that’s in use. You’ll need to change the Subscription preference setting for any running campaigns and broadcasts that use the topic you want to delete before you can delete it. You don’t need to worry about draft or stopped workflows. In Customer.io, go to Settings > Workspace Settings. Go to Subscription Center and hover over the topic you want to delete. On the right, you’ll see a trash can icon appear . Click this to delete the topic. Set a topic in a campaign or broadcast To enable subscription center topics, you must assign a subscription preference to all running campaigns and broadcasts - both API-triggered broadcasts and scheduled newsletters. Set any one of your subscription center topics as the subscription preference. This determines who will receive messages from your campaign or broadcast, not whether they enter into a journey. Only the trigger settings determine whether someone enters a journey; the subscription preference determines who will receive messaging of those that enter a journey.  Subscription preferences apply to all messaging channels The subscription center uses topics to control what content people receive. You can also use channel preferences to let people control which messaging channels they receive messages on. Both the topic and channel must be subscribed for a message to send. For example, let’s say you have a segment-triggered campaign. The segment is anyone who has purchased a product in the last 30 days. The subscription preference is set to people subscribed to Product Updates. People who are subscribed to Product Updates will receive messages from the campaign. People who are not subscribed to Product Updates will not receive messages from the campaign, but they will still enter the campaign if they meet the trigger criteria. You can also override the topic at the message level should the campaign subscription preference setting not fit for all messages. Campaigns and broadcasts will not take your subscription preference setting into account until you’ve clicked Enable in your subscription center. Set a topic in a campaign Under Messages in your campaign’s settings, select the topic for your campaign from the Subscription preference dropdown. Only people subscribed to this topic will receive messages from the campaign, unless you override the topic at the message level. Set a topic in a broadcast In the broadcast’s Recipients step, determine Who should receive this? Then select the topic for your newsletter or API-triggered broadcast from the Subscription preference dropdown. Only people subscribed to the selected topic will receive your newsletter or messages in your API-triggered broadcast, unless you override the topic at the message level. Bulk update topics for running campaigns and broadcasts To expedite enabling your subscription center, you can assign a topic to multiple campaigns, broadcasts, and newsletters at once through Workspace Settings > Subscription center. On the Topics table, select Review. Under the Pending tab is a list of all campaigns and broadcasts that are running or newsletters that are scheduled to run and need a topic assigned. Select the box next to each one you want to update. Select which topic you want to assign as the subscription preference from the dropdown. Click Apply. The checked campaigns, broadcasts, or newsletters will move to the Completed column. The All tab shows both Pending and Completed items. Use the filters at the top to search by name or description, tags, type of campaign or broadcast, and subscription preference. Override a topic for a message After creating at least one topic in your workspace’s subscription center, you can assign a subscription preference to your campaigns. You also have the option to override the subscription preference for each email, push notification, or SMS in the campaign. To override the subscription preference: Select the message in your campaign. On the left-hand pane, scroll to Override Campaign Settings. Select the Subscription Preference dropdown and choose to send based on a different topic preference or global subscription setting. By default, we send messages based on campaign settings. Click Save. Moving forward, Customer.io will draft or send this message based on the message-level override of the subscription preference. Enable the subscription center Before you can enable your subscription center, you must have created at least one topic, and applied topics to the Subscription preference setting for your active campaigns, scheduled newsletters, and API-triggered broadcasts. You may also want to backfill or migrate existing subscription preferences. When you enable the subscription center, the Unsubscribe link in your messages also lets people set their topic preferences. To enable your subscription center: Go to Settings > Workspace Settings. Click Subscription Center and then click Enable. After you set up your subscription center, see Manage subscription preferences to learn how to set, find, and filter preferences. For tracking and reporting, see Subscription preference metrics. --- ## Brand your subscription pages URL: https://docs.customer.io/journeys/subscription-center-branding/ If you have no subscription topics in your workspace, you can brand the **global unsusbcribe page** that your customers will see when they click your unsubscribe links. If you have at least one subscription topic (even if the subscription center is disabled), you'll have the ability to brand your **subscription preferences page**. You can add your own Logo and set a primary Brand Color so that your subscription pages look more like a native part of your website or app. Set up branding Global unsubscribes You can set the branding for your global unsubscribe page as long as you have not created subscription topics. Go to Workspace Settings > Subscription Center and click Settings. Then select Edit next to Branding. Select your Logo. Choose Same as account settings if you want your subscription center to match your account’s logo. You can also select Custom to use an image from your assets library or point to a URL. Set your Brand Color as a hex value. Click the current color to open a color picker. When you’re done, click Save in the top right. Subscription topics After you set up at least one topic, you can set your Logo and Brand Color. Go to Workspace Settings > Subscription Center and click Settings. Then select Edit next to Branding. Select your Logo. Choose Same as account settings if you want your subscription center to match your account’s logo. You can also select Custom to use an image from your assets library or point to a URL. Set your Brand Color as a hex value. Click the current color to open a color picker. When you’re done, click Save in the top right. Logo Guidelines You can save a logo to your asset library. Your logo can be up to 1MB in size (PNG, JPG, or GIF). We also limit the height of the logo to 80px. This means you may not want to use your largest, highest-resolution logo; you don’t want the logo to impact your users’ load times. Brand Colors For subscription preferences, the brand color affects the Save Preferences button and the background color of checkboxes. For the global unsubscribe page, the brand color affects the Unsubscribe button. We change the foreground color—for the button text and checkmarks—dynamically based on the background color you set. This ensures that your buttons and checkboxes are easy to see and read no matter what brand color you choose. --- ## Subscription FAQs URL: https://docs.customer.io/journeys/unsubscribe-faqs/ This article contains a series of frequently asked questions that apply to our subscription center and global unsubscribe functionality. What messages will someone stop receiving after unsubscribing? By default, if a customer is globally unsubscribed, we will not send them emails, push or SMS for any campaign or broadcast. If a customer is unsubscribed from a topic in your subscription center, we also stop sending email, push and SMS, but only for that topic. If you use channel preferences, people can also opt out of specific messaging channels. For example, if someone unsubscribes from the push channel, they won’t receive push notifications for any topic—even if they’re still subscribed to those topics. We continue to send slack messages and webhooks to unsubscribed, since these are often used for internal purposes. In-app messages also ignore users’ unsubscribed status, though channel preferences for in_app are respected if configured. Keep in mind, transactional messages (password resets or invoices, for example) send to unsubscribed by default, but you can toggle that off in the message’s settings or override it through our API. For other transactional use cases, you can override the default audience through your campaign or broadcast settings. You can also adjust this setting on an individual message for emails, push, and SMS messages. How do I add an unsubscribe link to my emails? To add an unsubscribe link, go to our subscription options overview. How do topics and channels work together? Topics and channels are independent dimensions of subscription control. Topics control what kind of content a person receives (for example, “Marketing Updates” or “Product News”). Channels control how they receive it (for example, email, SMS, or push). Both the topic and the channel must be subscribed for a message to send. Here’s how the logic works: Topic Channel Result Subscribed Subscribed Message sends ✅ Subscribed Unsubscribed Message blocked ❌ Unsubscribed Subscribed Message blocked ❌ Unsubscribed Unsubscribed Message blocked ❌ There’s no per-topic-per-channel preference; channels and topics are independent layers. A person can be subscribed to all topics but opt out of push, or subscribed to push but opt out of a specific topic. Webhooks bypass all subscription checks. See the subscriptions overview for details on how each channel handles subscription preferences. Do I need to manually add channels to the subscription center? Yes. Enabling a messaging channel in your workspace (like email or SMS) does not automatically add it to the subscription center. You must go to Workspace Settings > Subscription Center and explicitly add each channel you want people to manage preferences for. For each channel, you can set whether it’s opt-in or opt-out, and provide a custom name and description. Can I set preferences for individual email addresses or phone numbers? No. Channel preferences apply to the channel type as a whole—for example, email or sms. Setting email to false stops all email for that person; you can’t opt out of messages from a specific email address or phone number while keeping others. If you need more granular control, consider using topics to separate different types of content. Are unsubscribe links tracked? By default, unsubscribe links are not tracked in Customer.io. That includes these liquid tags: {% unsubscribe %} {% unsubscribe_url %} {% manage_subscription_preferences_url %} If you want to track unsubscribe links, use {% unsubscribe_url %} or {% manage_subscription_preferences_url %} and add class='tracked' to the <a> tag in the HTML: <a href="{% unsubscribe_url %}" class="tracked">Unsubscribe</a>. What is the List-Unsubscribe header? When you use an unsubscribe link in an email, we add the List-Unsubscribe header to your email automatically. If a person, clicks this header, we always globally unsubscribe them from messages, never from a single subscription topic.  Test sends do not contain a List-Unsubscribe header If you are sending ad hoc test emails from the template composer, they won’t include the List-Unsubscribe header. We generate the header from recipient information so we can properly attribute unsubscribes to the right message and person. How do I remove an unsubscribe link from my emails? An unsubscribe link is added by default in the Empty Layout used in rich text and code-based emails. Edit this layout by going to the Content > Email Layouts of the left hand nav of your workspace: You can also create a new layout without this link and add this through Layout & Preview while editing an email. In the drag and drop editor, remove the unsubscribe link by editing or deleting the block. How do I handle more complex messaging preferences? Check out our subscription center! Does every email require an unsubscribe link? We don’t programmatically enforce it, but we encourage it. If you are sending marketing emails, you should adhere to all local laws about customer communication. Do I have to use your unsubscribe link in emails? No. While we require you to include an unsubscribe whenever necessary and legal, we allow you to use your own unsubscribe functionality if you so choose. If you use your own, we won’t be able to track which email someone unsubscribed from or place an unsubscribe in your email header.  Custom unsubscribe links must adhere to new Google and Yahoo standards If you use custom unsubscribe links, you’ll also need to do a bit of development work to support the new RFC 8058 before June 1, 2024. See custom unsubscribe links (RFC 8058) for more information. How do I send my customers to an unsubscribe page in another language? The unsubscribe page will display in the language of your customer’s browser as long as this language is supported (see our list below). If you’d like to manually set the unsubscribe page language, append the optional parameter lang= within the {% unsubscribe_url %} liquid. For example, to redirect users to a French translation of the unsubscribe landing page use {% unsubscribe_url lang='fr' %}. This parameter accepts valid IETF Language Codes for any of the languages we currently have translations for.  Surround your language code in single quotes. Note the use of single quotes around the language code ('fr'). This is important if the code is being placed in a link in the drag-and-drop editor. It’s also important for links enclosed in double quotes (href="{% unsubscribe_url lang='fr' %}") that are placed in an href attribute in our rich-text or code editors. If you nest double quotes inside double quotes OR single quotes inside single quotes, your link will not work as expected. Currently supported languages: Language Code Brazilian Portuguese pt-br Bulgarian bg Chinese zh Czech cs Danish da Dutch nl English en Estonian et Finnish fi French fr German de Greek el Hebrew he Hungarian hu Italian it Japanese ja Latvian lv Norwegian no Polish pl Portuguese pt Romanian ro Russian ru Slovak sk Spanish es Swedish sv Thai th Turkish tr Ukrainian uk If you only use our global unsubscribe functionality and you provide an invalid or unsupported language code, we default back to the English unsubscribe page. If you use our subscription center and you provide an invalid or unsupported language code, we fallback on your default language, the language you created your subscription center in (Workspace Settings > Subscription Center > Settings > Localization). We are always willing to add additional languages, so please let us know if you can provide a translation! --- ## Manage subscription preferences URL: https://docs.customer.io/journeys/manage-subscription-preferences/ Learn how to set, find, and filter subscription preferences for your audience—including adjusting preferences in the UI, migrating from another service, and using the API. After you set up your subscription center, you can manage people’s subscription preferences in several ways. Set subscription preferences Beyond people unsubscribing themselves from messages, you can also: manually set subscription preferences in your workspace migrate people’s existing preferences from another service send out a campaign requesting they update their preferences track their preferences outside of the subscription center You have the option to set preferences for some subscription topics or channels, while preserving those set for others, using JSON dot notation. Adjust subscription preferences in the UI After you enable your subscription center, you can view and change people’s preferences on their profiles: Click Manage beside Subscription Preferences to change them. Click Save Preferences to adjust individual preferences or click Unsubscribe so they no longer receive emails, SMS, or push. Backfill or migrate preferences Before you enable subscription center topics, you might want to set or migrate people’s subscription preferences. You’ll almost certainly need to do this if you set your topics up as “opt-in” by default. You can set your audience’s subscription preferences using the reserved cio_subscription_preferences attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. This attribute contains both topics and channels preferences where every individual preference is a boolean (true/false). Topics are numbered based on the ID that you see in the UI—topic_1 corresponds to ID 1 in the left-column in your Subscription Center setup page. We set subscription preferences by topic ID rather than the topic Name, so that you can change the name of a topic without affecting your audience’s preferences. Channels use the channel type name as the key (email, sms, push, in_app, whatsapp, slack, line, inbox). Channel preferences let people opt in or out of specific messaging channels, independent of their topic preferences. When you send a message, a person must be subscribed to both the relevant topic and channel to receive the message. { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": false }, "channels": { "email": true, "sms": true, "push": false } } } Create a campaign to record preferences After your subscription center is enabled, you can send a campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. to customers to show them they now have the option to subscribe to topics and channels. You can prompt them to manage their preferences in the message. Set preferences outside of the subscription center It’s not always convenient for your audience to manage their preferences through messages. For example, a user might expect that they can change their preferences through your website. You can help your audience manage their preferences independently of messages by either: Generating a link to the subscription center for a user using our App API. You might do this if someone is already signed in to your app and you want to give them a way to manage their preferences. Building a custom preferences page, which gives you full control over their subscription experience. Resubscribe a person If a person accidentally unsubscribes from a topic or channel, they have the option to resubscribe. If they click the resubscribe button, we set their preference back to true and confirm that they are subscribed once again. If a person accidentally unsubscribes from all on the preferences pages, they just need to recheck the topics and channels they want to receive messages for then save their preferences again. Find subscription preferences After you set up your subscription center, you’ll find people’s subscription preferences when you select them on the People page. You can view and edit subscription preferences from the Overview tab of a person’s profile. Track changes to preferences On the Attributes tab, as well as your workspace’s Activity Log, you can track when someone changed a subscription preference with the reserved attribute cio_subscription_preferences (more on that below). A change is logged when: your customer updates a subscription preference from the subscription preferences page. you/an admin manually change a subscription preference from the Journeys UI. a campaign with an Update or Create Person workflow action updates a subscription preference. you update a person’s subscription preferences via CSV, Track API, web SDK, or a reverse ETL sync. Subscription preferences in the API When you look up a person using our API, or export a person, we include a person’s subscription preferences and their computed subscription preferences. The cio_subscription_preferences attribute contains preferences that a person set through the subscription center—or attributes you otherwise applied to a person. This includes both topic and channel preferences. However, a person might not have set preferences, or you may have changed topics or channels since the last time a person set their preferences. For these cases, we also include a _cio_subscription_preferences_computed attribute containing all of a person’s subscription preferences, including defaults for topics and channels a person hasn’t set preferences for yet. For example, if you have an opt-in topic that a person hasn’t set a preference for, that topic would show false—even though a person doesn’t have that attribute on their profile. The same applies to channel preferences. This is an example of what we show when you look up a person’s attributes.  Use reporting webhooks to track preference changes If you capture your people’s attributes in an external CRM or another system, you can use reporting webhooks to get real-time events when people change their subscription preferences. { "customer": { "id": "1", "attributes": { "\_last_emailed": "1528932553", "created_at": "1489014595", "email": "test@example.com", "id": "1", "cio_id": "03000001", "cio_subscription_preferences": "{\"topics\":{\"topic_7\":false,\"topic_8\":false},\"channels\":{\"email\":true,\"push\":false}}", "\_cio_subscription_preferences_computed": "{\"topics\":{\"topic_6\":false,\"topic_7\":false,\"topic_8\":false},\"channels\":{\"email\":true,\"sms\":true,\"push\":false}}" }, "timestamps": { "cio_id": 1489014595, "\_last_emailed": 1528932553, "created_at": 1489014595, "email": 1508932553, "id": 1489014595, "\_cio_subscription_preferences_computed": 0, "cio_subscription_preferences": 1673987303 }, "unsubscribed": false, } } Filter by subscription preferences On the People page, you can filter by subscription preferences. Filtering works the same way for both topic and channel preferences—you set conditions based on the corresponding attribute path. Topics: use the attribute cio_subscription_preferences.topics.topic_<id>. For example, to find people who unsubscribed from a topic, filter where cio_subscription_preferences.topics.topic_<id> is equal to FALSE. You can also create data-driven segments to track topic subscription trends over time. Channels: use the attribute cio_subscription_preferences.channels.<channel>. For example, to find people who have opted out of push notifications, filter where cio_subscription_preferences.channels.push is equal to FALSE. --- ## Subscription preference metrics URL: https://docs.customer.io/journeys/subscription-metrics/ Track unsubscribes from topics, monitor subscription trends over time with data-driven segments, and send subscription events to external systems. You can track subscription preference changes across campaigns, broadcasts, and newsletters—and send subscription events to external systems through reporting webhooks and data warehouse integrations. Metrics for subscription preferences You can track unsubscribes from topics across campaigns and broadcasts within the Journeys UI or through reporting webhooks. Unsubscribed from topics counts each subscription topic that a user unsubscribed from through a delivered message. As such, you may see a percentage above 100; say you have 4 subscription topics and 5 messages are delivered to five users. Two users unsubscribe from 3 topics each (6 total). We would divide this by the number of delivered messages (5) to get 1.2 or 120% unsubscribed from topics. You can read more about how our subscription center works in Customer.io. For campaigns and API-triggered broadcasts, you can find data in the Metrics tab under Performance & Delivery Metrics and Message Metrics. Check Unsubscribed from topics in the right hand column of Performance & Delivery Metrics to update the chart. Scroll down to Message Metrics to find the unsubscribed rate per message. Select on the right and check the box for Unsubscribed from topics. For newsletters, go to the Overview tab. You’ll find Unsubscribed from topics at the top of the table. You can also go to the Analysis Dashboard to view unsubscribed rates across any campaign, newsletter, and API-triggered broadcast. Check which type of workflows you want to compare at the top. Then select the table icon to add Unsubscribed from topics as a column. Tracking subscriptions over time You can track the evolution of the volume of subscribed or unsubscribed profiles for a subscription topic or channel by creating data-driven segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions.. The examples below use topic preferences, but the same logic applies to channel preferences. Use the attribute cio_subscription_preferences.channels.<channel> (for example, cio_subscription_preferences.channels.push) instead of cio_subscription_preferences.topics.topic_<id> when building segments for channels.  Subscription preferences attribute Keep in mind that users do not have a subscription preference attribute until their status has been explicitly set, which means it is no longer the default status you set in your subscription center. As such, we’ll have to target attributes based on whether they exist as well as when they equal certain, boolean values. When creating a data-driven segment, a subscription topic will only show in the dropdown of attributes AFTER at least one person’s preference has been explicitly set for the topic. At this point, the topic will also show as an attribute in the data index. For this recipe, let’s say a workspace has 2 topics: an opt-out topic and an opt-in topic. Remember that opt-out means people are subscribed by default (they must opt-out to stop receiving messages) and opt-in means people are unsubscribed by default (they must opt-in to receive messaging). Tracking people that are subscribed to a topic To track the evolution of subscribers to the 2 topics above, create 2 data-driven segments: Opt-out topic For an opt-out topic, people are subscribed if they have not unsubscribed from it, that is, their subscription comes from the default status of the topic (so the attribute doesn’t exist) or if they have explicitly subscribed to the topic (so the attribute exists and equals true). To track all people who are subscribed to an opt-out topic, set this condition: Attribute cio_subscription_preferences.topics.topic_<id> is NOT equal to FALSE where topic<id> corresponds to the topic ID you can find in your workspace’s subscription center or fetch through the App API. Opt-in topic For an opt-in topic, people are subscribed only if they have explicitly subscribed to it (so the attribute exists and equals true). To track all people who are subscribed to an opt-in topic, set this condition: Attribute cio_subscription_preferences.topics.topic_<id> is equal to TRUE where topic<id> corresponds to the topic ID you can find in your workspace’s subscription center or fetch through the App API. Tracking people that are unsubscribed from a topic To track the evolution of unsubscribed users to the 2 topics above, create 2 data-driven segments: Opt-out topic For an opt-out topic, people are unsubscribed only if they have explicitly unsubscribed (so the attribute exists and equals false). To track all people who are unsubscribed to an opt-out topic, set this condition: Attribute cio_subscription_preferences.topics.topic_<id> is equal to FALSE where topic<id> corresponds to the topic ID you can find in your workspace’s subscription center or fetch through the App API. Opt-in topic For an opt-in topic, people are unsubscribed by default (so the attribute doesn’t exist) or if they have explicitly unsubscribed from the topic (so the attribute exists and equals false). To track all people who are unsubscribed to an opt-in topic, set this condition: Attribute cio_subscription_preferences.topics.topic_<id> is NOT equal to TRUE where topic<id> corresponds to the topic ID you can find in your workspace’s subscription center or fetch through the App API. Data-out integrations You can use our reporting webhooks to send information on whether a person is subscribed or unsubscribed from messaging and whether the status of any of their subscription preferences has changed. In order to send the specific values of people’s preferences, check “Include body content and headers in all Sent events” to the right of the event checklist. We’ll also send subscription preferences as a part of the Attributes schema in our outbound data warehouse integrations. --- ## Multi-language support for the subscription center URL: https://docs.customer.io/journeys/subscription-center-translation/ You can translate, aka localize, your subscription center in *Workspace Settings*. To translate custom copy on your subscription preferences page, like topics and a custom header and headline, you first need to add a topic and, optionally, customize your heading. You can localize your subscription preferences page before and after enabling your subscription center. We currently support 29 languages. Please let us know if you need support for another one! Factors that influence how translations render for your audience The following factors influence which languages your users will see on their subscription preferences page in this order: The language parameter you add to the unsubscribe URL in emails. For example: {% unsubscribe_url lang='fr' %}. This also applies to the manage preferences URL like {% manage_subscription_preferences_url lang='es' %}. If you don’t provide a language parameter, then the language preferences in your recipients’ browser settings take precedence. If you don’t provide translations for any of your recipients’ preferred languages, then they’ll see the language you created your topics and custom header copy in. In our UI, we call this the default language. The standard copy (like buttons) would render according to the recipients’ browser settings if the language is supported by Customer.io.  Note on supported languages and browser settings There can be a disconnect between the language your users see for topics and the language they see for static copy. When there is no custom translation for your content (topics and custom headers), then the standard, static copy (like buttons and default header) will continute to display according to your recipients’ browser settings if the language is supported. This means your users could see two languages on your subscription preferences page. For example: If their preferred language is French, one of the 29 languages we support, then their browser will surface the French translation provided by Customer.io for standard copy. But the topics and custom header would show in your default language, which may not be French. If their preferred language is Arabic, a language we do not support currently, then the translation they see for standard copy will be English. Your custom copy - topics and headers - would show in your default language. Some browsers will automatically translate the page into their preferred language, though. Your users can always change the language of static copy using the language selector in the top-right corner of the page. If you have translations for your custom copy that match the dropdown selection, those will also appear. Add or edit translations Customer.io translates standard, static copy on global unsubscribe pages, unsubscribe pages, and subscription preferences pages. This includes headers and button copy. We do not automatically translate any of your custom copy, including topics, channels, or custom headings. To ensure your subscription preferences page is translated correctly for all of your users, you can add translations through your subscription center settings. The steps below use topics as an example, but channel names and descriptions can also be localized the same way. To localize your subscription preferences page, you must create at least one topic. Go to Settings > Workspace Settings > Subscription center > Settings. Click Add languages beside Localization. Select the languages you want to support and click Add. From here, click the pencil icon to set translations for your topics and channels. Click Save. If you edit the default language of a topic or add a new topic, your subscription preferences page will still render—even if you haven’t updated or added translations! If this happens, we’ll show a warning icon for the affected items, reminding you to update your translations. Edit the status of translations Just because your subscription center is enabled does not mean your translations are live to users. You must separately publish each translation. You can make a translation live in edit mode or from the table listing: Go to Settings > Workspace Settings > Subscription center > Settings. Check the box beside each language you want to make live under Localization. Select Make live at the top of the table. If Make live is disabled, check to see if you’ve finished translating the copy for each language you checked. The status for each translation will change to Live. To unpublish a translation from your subscription preferences page, follow the same steps above but select Make not live instead. You will see a banner message confirming your action. Preview your subscription preferences page at the top of Settings to review your changes. Delete translations You can delete a translation in edit mode or from the table listing. To delete a translation from your subscription preferences page and workspace: Go to Settings > Workspace Settings > Subscription center > Settings. Check the box beside each language you want to delete under Localization. Select Delete at the top of the table. You’ll see a success banner indicating how many languages you just deleted. --- ## Migrate subscription preferences URL: https://docs.customer.io/journeys/migrate-subscription-prefs/ You may have managed your audience's subscription preferences manually—with your own attributes in a system outside of Customer.io. Before you use our subscription center feature, you probably want to migrate your audience's preferences to the `cio_subscription_preferences` attribute so your audience's preferences still apply when you enable the subscription center.  These steps will not affect your audience’s global subscription status This page is about migrating subscription preferences—the topics and channels a person wants (or does not want) to receive messages for. These steps will not affect your audience’s unsubscribed attribute—which indicates whether a person has opted out of messages. How it works If you used to manage subscription preferences outside our subscription center feature, or you set up new “opt-in” topics or channels, you’ll want to apply people’s current preferences to the cio_subscription_preferences attribute. This ensures that you continue to observe your audience’s preferences when you enable the subscription center feature in Customer.io. To migrate your audience’s preferences, we’ll: Create a segment of people who have subscription preference attributes Use this segment as a campaign trigger In the campaign, use a Create or Update Person action to set current preferences as topics and channels in the cio_subscription_preferences object. The subscription preferences attribute You can set your audience’s subscription preferences using the reserved cio_subscription_preferences attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. This attribute contains both topics and channels preferences where every individual preference is a boolean (true/false). Topics are numbered based on the ID that you see in the UI—topic_1 corresponds to ID 1 in the left-column in your Subscription Center setup page. We set subscription preferences by topic ID rather than the topic Name, so that you can change the name of a topic without affecting your audience’s preferences. Channels use the channel type name as the key (email, sms, push, in_app, whatsapp, slack, line, inbox). Channel preferences let people opt in or out of specific messaging channels, independent of their topic preferences. When you send a message, a person must be subscribed to both the relevant topic and channel to receive the message. { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": false }, "channels": { "email": true, "sms": true, "push": false } } } Migrate subscription preferences There are plenty of ways to map subscription preferences. In this case, we’re using a campaign, but you could also use our API, upload a CSV, etc. Before you start this process, we recommend that you: Create your topics and channels Apply topics to your campaigns and broadcasts Figure out all the places where you’ll need to update subscription preferences. If you let your audience set their subscription preferences when they sign up, or inside your mobile apps, etc, you’ll want to make sure that you update those places to use Customer.io’s subscription center attributes as well. Before you begin: relate preferences to topics and channels In Customer.io, we store topic preferences by number (incremental, beginning at 1), so that changing the name of a topic doesn’t cause cascading changes to your audience. But that means that you’ll need to map your previous preference attributes to your new topic number. So, if your attribute was previously called basketball and is now your first topic, you’ll map your audience’s basketball preference to cio_subscription_preferences.topics.topic_1. Channel preferences use the channel type as the key—for example, email, sms, or push. If you previously tracked channel opt-outs with custom attributes (like no_sms), you’ll map those to cio_subscription_preferences.channels.sms. Create a segment In this process, we assume that you previously stored your audience’s preferences as individual attributes, but the same basic process applies if you previously stored preferences as an object, array, etc. Go to Segments and click Create Segment. Set a Name and Description for your segment, and then click Create Data-driven Segment. Set up your campaign so At least one of the following conditions match. This ensures that people with any preference join your segment. Add conditions where each “preference” attribute exists. Click Save Changes. Now you’re ready to set up a campaign to move your preference attributes to the new cio_subscription_preferences attribute. Create your migration campaign This is a very short campaign with one step that simply sets your audience’s preferences. We’ll use the segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. that you created as the trigger. And then we’ll map your previous migration preferences to attributes. Go to Campaigns and click Create Campaign. Set a Name and Description for your campaign. Click Choose trigger then select Attribute or Segment. Select the segment you created earlier. Click Save. In your workflow, add a Create or Update Person action, select it, and click Add Details. Set subscription preferences via JavaScript. Set and overwrite all preferences Set and overwrite all preferences The Attribute is cio_subscription_preferences. The Value is JavaScript. Don’t worry if you don’t know JavaScript; we’ve provided an example below! In the final box, add JavaScript to map your data to the subscription preference attribute. In our example, we check if the baseball and basketball attributes exist. Since these attributes are boolean, we can map the value directly to the new subscription preference, if they exist; otherwise, we assume that a person isn’t subscribed to the corresponding topic (false). Be sure to include mappings to all topics and channels because this method does NOT preserve existing preferences for unspecified items. Those would revert back to the default opt-in/out status. return { topics: { topic_1: customer.baseball ? customer.baseball : false, topic_2: customer.basketball ? customer.basketball : false }, channels: { email: customer.wants_email !== false, sms: customer.wants_sms !== false, push: customer.wants_push !== false } } Set preferences and preserve others Set preferences and preserve others Add a line for each topic or channel you want to set: For topics, the Attribute is cio_subscription_preferences.topics.topic_<id> where id is the system-generated id for the topic name in the subscription center. For channels, use cio_subscription_preferences.channels.<channel> (for example, cio_subscription_preferences.channels.email). The Value is JavaScript. In the final box, enter JavaScript to return the preference value from an attribute or a default if the attribute doesn’t exist. This method updates only the specified preferences and preserves existing preferences for unspecified topics and channels. return customer.<attribute_name> ?? <boolean>; (Optional) Delete the attribute that you’re migrating from. After we apply the baseball attribute to topic_<x>, we probably don’t need to keep it around! After you’ve added all of your attributes, click Save and start your campaign. Update subscription preferences via the API For each person in your workspace, you can set subscription attributes using the identify action in our API. Your payload changes based on whether you use our Pipelines or Track APIs, but we’ve posted examples of both below. To update some preferences while preserving others, use JSON dot notation: "cio_subscription_preferences.topics.topic_<topic ID>":<boolean> for topics or "cio_subscription_preferences.channels.<channel>":<boolean> for channels. Pipelines API (Recommended) Pipelines API (Recommended) https://cdp.customer.io/v1/identify { "userId": "cool.person@example.com", "traits": { "cio_subscription_preferences": { "topics": { "topic_1": true }, "channels": { "email": true, "sms": true, "push": false } } } } Classic Track API identify Classic Track API identify https://track.customer.io/api/v1/customers/cool.person@example.com { "cio_subscription_preferences": { "topics": { "topic_1": true }, "channels": { "email": true, "sms": true, "push": false } } }  You can also update in batches The Pipelines API supports a /batch endpoint that lets you send changes for multiple people in a single request. Set subscription preferences using the web SDK If you use our JavaScript SDK or Classic JavaScript SDK on your website, you can set subscription preferences with the identify function. You might want to do this if you let people set their subscription preferences as a part of your signup flow. JavaScript Source (Recommended) JavaScript Source (Recommended) cioanalytics.identify('cool.person@example.com', { first_name: 'cool', last_name: 'person', cio_subscription_preferences: { topics: { topic_1: true, topic_2: false }, channels: { email: true, sms: true, push: false } } }) Classic JavaScript SDK Classic JavaScript SDK _cio.identify({ email: "cool.person@example.com", first_name: 'cool', last_name: 'person', cio_subscription_preferences: { "topics": { "topic_1": true, "topic_2": false }, "channels": { "email": true, "sms": true, "push": false } } }) Upload subscription preferences via CSV You can set some or all subscription topic preferences for people by importing a CSV in the People tab. Set one or more topic preferences for a person You can use this method to update any and all subscription preferences for people without overwriting preferences for topics not specified in the CSV. Add each subscription center topic name you want to set as its own column header. Upon upload, you’ll map each header to the attribute cio_subscription_preferences.topics.topic_<topic ID> where the topic ID corresponds to the topic name. You can find this on your subscription center landing page or by retrieving subscription center topics in our App API. After you complete the import, you’ll see that only the topic preferences you specified in the CSV show changes on the person’s profile. Before uploading, another option is to add a column header that already matches the JSON dot notation above. When you go to map fields, the correct attribute name automatically populates. Set all topic preferences for a person You can use this method to update ALL topic preferences per person, not a selection of topic preferences.  Include every topic and value per person If you do not include all topic preferences for each person using this method, the person’s preferences that are not specified in the import will be overwritten to match the default opt-in/out status of the topic. You can import subscription preferences for a person by adding a column header cio_subscription_preferences and including the same JSON structure that our API expects. The contents of this column are topics objects, as follows: email,first_name,cio_subscription_preferences person@example.com,person,{"topics":{"topic_1":true,"topic_2":false}} another.person@example.com,another,{"topics":{"topic_1":true,"topic_2":true}} See this spreadsheet for an example. --- ## Set preferences outside of the subscription center URL: https://docs.customer.io/journeys/set-preferences-outside-center/ Let your audience manage their subscription preferences outside of your Customer.io messages—in your app, during sign-up, or anywhere else you want to provide a link. How it works Customer.io’s Subscription Center feature lets your audience set their subscription preferences when they click Unsubscribe or Manage your preferences in your messages. But you might want to let your audience proactively manage their preferences outside of messages—like as a part of your in-product settings or as a part of your sign-up flow. There are two ways to do this: Link to the hosted subscription center — Generate a signed URL that takes a person directly to your Customer.io-hosted subscription center page. This is the simplest approach and requires no custom UI. Build a custom subscription preferences page — Use our APIs to fetch and update preferences within a page that you host. This gives you full control over the design and experience. Generate a subscription center link You can use our API to generate a signed URL that links directly to your Customer.io-hosted subscription center. Your users can manage all of their topic preferences from this page—no custom form required. This is useful when you want to provide a subscription center link in places like: Your app’s account settings or profile page A welcome or onboarding email sent through a custom system An SMS or push notification flowchart LR a[Your app or backend service]-->b subgraph App API direction LR b[Generate subscription center token & URL] end b-->c[Person visits hosted subscription center] Call the Generate a subscription center token endpoint with the person’s identifier: curl --request GET \ --url https://api.customer.io/v1/subscription_center/person@example.com/token \ --header 'Authorization: Bearer YOUR_APP_API_KEY' The response includes a token and a ready-to-use url: { "token": "abc123...", "url": "https://track.customer.io/u/i/abc123.../" } Serve the url value as a link for your customer. The token is valid for 24 hours.  Custom link tracking domains If you use a custom link tracking domain, you can construct the URL yourself using the token: https://<your-tracking-domain>/u/i/<token>/<language-code>. The language code is optional. Create a custom subscription preferences page If you need full control over the branding and experience of your subscription preferences page, you can use our App and Track APIs to fetch and update preferences within a page that you host. Use our App API to fetch subscription preferences for your customers and our Track API to update subscription preferences. You can also set preferences using our Web SDK. You can set your audience’s subscription preferences using the reserved cio_subscription_preferences attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. This attribute contains both topics and channels preferences where every individual preference is a boolean (true/false). Topics are numbered based on the ID that you see in the UI—topic_1 corresponds to ID 1 in the left-column in your Subscription Center setup page. We set subscription preferences by topic ID rather than the topic Name, so that you can change the name of a topic without affecting your audience’s preferences. Channels use the channel type name as the key (email, sms, push, in_app, whatsapp, slack, line, inbox). Channel preferences let people opt in or out of specific messaging channels, independent of their topic preferences. When you send a message, a person must be subscribed to both the relevant topic and channel to receive the message. { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": false }, "channels": { "email": true, "sms": true, "push": false } } } flowchart LR a[create custom form outside Customer.io]-->b subgraph App API direction LR b[fetch preferences on page load] end b-->c subgraph Track API or Web SDK direction LR c[set preferences when form is submitted] end App API: Fetch preferences You can fetch subscription preferences from your workspace using our App API endpoint: /v1/customers/{customer_id}/subscription_preferences. The response includes both topic and channel preferences. const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'api.customer.io/v1/customers/sdade8@example.com/subscription_preferences?id_type=email', headers: { 'Accept': 'application/json', 'Authorization': 'Bearer' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); API: Set preferences If you have your own backend integration, you can send subscription preferences—including both topic and channel preferences—to your workspace through our APIs or our libraries. We’ve provided some examples below. Our Pipelines API preserves preferences by default. Our classic Track API does not. If you use our classic Track API, we’ve provided examples to help you preserve or overwrite existing preferences depending on your use case. Channel preferences work the same way as topic preferences in all APIs. Pipelines (Recommended)Track v1 (Classic): Set & overwrite prefsTrack v1 (Classic): Set & preserve prefsTrack v2 (Classic): Set & overwrite prefsTrack v2 (Classic): Set & preserve prefs Pipelines (Recommended) The Pipelines API automatically preserves any preferences you don’t set. Using our example below, if there was a topic_4 that was set to true, it would remain true after the update. You can reset all preferences by passing an empty topics object. The same behavior applies to channel preferences in the channels map. This example uses our API directly, but we also have client-side JavaScript and server-side libraries that can make things easier. const axios = require('axios'); let data = JSON.stringify({ "userId": "customer@example.com", "traits": { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": true, "topic_3": false }, "channels": { "email": true, "sms": true, "push": false } } } }); let config = { method: 'post', maxBodyLength: Infinity, url: 'https://cdp.customer.io/v1/identify', headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic <yourApiKey>:' }, data : data }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); Track v1 (Classic): Set & overwrite prefs const axios = require('axios'); let data = JSON.stringify({ "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": true, "topic_3": false }, "channels": { "email": true, "sms": true, "push": false } } }); let config = { method: 'put', maxBodyLength: Infinity, url: 'https://track.customer.io/api/v1/customers/sdade8@example.com', headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic' }, data : data }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); Track v1 (Classic): Set & preserve prefs const axios = require('axios'); let data = JSON.stringify({ "cio_subscription_preferences.topics.topic_2": false, "cio_subscription_preferences.channels.email": true }); let config = { method: 'put', maxBodyLength: Infinity, url: 'https://track.customer.io/api/v1/customers/sdade8@example.com', headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic' }, data : data }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); Track v2 (Classic): Set & overwrite prefs const axios = require('axios'); let data = JSON.stringify({ "type": "person", "identifiers": { "email": "sdade8@example.com" }, "action": "identify", "attributes": { "cio_subscription_preferences": { "topics": { "topic_1": true, "topic_2": true, "topic_3": true }, "channels": { "email": true, "sms": true, "push": false } } } }); let config = { method: 'post', maxBodyLength: Infinity, url: 'https://track.customer.io/api/v2/entity', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Basic' }, data : data }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); Track v2 (Classic): Set & preserve prefs const axios = require('axios'); let data = JSON.stringify({ "type": "person", "identifiers": { "email": "sdade8@example.com" }, "action": "identify", "attributes": { "cio_subscription_preferences.topics.topic_1": false, "cio_subscription_preferences.channels.push": false } }); let config = { method: 'post', maxBodyLength: Infinity, url: 'https://track.customer.io/api/v2/entity', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Basic' }, data : data }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); }); Javascript/Web SDK: Set preferences If you use our JavaScript source (which we recommend) or our classic JavaScript SDK, you can set preferences with the identify function. You can identify a person when they first sign up for your service and set their preferences or when they update their preferences in a settings page that you make available to them. JavaScript client (Recommended)Classic JS: Set and overwrite all preferencesClassic JS: Set preferences and preserve others JavaScript client (Recommended) With our JavaScript client, you can pass an empty topics or channels object to overwrite a person’s preferences. Otherwise, we’ll preserve the preferences that you don’t update. _cio.identify({ userId: '019mr8mf4r', cio_subscription_preferences: { topics: { topic_1: true, topic_2: true, topic_3: false }, channels: { email: true, sms: true, push: false } } }); Classic JS: Set and overwrite all preferences  Want to preserve preferences with our classic JavaScript SDK? To set some preferences, while preserving those set for other topics or channels, you need to use JSON dot notation "cio_subscription_preferences.topics.topic_<topic ID>":<boolean> or "cio_subscription_preferences.channels.<channel>":<boolean> in your request. To overwrite preferences, you’ll pass the entire cio_subscription_preferences object but you’ll stringify it. _cio.identify({ // request must contain an identifier like `id` or `email`, depending on the settings in your workspace email: 'cool.person@example.com', cio_subscription_preferences: JSON.stringify({ topics: { topic_1: true, topic_2: false, topic_3: false}, channels: { email: true, push: false } }) });  You must stringify the values in order for the attribute to successfully update on the person’s profile. Classic JS: Set preferences and preserve others _cio.identify({ // request must contain an identifier like `id` or `email`, depending on the settings in your workspace email: 'cool.person@example.com', // Set topic preferences using dot notation "cio_subscription_preferences.topics.topic_<topic ID>":<boolean>, // Set channel preferences using dot notation "cio_subscription_preferences.channels.email": true, "cio_subscription_preferences.channels.push": false }); --- ## Send from a personal support rep URL: https://docs.customer.io/journeys/assign-a-personal-support-rep/ Introduction Onboarding is hard to get right. You want to show off the benefits of your app at a large scale while creating a personal touchpoint. Many of our customers introduce a dedicated customer support person to their end user as soon as they sign up. The new user can then reach out directly with any concerns or questions. This way, end users know that they’re getting one-to-one attention from the start, and that someone will be there to help them as they discover the app. Customer.io lets you set this up easily, resulting in personalized onboarding emails that look like this: Hi Jane, Nice to meet you! I’m John and I work out of our Manhattan office. I wanted to take the opportunity to welcome you, and let you know that if you have any questions, I’m here to help! You can get in touch at this address or call 123-456-7890. Have a great day! John S. In this recipe, you’ll learn how to assign a personal support or account representative. Ingredients Basic knowledge of Liquid tags Ability to send attributes to Customer.io Method Store the attribute When you create a new end user in Customer.io, we recommend that you store their personal support rep’s information as part of their attributes. This is an example of how you might do that with the identify() function in our JavaScript library: JavaScript Source (Recommended) JavaScript Source (Recommended) cioanalytics.identify('1898', { email: 'jane@example.com', created_at: 1743259962, // Custom user attributes first_name: 'Jane', plan_name: 'free', rep_name: 'John', rep_email: 'john@company.com' }); Classic JavaScript SDK Classic JavaScript SDK _cio.identify({ id: '1898', // must be unique per customer email: 'jane@example.com', created_at: 1743259962, // Custom user attributes first_name: 'Jane', plan_name: 'free', rep_name: 'John', rep_email: 'john@company.com' }); Here, I’ve used rep_name and rep_email, but you can define them however you like. Then, when you check out this person in Customer.io, their attributes look like this: Create a “From” address using Liquid tags. Head to Workspace Settings in the left-hand menu, select Email and then define a new From Address using Liquid. Make sure that you input the same terms that you used on the customer attribute. We used rep_name and rep_email earlier, so we made sure to reuse them here. If you used something else (support_rep, for example), your From Address might use the Liquid tags {{customer.support_rep_name}} and {{customer.support_rep_email}}. Select that “From” address in composer When you’re creating your onboarding campaign, select your new From Address: And use the same Liquid tags when composing your message: Tip! You can add all sorts of attributes for a personal support rep that can help you customise your message: city, phone number, and photo are a few examples. That’s it! We’ll merge in the from address when we generate the email for that user. Wrap Up Assigning end users a personal support rep is a great way of approaching onboarding. This way, whenever people reply to onboarding emails, it goes straight to their rep, someone who is already familiar with them and their journey through the app. As a result, your customers get the tailored, empathetic support they deserve. If you have questions about this recipe or how to apply it to your business, get in touch! --- ## Find messages using the Message Library URL: https://docs.customer.io/journeys/message-library/ The Message Library helps you search for messages across your campaigns, broadcasts, and newsletters. This makes it easier to find specific messages, or campaigns containing a specific message. Go to Message Library to see a list of all the messages in your workspace and their current status. You can search for messages by name, subject, content, or from address. You can sort messages based on when they were last updated. Click to switch between ascending and descending sorting. Click any message in the list to go directly to the message. From here, you can edit your message or edit settings in the campaign or broadcast containing the message. Search for and filter messages You can search for messages in your workspace by name, subject, content, or even the from address it sends from. We do support partial results, making it easy to find messages based on a particular word or phrase you use in your messages. Additionally, you can filter the results on the page by status (Sent, Active, Drafted, etc.) and message type (Email, Push Notification, SMS, etc). Click on a result to go directly into the message. If your message is in an active campaign and you want to make changes, learn more about how changes to your message would affect people in the campaign. --- ## Assets library: store files URL: https://docs.customer.io/journeys/asset-library/ Your workspace has an Assets library where you can store images and PDFs for reuse across messages (email, in-app, etc.). How it works Any images you upload are stored in your Assets page. You can upload images in bmp, jpeg, png, and gif formats. You can store PDFs that you then link to from a message. File size limits You can upload images and PDFs to your Assets library, but limits apply: Maximum image width and height: 4096 px Files must be smaller than 2 MB. For optimal loading times, we recommend that you upload images smaller than 1 MB.  You can upload and optimize images larger than 2 MB through Design Studio Learn more about image optimization while creating a message in Design Studio. Add files To add a file to Assets, you can go to Content > Assets or upload an image while creating a message. From Assets, click Upload Assets to get started. Insert an image Learn more about adding images through our editors: Email Design Studio Drag-and-drop Rich text: Click in the toolbar. Code: Click in the toolbar. In-app: Click an image-based component and choose your file. Push SMS/MMS Slack Link to an image (push and SMS) For push notifications and SMS/MMS, you’ll need to upload your images to Assets first then copy the link and paste it into your message. Go to Content > Assets, and find the image you want to copy. Hover over the image, and click . Go to your message and paste the URL where you need to use your image. Learn more about adding links to push and SMS. Link to a PDF If you want to include a PDF for your customers, you’ll upload it to Assets first, and copy the link. Then you’ll hyperlink text in your message to the PDF. The way you link to a PDF depends on the type of editor you use. Learn more about email attachments and transactional messages. Design Studio In Design Studio, you can link to PDFs in text-based components. Go to Assets, and click to copy the PDF link. Go to your email, highlight some text, and click from the bubble menu. Leave “URL” as the type, and paste in your PDF link. Drag-and-drop email editor In the drag-and-drop editor, you can link to PDFs in text blocks. Click , change the Link Type to “File Manager,” and select your PDF. Code email editor In the code editor, click Insert, and select your PDF to insert a link to the file. We automatically hyperlink the word “PDF”, so remember to change that text to something that suits your message. Rich text email editor Click Insert, and click to copy the link to your PDF. Highlight the text, click , and paste the link you copied. Organize your files You can organize your images and PDFs into folders and move your files around through drag-and-drop. From Assets, click New Folder, and enter the name of your new folder. Then move your files: Click and hold to drag files into the folder or Hover over files, check the boxes, then click and hold to drag them into folders. Sort and filter assets By default, the Assets page sorts images by creation date in descending order; the most recent images are at the top. You can change the sort value and order to find your files. Use the dropdown to change the sort value between Created, Name, or Size. Select or to change the sort order. Click to display your images in a list and to show large thumbnails. Delete assets Hover over a file, and click to delete it. To delete multiple items, hover over files, check the boxes, and then click Delete. Deleting a file in use does not impact messages; you must remove or replace the file within a message editor to change the image. --- ## Image requirements URL: https://docs.customer.io/journeys/image-support/ The types of images and media you can use, and their maximum file sizes, depend largely on the platform you send messages to (email, mobile push, slack, etc). General image and media guidelines This page provides some guidance about the maximum file sizes and types supported by our Assets library and various platforms. However, you generally want to limit the size of your images to the smallest possible file sizes. While some messages support larger file sizes, it’s best to limit file size to reduce load time. A person viewing your message on their phone using cell service, for instance, might have trouble loading messages with large images or attachments. Limiting the size of your media helps ensure that your messages load in a timely manner. Images in the Assets page You can upload images to the Assets page. We’ll host your images and let you reuse them across messages. Supported file types: BMP, JPG/JPEG, PNG, GIF Maximum width and height: 4096 px Maximum size: 2 MB  You can upload files to our Assets page that might not work in messages The Asset page supports a general array of image types up to 2 MB in size. Some message types don’t support images that large or may not support BMP images. You can move images into folders in your asset library without impacting the images’ links. This means that reorganizing images in the asset library won’t cause messages to fail to send in live campaigns.  You can upload and optimize images larger than 2 MB through Design Studio Learn more about image optimization while creating a message in Design Studio. Images in emails The image types and sizes we support depend on whether you host your images yourself or upload images directly through our editors. Uploading images You can upload images from a message editor and reuse them across your messages: Supported file types: JPG/JPEG, PNG, GIF Maximum width and height: 4096 px Maximum size: 2 MB  You can upload and optimize images larger than 2 MB through Design Studio Learn more about image optimization while creating a message in Design Studio. Hosting your own images Hosting your own images lets you control your own image URLs and provides more flexibility with the size or format of your images. However, while hosting your own images can provide greater flexibility in the sizes and types of images you add to your messages in Customer.io, your audience’s email clients may support different sizes and types of images. We recommend limiting your hosted images to common file types (.jpg, .jpeg, .gif, or .png) and reasonable sizes (smaller than 1 MB) to ensure that your audience sees your whole message, no matter what client or provider they use. When you host your own images: URLs must end in a known image file extension, i.e. .jpg, .jpeg, .gif, .png. Supported file extensions are determined by individual recipients’ email clients. Images in push notifications You can send images as a part of custom push notification payloads. While you host the images that you send in push notifications, your images are still subject to the requirements of the platform you send notifications to—Android or iOS. If you use an image in a notification intended for people on both Android and iOS platforms, you should use the most restrictive image requirements to ensure that your audience receives your notification as intended, no matter their device platform. Android push notification image support When you provide an image URL in a custom push notification payload, your audience gets the notification and then downloads the image after the notification is on the phone. Supported file types: BMP, GIF, JPEG, PNG, WebP, HEIF (requires Android 8.0+) Maximum size: 1 MB { "message":{ "notification": { "body" : "This is an FCM notification that displays an image.!", "title" : "FCM Notification", "image": "url-to-image" } } } iOS push notification image support If you want to send an image as a part of an iOS push notification, you need to add a notification service extension. The service extension allows client devices to receive notifications from your push. iOS supports JPEG, GIF, and PNG images. { "aps": { "mutable-content": 1, "alert": { "title": "Message with image!", "body": "Check out this image!" } }, "image": "url-to-image" } Images in Slack You can share images that you host or from any hosting service with a slack channel. Images will preview in the channel if your hosting URL ends in a known image extension—.jpg, .gif, .png, etc. While Slack technically supports files up to 1 GB, large images likely won’t preview properly in a channel—especially for members of the channel using their phone or with slower connection speeds. Try limiting the size of your images so everybody in the channel can see them. --- ## Message Statuses URL: https://docs.customer.io/journeys/message-statuses/ Each individual message that you send to a person travels through Customer.io, to a delivery provider, and ultimately to a person. Customer.io logs the status of each individual delivery and journey, so you know whether or not a person has received a message, opened it, and responded to it. We also aggregate these statuses as metrics, so you know not only how well a message performed with an individual person, but how well it was received by your audience at large. This page is designed to help you understand the various states your messages progress through, and how we display those states in different areas of Customer.io. Status vs. Metric A Message Status explains the state of an individual delivery (the instance of a message intended for an individual) or journey (the instance of a campaign that a person travels through). We track each delivery’s progress from when we first generate and try to send it, whether or not we’re successful, and how a person interacts with it. For example, a message that is Queued is not counted in your Sent metrics, because it hasn’t been sent yet. As you look at message metrics, you should consider the time since you sent the message: have people had enough time to receive or open the message? This time is likely to change based on the type of message you sent: SMS and push notifications tend to see more immediate responses compared to emails, but also contain less information and maybe have a smaller, more confined scope. Metrics aggregate message and journey statuses, providing information about the performance of your messages and campaigns—the percentage of people who opened a message or finished a campaign journey. Metrics are generally based on the total number of delivered messages, but may require the delivery provider or audience to report back before we generate metrics. So, a message or delivery may have a status—like “drafted” or “sent”—but may not have generated metrics if a person hasn’t opened your message or clicked a tracked link yet.  Message statuses and metrics vary by message type The type of message that you send determines both the statuses that Customer.io reports, and the metrics we gather. See statuses by message type to learn more about the states each message type may have. For example, here’s an email with a Sent status. It has been Sent (its most recent interaction) and Opened, but it has not been clicked or driven a conversion: All of these things are aggregated to form campaign metrics. The Deliveries log shows you the status of your most recent deliveries. When does Customer.io get the status of a message? There are three different entities that report message statuses: Customer.io, your delivery provider, and the person you send messages to. Whether or not you see a particular message status in Customer.io depends on both an event happening (i.e. a person opened a message), and a the responsible entity reporting that event to Customer.io. So, beyond the message statuses that we generate before sending a message to a delivery provider, there may be a delay between the delivery provider or a person interacts with your message, and when we update the message status or relevant metrics in your dashboard. This chart shows the different message statuses for an email, the parties responsible for reporting each message status, and the general flow of statuses. Set up reporting webhooks Each message status represents an event that you can send to an external source with Reporting Webhooks. Reporting webhooks call an endpoint you provide with an HTTP POST whenever Customer.io reports a change to a message status that you want to subscribe to. The webhook request includes a JSON payload with information about the event—information from Customer.io that you can feed directly into your customer relationship management (CRM) platform or another backend to track your audience’s progress along a messaging path. Message status before send When you send a message to a person, it travels from Customer.io to the delivery provider, and the delivery provider is responsible for making sure that it gets to your recipient. A lot happens before a we send a message to a delivery provider. The following message states all occur within Customer.io before your message leaves Customer.io. Queued We’re processing a message and preparing to send it to the delivery provider. We’ve not attempted to send it yet, but we will soon. Drafted We’ve generated a draft of your message, but you must manually send it. We will not send it automatically. If you’re using the Queue Draft feature, your messages will sit in this state before you opt to send them. This gives you a chance to check your drafts before you send them or monitor the individuals you send messages to. See our Queue Draft documentation for more about this feature. Attempted We’ve started trying to pass your message on to the delivery provider (e.g. Mailgun in the case of emails, for example, or APNs/FCM for push notifications). In some cases, the attempted state can mean a retry-able failure. Failed This message did not leave Customer.io for the delivery provider. See this article to understand and diagnose why your message never left Customer.io. In many cases, messages fail due to missing liquid variables, or a failure in liquid logic, resulting in an empty field or a misshapen message. Undeliverable A message was not sent to the delivery provider because the intended recipient unsubscribed, reached their message limit, or was deleted. If you have a message limit and you regularly see undeliverable messages, you may want to change your limit’s time window.  Retry undeliverable messages If messages are marked undeliverable because a person reached their message limit, you can retry the message when a person is within their message limit or retry the message and ignore applicable message limits for recipients. Message Statuses Reported by the Delivery Provider When a message leaves Customer.io, we rely on delivery providers to tell us whether or not a message has made it to the customer Sent A message has successfully made it from Customer.io to the delivery provider. It is their responsibility to ensure that the message is delivered, and we’re waiting for further information to determine if the user interacted with it. Bounced The user’s email address wasn’t valid. This could be a hard bounce (usually a permanent issue like a non-existent email address) or a soft bounce (a temporary issue like a full mail box). In the case of hard bounces for emails, subsequent messages to those addresses will be suppressed. Hard Bounces vs. Soft Bounces Hard bounces are permanent delivery failures caused by invalid email addresses, non-existent domains, or permanently blocked addresses. Customer.io immediately marks these as failed and adds the email address to your suppression list to prevent future delivery attempts. Soft bounces are temporary delivery failures that may resolve over time. Common causes include: Mailbox full Temporary server issues Message size too large Temporary recipient unavailability Soft Bounce Retry Logic  Retry logic only applies to emails sent through our delivery network If you use Custom SMTP, retry functionality depends on your sending provider. You may need to contract your provider to understand how they handle retries. When Customer.io encounters a soft bounce, we automatically retry delivery up to 8 times over 12 hours according to the following schedule: Attempt Timing 1 Immediate 2 10 minutes 3 15 minutes 4 30 minutes 5 1 hour 6 2 hours 7 4 hours 8 4 hours If all retry attempts fail within this 12-hour window, the email is marked as permanently failed with the reason Too old. Mailgun Retry Rules Our email delivery partner Mailgun handles retries based on the type of error response: 4xx errors (client errors): Generally retried according to the schedule above, with exceptions for blacklist-related rejections which are treated as permanent failures 5xx errors (server errors): Treated as permanent failures and not retried Mailbox full errors: Not automatically added to the suppression list, though it’s best practice for senders to monitor and manage these cases If a soft bounce isn’t initially registered, Mailgun may retry more frequently based on server conditions and response times. Delivered Customer.io has received confirmation from the delivery provider that a message was sent to the recipient’s email service provider (ESP). Where we calculate campaign and message metrics for emails, we use Delivered as the basis for open, click, conversion, and unsubscribe calculations by default. If you do use a Custom SMTP server or otherwise don’t send Delivered metrics through your ESP, you can base metric calculations on the number of messages Sent instead. You can change the default denominator for metrics in your workspace settings. We’ll also infer delivered metrics from clicks and opens—because a message must have been delivered before someone opened or clicked it. We attempt to track Delivered for push notifications, but it may be blocked by the device operating system. Use Opened or Sent metrics instead; Sent indicates that a delivery token is valid and is eligible to receive your message. Status after successful delivery After a person receives a message, we receive statuses from a few different places: your delivery provider, the recipient directly, or programmatically from you! The delivery provider reports statuses like bounces, spammed messages, and suppressed emails. The delivery provider is in a position to know when messages don’t make it to a person, or are otherwise rejected by a person, and they report that information back to Customer.io. In many cases, a person directly reports how they interact with a message. They open an email (which has a tracking pixel reporting directly to Customer.io that they opened a message); or they click a tracked link in a message. These statuses reflect interaction. In cases where the delivery provider or a person cannot report a status, you can report a status back to us. For example, you can report push notification opens back to us. Or if you use a custom subscription center, you can report unsubscribes to use through the API. Opened For email, we count an open when the recipient’s email client loads an invisible image (tracking pixel) or when the recipient clicks one of the links in the email. If a customer has images disabled in their email client and an open is tracked based on a click event, the “Opened Email” and the “Clicked Email” events will show as having occurred at the same time. Email open metrics can be misleadingly high or low due to email client behavior. Use email opened metrics to track trends over time and investigate potential inbox placement issues. For in-app-messages, the opened metric means the message has been delivered and displayed to a person. For push notifications, our SDKs automatically track when they’re opened. If you don’t use our SDKs, you need to send us events to track push notification opens. Human opened This metric is available for emails opened since March 20, 2025. Compared to the opened metric, the human opened metric excludes emails opened by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, user agents identified as bots, and known scanners—based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. Machine opened This metric is available for emails opened since March 20, 2025. This metric includes opens by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, and user agents identified as bots. It also captures opens identified as known scanners, based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. In the UI, this metric is calculated as the difference between Opened and Human Opened to ensure Opened reflects the total. Clicked If link tracking is enabled for a message, the user clicked a link in the message. You can also do link tracking to see metrics for individual links that your audience clicks. We do not track Clicked metrics or do link tracking for push notifications. When a person taps a push notification, the message is marked as Opened. Human clicked This metric is available for tracked links in emails since April 20, 2025. Compared to the clicked metric, the human clicked metric excludes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. Machine clicked This metric is available for tracked links in emails since April 20, 2025. This metric includes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. In the UI, this metric is calculated as the difference between Clicked and Human Clicked to ensure Clicked reflects the total. Converted A person matched your conversion criteria. In general, we attribute the conversion to the last message a person received before matching conversion criteria. You can read more about how conversions work. Spammed A person marked your email as Spam with their email service provider. All subsequent emails for them are marked “suppressed.” Suppressed A message is suppressed in the following cases: The email address you sent a message to was previously a hard bounce (an invalid address). The recipient person marked any previous email message as spam. In this case, all subsequent email messages after the person marked your email as Spam are suppressed. When a message is marked as suppressed, the corresponding email address is automatically added to your email service provider’s (ESP) suppression list, preventing a person from receiving emails from you in the future (e.g. emails intended for these addresses are suppressed by the provider). If Customer.io is your delivery provider, you can find your email suppression list in your workspace settings. If someone has been inadvertently added to this list, or requests emails from you after previously marking a message as spam, you can remove them from the suppression list manually. If you manage deliveries through a Custom SMTP provider, you can go through them to find your suppression list. Statuses by message type Different message types produce different statuses. The table below lists the different states of each message type.   Email SMS WhatsApp Push In-App Slack LINE Webhook Drafted check_circle check_circle check_circle check_circle check_circle check_circle highlight_off check_circle Queued check_circle highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off Attempted check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle Failed check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle Sent check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle Delivered check_circle check_circle check_circle check_circle highlight_off highlight_off check_circle highlight_off Bounced check_circle check_circle highlight_off check_circle highlight_off highlight_off highlight_off highlight_off Suppressed check_circle check_circle highlight_off check_circle highlight_off highlight_off highlight_off highlight_off Opened check_circle highlight_off check_circle check_circle check_circle highlight_off highlight_off highlight_off Clicked check_circle highlight_off check_circle highlight_off check_circle check_circle check_circle check_circle Converted check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle Spammed check_circle highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off Unsubscribed check_circle highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off --- ## Deliveries & Drafts data URL: https://docs.customer.io/journeys/deliveries-drafts-data/ The Deliveries & Drafts page shows you a list of delivered or drafted messages across all journeys and provides a way to export data within a specified timeline. To view deliveries or drafts over a specific time period: Select Deliveries & Drafts from the left hand menu. Select either the Deliveries or Drafts tab. Filter for messages across any and all channels (email, in-app, etc). For delivered messages, you can search by status (sent, failed, attempted, etc), too. Click a message to view the message body and supporting data, such as the campaign it belongs to. Export to CSV To export deliveries or drafts over time: Select Deliveries & Drafts from the left hand menu. Select either the Deliveries or Drafts tab. From the Deliveries tab, filter by the channel and status you want in your export. The date filter on the table has no impact on the export; you’ll set that in the next step. From the Drafts tab, filter by the channel and date range (maximum 6 months at a time) you want to export. Select Export to CSV above the table (for Deliveries, you’ll choose your date range here - max 6 months at a time). Then confirm your export. The export may take a few minutes, depending on the amount of data. You will receive an email to download your CSV or you can find it on the Exports page (under Configure data, click More). Export fields id campaign_id - includes deliveries for API-triggered broadcasts campaign_name - includes deliveries for API-triggered broadcasts newsletter_id newsletter_name template_id - message id template_name - message name channel transactional_message_id transactional_message_name subject action_id - id of the workflow action item. For instance, if you dragged an email onto the canvas and clicked the item, you’d see the id on the top right of the left pane. parent_action_id customer_id recipient - the To field of the message created created_RFC3339* failure_message sent sent_RFC3339* delivered delivered_RFC3339* opened opened_RFC3339* clicked clicked_RFC3339* converted converted_RFC3339* bounced bounced_RFC3339* spammed spammed_RFC3339* unsubscribed unsubscribed_RFC3339* suppressed suppressed_RFC3339* failed failed_RFC3339* drafted drafted_RFC3339* topic_unsubscribed topic_unsubscribed_RFC3339* email - will not populate if you deleted the user from your workspace *RFC3339 is a date and time standard like so: 2023-05-09T18:20:45Z. Troubleshoot failed and attempted messages If you see Attempted or Failed as a status on your message, go to Failed and attempted messages for more information and help fixing any errors. --- ## Personalize messages with liquid URL: https://docs.customer.io/journeys/using-liquid/ Liquid is a templating language that lets you personalize messages based on your audience's data. Any part of your message can contain liquid, meaning that you're not just limited to personalized message content: you can use liquid to personalize your sender information, email subjects, and so on.  New to liquid? Check out our tutorial for marketers! Our tutorial can help you understand the fundamentals of message personalization and how to use your data to send engaging messages that increase conversion rates. When you’re ready, you can find examples of liquid for a variety of use cases in Liquid recipes. How it works Liquid lets you use variables and other logical statements in messages to personalize content. These can be customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., event data, and more. When you send a message through Customer.io (also called a deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery.), we process the liquid syntax in your messages to personalize the content for your recipient. We can break liquid down into three concepts: Tags define logic and control flow (like a for loop). Keys contain objects and attributes that dynamically render based on your recipient, like {{customer.first_name}}, where customer is the object and first_name is the attribute saved on your customer’s profile. Filters modify liquid output (you can filter a key) (like {{customer.first_name | capitalize}}). For example, if you store a person’s name as an attribute called first_name, you can reference the name with this key: {{customer.first_name}}. Then when you send the message, the person’s actual name replaces the liquid. If your liquid statements don’t evaluate properly—like if a person doesn’t have a first_name attribute—the message won’t send. This is to prevent people from receiving incomplete messages! You’ll see Failed in your message logs. To prevent messages from failing due to liquid errors, add fallback statements that render when someone doesn’t have an attribute. Hi {{customer.first_name | default:"Buddy"}}! flowchart LR a["message with {{customer.first_name}}"] a-->b{Does customer have first_name attribute?} b--->|yes|c[Hi Alex!] b--->|yes|d[Hi Blair!] b-.->|no|e{Is there a fallback?} e-->|yes|f[Hi Buddy!] e-.->|no|g[Message failure] style c fill:#B5FFEF,stroke:#007069 style d fill:#B5FFEF,stroke:#007069 style f fill:#B5FFEF,stroke:#007069 style g fill:#FFC4CF,stroke:#69002C  The default filter only works with our latest version of liquid. Find out which version you’re on with our Liquid upgrade article. If you’re using our legacy liquid, you can use if/else statements for fallbacks. Reference nested attributes Liquid uses JSON dot notation to access nested properties—data that’s not a single value, but an object or array of information. For example, imagine that someone refers a customer to you, and you store that referral as an attribute. You might have an attribute called referrer that contains information like how the referring party is related to the new customer. You could access those nested properties in your workflows with {{customer.referrer.relationship}} or {{customer.referrer.name}}. If you’re new to JSON and organizing data, check out our introduction to JSON. Preview liquid When you create a message, you can search for people and other data to check how your liquid renders. Liquid while editing Liquid with preview data At the top, the customer’s attribute first_name renders according to the person in the preview, Christiana. Towards the bottom, the message.subscription_topic.name key renders the fallback “these” because there is no subscription topic associated with the message. Then the now key renders the year. Personalize messages with Customer.io objects You’ll generally use three types of objects to personalize messages based on customers’ profiles and workflow trigger data: customer, event, and trigger objects. Object Liquid syntax When to use customer {{customer.<attribute_name>}} Use this to personalize messages with customer information. journey {{journey.<attribute_name>}} Use this to personalize messages with data from webhook actions. We store this data temporarily in a campaign, instead of a customer’s profile, so you can streamline your workspace data. event {{event.<attribute_name>}} Use this to personalize messages with data from an event that triggers a campaign. trigger {{trigger.<attribute_name>}} Use this to reference data that triggers a transactional message or API-triggered broadcast. You can also use this in webhook-triggered campaigns. {{trigger.<object_type_name>.<attribute_name>}} Use this to reference data from an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. that triggers a campaign. {{trigger.relationship.<attribute_name>}} Use this to reference relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. data for an object or relationship-triggered campaign. You can reference attributes (using the customer or journey keys) in any message, but you can only use event keys in event-triggered campaigns. To reference data for other trigger types, use the trigger key.  Check out other Customer.io-specific keys! We have keys for unsubscribing from messages and meta keys that help you track your messages. Customer attributes The {{customer.<attribute_name>}} key represents attributes associated with people. You can reference customer data, except anonymous data, in any message or action. Check out anonymous in-app messages for ways to reach unidentified users. For instance, to greet a customer, you could add their name: Hi {{ customer.first_name | capitalize }}! It’s best practice to set up a fallback, in case a recipient doesn’t have a first name. Learn more about fallback options below. Hi {% if customer.first_name %}{{ customer.first_name | capitalize }}{% else %}there{% endif %}! If you capture customers’ full names in a single field, you could break them apart with the split filter. This renders the first word in the field. {{ customer.full_name | split: " " | first }} You can find a complete list of our tags and filters in our liquid syntax article. Journey attributes Journey attributes are temporary attributes that you can set and update throughout a person’s journey. You can use them to store data essential for someone’s journey that doesn’t need to be stored for use outside of the campaign. Learn more about setting journey attributes. For example, if you wanted to send a person information about the weather in their area, you could use a webhook to fetch the forecast and store this on a journey attribute. Reference journey attributes in conditions After you set a journey attribute, you can reference it in any condition in your workflow. For instance, in this branch, people go down different paths based on the journey attribute weather: Reference journey attributes in messages After setting journey attributes, you can add them to messages or other webhooks using the liquid object journey. Following the example above, the syntax would be {{journey.weather}}, where weather is the journey attribute name you set in the webhook. To preview journey attributes in a message, you have to provide your own data. Go to the Sample data panel, and select the Journey tab. Input any useful value under your attribute name.  If the value of your journey attribute contains liquid syntax, make sure you use the render_liquid tag Learn more in How to reference attributes with nested liquid. How to reference journey attributes with nested liquid You might store attributes containing liquid syntax, like when you store personalized email content generated from an LLM action or webhook. For example, imagine that you have a journey attribute named body with the following value: "body": "Hello {{customer.first_name}}!" If you reference this journey attribute in a message with {{journey.body}}, the body field won’t be evaluated! Instead it’s as static text! So you’ll see “Hello {{customer.first_name}}!” instead of “Hello Alex!” To render liquid syntax dynamically, you need to wrap the liquid object in {% render_liquid %}, like {% render_liquid journey.body %}. Then, when the message sends, we’ll evaluate {{customer.first_name}} and render “Hello Alex!” as you’d expect. Events that trigger workflows Use the trigger key to reference trigger data for these workflows: Transactional messages API-triggered broadcasts Webhook-triggered campaigns Object or relationship-triggered campaigns For event-triggered campaigns, you’ll reference trigger data with the event key. Event-triggered campaigns Event objects let you include data from events that trigger campaigns; you can’t use this to target any other events performed by people. You might create an event-triggered campaign around purchases. Imagine a customer bought a pair of socks, and you want to give them a discount on their next pair. To do that, you send Customer.io a purchase event with how much the socks cost. If you look at a person’s recent activity, the event might look like this: You could then pull that data into your message content. In this example, the message includes the product and a conditional that shows a different discount code depending on the amount of money they spent. Thanks for your purchase! We hope you enjoy your new {{ event.product | default: "items" }}. {% if event.price > 20 %} Save 20% on your next purchase over $20 with code SAVE20! {% else %} Save 10% on your next purchase with code FIRST10! {% endif %} Based on the recent activity above, the person would receive this message: Thanks for your purchase! We hope you enjoy your new socks. Save 20% on your next purchase over $20 with code SAVE20! You’ll always use the object event to reference event trigger data, never the actual name of the event. You can also reference event trigger meta data through these keys: event_name represents the event name, as sent into Customer.io. event_id represents the ID as logged by Customer.io; this value is always unique. event_timestamp is the event timestamp in Unix epoch format.  You can only pull in data from the event that triggered your campaign Even if you reference other events in your campaign—like in a Wait until action or in your conversion criteria, you can only use the event key to access trigger data. Webhook-triggered campaigns Unlike other types of campaigns, you can’t send messages to customers in webhook-triggered campaigns. Rather, you use these campaigns to trigger workflows that send events or update customers based on the incoming data. For example, let’s say you have a survey tool, but don’t have the resources to integrate it with Customer.io right now. But you want to reach out to customers who submit a low score. You could use a webhook-triggered campaign to transform the incoming data to trigger a message for customers in another campaign. The process would look like this: Create your webhook in your third-party tool. Trigger a campaign by this webhook. Add a Send Event action to the campaign. This transforms the incoming data into an event and saves it to a customer’s profile. Trigger a campaign based on this event. Add a message to the event-triggered campaign to follow up on their survey results. While {{trigger.<property>}} is available in webhook-triggered campaigns, you’ll only use this to reference incoming data in a Send and receive data action. This action lets you send a webhook out of Customer.io and include trigger data. Otherwise, you’ll use our condition builders to transform trigger data into events or attributes; you won’t need this liquid syntax. Follow our step-by-step guide for third-party survey tools or check out our other recipes to get started with webhook trigger data. Object and relationship-triggered campaigns Objects are non-people entities you can relate to people. For instance, a person might belong to an account object in your workspace. The connection between a person and object is called a relationship. You can trigger campaigns by objects and relationships and include the trigger data in your workflows. For example, say you want to inform people when accounts they manage upgrade their plans from premium to enterprise. You could trigger a campaign when an account’s plan name is updated and send a message informing the managers: Good news! {{trigger.account.name}} has upgraded to enterprise! To include trigger data for objects, you must include the object type name: {{trigger.<object_type>.<attribute_name>}}. In this example, “account” is the object type, which groups all accounts together. You could also include information on the recipient’s relationship to this account. You're receiving this message because you're a {{trigger.relationship.role}} on this account. To include relationship data, you don’t include the object type name, just “relationship”: {{trigger.relationship.<attribute_name>}}. The message content might look like this: Good news! Acme Inc.0 has upgraded to enterprise! You're receiving this message because you're a manager on this account. Check out Objects in liquid for more examples. Transactional messages and API-triggered broadcasts When you create a transactional message or an API-triggered broadcast, you may want to include data from the payload that triggered the message or workflow. You can reference properties from the trigger event as {{trigger.<property>}}. For example, you could send a transactional message that’s a purchase receipt with information on shipping. The trigger event might include the product name and range of time it could take to deliver the package. "message_data": { "product": "trampoline", "minShipTime": 4, "maxShipTime": 6, "unitOfTime": "weeks" } Then the message could include liquid like this: Thanks for your purchase! Your {{trigger.product}} has shipped! It will arrive in {{trigger.minShipTime}} to {{trigger.maxShipTime}} {{trigger.unitOfTime}}. Based on the trigger data, the message would send with the following info: Thanks for your purchase! Your trampoline has shipped! It will arrive in 4 to 6 weeks. Learn more about personalizing messages with trigger data for API-triggered broadcasts. Add message and workflow metadata This section talks about liquid keys that output high-level data around your messages, campaigns, and more. You can personalize messages with customer or trigger data using the keys above. When you preview a message or we generate a delivery, we produce metadata about your message, the campaign it came from, and more that you can reference with liquid. Generally, you wouldn’t want to expose this data in a message, but you might use them behind-the-scenes to construct custom unsubscribe links or send info to your email analytics tools. The meta data available changes based on the type of workflow, message, or action you’re editing. While many keys are available when you preview a message, some, like delivery_id, only populate at send time. You can find a full list of them on the liquid syntax page under Customer.io Meta Keys. Liquid fallbacks Messages will fail to send if they include data that doesn’t exist—like when a person doesn’t have an attribute or an event doesn’t have a property you reference. To prevent message failure due to liquid errors, you should add fallback logic or filters. We have two versions of liquid: “legacy” liquid and our “latest” liquid. Most of our syntax works across both liquid versions. However, there are some tags and filters that behave differently. For instance, default is available only on the latest liquid version. Learn about our liquid upgrade and how to see which version you’re on. Common conditionals like this would work across both versions, so depending on the syntax you need, it may not matter what version you’re on: Hi {% if customer.first_name %}{{ customer.first_name | capitalize }}{% else %}there{% endif %}!  Use the Add Liquid option in the drag-and-drop email editor If you’re using our drag-and-drop email editor, and you want to add liquid that involves logical or comparison operators (&, >, or <), click Add Liquid in a text block to ensure the liquid renders correctly. Fallback that covers both liquid versions Most of our syntax works across both liquid versions. However, there are some tags and filters that behave differently. If you’re on an account that has messages that use both the legacy and liquid versions, you may want to ensure that any snippets or content reused across messages render correctly for both versions. For content that renders based on liquid version, check out our article on liquid versions.. Fallback for latest liquid In addition to standard if/else conditions, you can use the default filter as a shorthand to set a fallback value. For example, if some people don’t have a plan_name, you could create a fallback that renders for them: You are currently on our {{ customer.plan_name | default:"custom" | capitalize }} plan. Without a plan name, the person will receive this message: You are currently on our Custom plan. In our rich text email editor, we also have a shortcut for you! Fallback for legacy liquid For legacy liquid, create a fallback with an if statement. You can use the following shortcut to add a legacy liquid fallback in our rich text email editor. For all other editors, here’s how you would write the liquid logic from scratch. {% if customer.plan_name != blank %} You are currently on our {{ customer.plan_name | capitalize }} plan. {% else %} Please choose a plan. {% endif %} If the person has a plan_name of Trial, then the liquid will display, You are currently on our Trial plan. If the person’s plan_name is blank, the liquid will display, Please choose a plan. Alternatively, if you don’t want to show anything for people without an attribute, exclude the else statement: {% if customer.plan_name != blank %} You are currently on our {{ customer.plan_name | capitalize }} plan. {% endif %} --- ## Liquid upgrade URL: https://docs.customer.io/journeys/liquid-upgrade/ We are upgrading how we render liquid in your messages to better support you. The version we use to render your liquid depends on when you created your account with us.  What liquid version are you on? If your Customer.io account was created on or after Nov 28, 2023, you are using the latest liquid version for all messages. If your Customer.io account was created before Nov 28, 2023, see Changing your liquid version for more info. See below for the differences between the two versions. How to check your liquid version Within any message editor, hover over the last saved date at the top to see the liquid version you’re using. If you’re using the legacy liquid version, you can upgrade to the latest version. If you upgraded a message to the latest liquid but want to return it to legacy due to syntax issues, you can also downgrade, but we recommend using a fallback if possible. Change your liquid version We’ve updated many accounts already to use the latest liquid version by default for new messages. You can check your liquid version on any message. The latest liquid version offers improved filters (e.g. default filter), faster rendering, and better tools for troubleshooting liquid syntax. See the differences between our legacy and latest liquid versions below. For accounts created on or after Nov 28, 2023, you’re already using the latest liquid version for all messages. For accounts created before Nov 28, 2023, you have the option to upgrade and downgrade: You can upgrade all new messages to use the latest liquid version by default in your workspace settings. (There is no setting for upgrading all existing messages at once.) You can downgrade if you find you’re not ready. Review the differences between versions so you can be confident you’re making the right choice. You can upgrade your liquid version on a template-by-template basis. That is, if you upgrade an email then make a copy, the copy will use the latest liquid, too. You can also downgrade the liquid version of a message, if necessary or provide a fallback. Workspace: upgrade liquid For accounts created before Nov 28, 2023, you can go to workspace settings to change what liquid version your new messages use by default. Accounts created on or after this date already use the latest liquid version for all messages, which is our long-term goal for everyone. Go to General Workspace Settings, and scroll down to What liquid version do you want to use by default for new messages? What does this mean for your messages? In your workspace, any new message moving forward (email, in-app, etc) will use our latest liquid version. You must upgrade this setting in each of your workspaces; there is no account-level setting. All new messages, including their translations, will use the latest liquid version. You must upgrade existing messages to use the latest liquid version. There is no setting for upgrading all existing messages at once. You should still preview your messages before sending to make sure your liquid renders as you’d expect. Review the differences between liquid versions to make sure you’re ready to upgrade. Keep in mind, you can also upgrade individual messages if you’re not ready to commit to all new messages. To upgrade at the workspace level, click “Upgrade liquid” then confirm your action. You cannot upgrade snippets and layouts directly; these will render based on the liquid version of the message they are used in. Workspace: downgrade liquid For accounts created before Nov 28, 2023, you can go to workspace settings to change what liquid version your new messages use by default. Go to General Workspace Settings, and scroll down to What liquid version do you want to use by default for new messages? What does this mean for your messages? In your workspace, any new message moving forward (email, in-app, etc) will use our legacy liquid version. You must downgrade this setting in each of your workspaces; there is no account-level setting. All new messages, including their translations, will use the legacy liquid version. You must downgrade existing messages to use the legacy liquid version. There is no setting for downgrading all existing messages at once. You should still preview your messages before sending to make sure your liquid renders as you’d expect.  Provide a fallback instead of downgrading We recommend you provide a fallback instead of downgrading, if possible, as we intend to move everyone over to our latest liquid verison in the long-term. Keep in mind, you can downgrade individual messages instead of all new messages, as well. To downgrade at the workspace level, click Downgrade liquid” then confirm your action. You cannot downgrade snippets and layouts directly; these will render based on the liquid version of the message they are used in. Message: upgrade liquid For accounts created before Nov 28, 2023, you can upgrade to the latest liquid version on a template-by-template basis. That is, if you upgrade an email then copy it, the copy will use the latest liquid, too. Go to a message with legacy liquid, click Actions or three horizontal dots in the top right, and see “Upgrade Liquid version…” in the dropdown. If you want to upgrade a translated message, you’ll need to upgrade each language variant in the message, not just the default.  Check out the differences between our two versions of liquid to make sure you’re ready to upgrade. To upgrade, click “Upgrade Liquid version…” then confirm your action. This will only upgrade the liquid version for the message you are on. You must go into each message to upgrade the version. You cannot upgrade snippets and layouts directly; these will render based on the liquid version of the message they are used in. Message: downgrade liquid For accounts created before Nov 28, 2023, you can also downgrade a message to the legacy liquid version. To downgrade, go to a message with the latest liquid, click Actions or three horizontal dots in the top right, and click “Switch Liquid version…” in the dropdown. If you want to downgrade a translated message, you’ll need to downgrade each language variant in the message, not just the default.  Provide a fallback instead of downgrading We recommend you provide a fallback instead of downgrading, if possible, as we intend to move everyone over to our latest liquid verison in the long-term. Click “Switch Liquid version…” then confirm your action. This will only downgrade the liquid version for the message you are on. You must go into each message to downgrade the version. Fallback for both latest and legacy liquid versions This section is useful if you want to reference liquid across messages and not worry about which liquid version you’re on. You’ll assign a variable to establish the liquid verison of the message and reference the variable to display the correct output.  You must assign the variable and reference it in the template itself or in a snippet. You cannot assign a variable in a snippet, then reference it in a template and vice versa. Assign the variable in the template or snippet followed by this conditional: {% assign liquid_version = nil | default: "latest" %}{% unless liquid_version == "latest" %}{% assign liquid_version = "legacy" %}{% endunless %} Then you can reference the variable in a conditional to display the proper content: {% if liquid_version == "latest" %}liquid that renders for latest liquid version{% else %}liquid that renders for legacy liquid version{% endif %} Below is a breakdown of what is deprecated and new across liquid versions. A fallback like the one above can help you make sure, for instance, that operators supported by the legacy liquid version render when possible while a replacement for these operators renders with the latest version: {% assign liquid_version = nil | default: "latest" %}{% unless liquid_version == "latest" %}{% assign liquid_version = "legacy" %}{% endunless %} {% if liquid_version == "latest" %}{{ product_color | default: "red" }}{% else %}{% if product_color != blank %}{{ product_color }}{% else %}red{% endif %}{% endif %} Differences in liquid versions Deprecated with the latest liquid version: timezone - We deprecated the timezone filter in the latest liquid version. Moving forward, use timezone as an argument with the date filter instead. Legacy: {{ created_at | timezone: 'America/New_York' }} Latest: {{ created_at | date: '%Y-%m-%d %H:%M:%S', 'America/New_York' }} Note: We support the Asia/Riyadh time zone in the latest version of liquid. We no longer support: Asia/Riyadh87 Asia/Riyadh88 Asia/Riyadh89 Mideast/Riyadh87 Mideast/Riyadh88 Mideast/Riyadh89 htmlencode - Use escape to escape a string moving forward. Legacy: {{ "win+help@customer.io" | htmlencode }} or {{ "win+help@customer.io" | escape }} Latest: only {{ "win+help@customer.io" | escape }} New to the latest liquid version: default - It’s finally here! Now you can set a default value when one does not exist. json_array_uniq - Now you can evaluate an array of JSON objects to output an array containing all the unique objects based only on the value of the passed-in key name. Use the filter to_json if you need to output string values. break - You can use a break tag to exit a loop. Differences between filters supported in both versions These filters exist across both versions, but behave differently in the latest liquid: timezone - You can offset the time zone in minutes, not hours. For instance, 360 would offset the time zone by -6:00 hours. currencyand rounded_currency - These filters accept both a locale and currency code. The legacy version only accepted a locale. These locales are only supported in the latest version: gsw-CH, ja-JP, kk, mg, sv-FI, sv-SE, uk, zh-Hant-MO, zh-MO. The locale zh-YUE is only supported in the legacy version. escape - Use url_encode to encode URLs instead. sort - This no longer throws an error, preventing messages from sending, when a list/array contains null values. However, when an array contains null values, it won’t reorder the array. concat - Now this accepts an object or an array. sha256 - This converts a string to the same output as hmac_sha256. times - Compare examples from the filter list to see when outputs contain decimals or not. In the latest version, if the output is a whole number, it will never contain a decimal. divided by - Compare examples from the filter list to see when outputs contain decimals or not. In the latest version, if the output is a whole number, it will never contain a decimal. modulo - The output will always be a positive number, even when there’s a negative number as input. sum - The output aggregates numbers. Every value is cast to a number. Unlike with legacy liquid, sum does not concatenate numbers, strings, or booleans. Variable fallbacks Both versions With both our liquid versions, you can compare to blank to establish a fallback to some default value. For example, the if statement below renders “Hello there!” when first_name is not stored on a person’s profile or when first_name equals null, false, or an empty string. {% if customer.first_name == blank %} Hello there! {% else %} Hello {{customer.first_name}}! {% endif %} You can also compare to nil to check if a value exists. For example, if the value below for previous_purchases is false, the value exists/is not null. Compare key-values to nil if false is a valid value. Otherwise, you should compare key-values to blank if false is not a valid value. {% if customer.previous_purchases == nil %} Make a purchase! {% else %} {% comment %}else statement renders if previous_purchases equals false{% endcomment %} If you liked your last purchase, try these other products! {% endif %} Also, consider math functions. You could use this conditional to set a fallback with either liquid version. {% if customer.order_count < 10 %} We thought you might like these items: {% else %} As a thank you, take 20% off your next order! {% endif %} The else statement would render even if the order_count did not exist for a customer (people who had not ordered yet). If that’s not what you want, you should check if the variable exists first. {% if customer.order_count %} {% if customer.order_count < 10 %} Have you checked out these items? {% else %} As a thank you, take 20% off your next order. {% endif %} {% endif %} This way, people without an order_count would not see either conditional statement. Latest only With our latest liquid, you can also use the default filter to set a fallback. {{ customer.first_name | default: 'there' }} With our latest liquid, you can check if an array has values with empty. {% if customer.purchases == empty} Ready to make a purchase? {% else %} Ready to make your next purchase? {% comment %} You could also loop through purchases here!{% endcomment %} {% endif %} --- ## Liquid syntax list URL: https://docs.customer.io/journeys/liquid-tag-list/ This page contains a list of the liquid tags, keys, and filters available to you in Customer.io. If you're new to liquid, you can learn more in our guide about [personalizing messages](/journeys/using-liquid/). --- ## Liquid recipes URL: https://docs.customer.io/journeys/liquid-recipes/ Learn how to add liquid syntax for your use case. You can find our available tags, keys, and filters in our Liquid syntax list, and you can learn more about how to use liquid in Personalize messages with liquid. Countdown to an event If your customers subscribe to an event—a webinar, they buy tickets to a movie or concert, etc—you can include a countdown to an event or specific date and time that your audience is interested in! The timer produces an animated GIF that counts down to the event. The countdown timer GIF cannot contain more than 60 frames. This limits the size of the image in your messages and ensures that your message loads properly. So, if you set the resolution for your timer to seconds, the counter will stop 60 seconds after the image loads. The countdown image reloads when a person opens the message again, so it’ll always be relevant to the user’s current time, but it cannot count down indefinitely. {% countdown point:64 font:roboto weight:light fg:000000 bg:f2f6f9 time:"2022-07-04 12:00:00 (GMT)" locale:en looping:true resolution:S frames:2 %} Countdown timers take several parameters. Your timer must include the font size, the foreground (font) color, the background color, and the time you want to count down to. Parameter required format default description point ✓ integer The font size for the timer time ✓ ISO 8601 timestamp The date and time you want to countdown to in the format YYYY-MM-DD hh:mm:ss (TZ). Remember to close the time in quotes, as the value includes a space. fg ✓ hex color The foreground (font) hexidecimal color bg ✓ hex color The background hexidecimal color font string inter, roboto The font family for your timer weight string normal The font weight, takes normal CSS font-weight values. locale language code en The language you want to display: en (English), ru (Russian), jp (Japanese), zh (Chinese), pt (Portuguese), es (Spanish) looping boolean false Determines whether the countdown timer should restart after it finishes resolution one of S, M, H, D S Determines how often the timer counts down—by the second, minute, hour, or day. frames integer 1 Number of seconds you want to show, based on the resolution, where seconds: 60, minutes: 2, hours: 1, days: 1 Math operations with attributes AttributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. are stored as strings—even if the attribute value is a number or an integer. When you want to use a number/integer attribute in a math operation, or evaluate it against another number, you need to convert the attribute to a number/integer to ensure that your liquid statements evaluate properly. You can do this using plus to add zero to the attribute. {% assign my_attribute = customer.my_attribute | plus: 0 %} {% if my_attribute > 0 %} Your attribute is greater than 0! {% else %} Oh no, your attribute is 0 or negative. {% endif %} Links You can include attributes within links as well, in order to send your users to a custom page. Example: <a href="http://www.yoursite.com/{{customer.ATTRIBUTE}}"> Displaying a timestamp as a regular date Let’s say you have a customer attribute called expiration (for a trial, maybe) that you store as a timestamp. If you wanted to display that expiration date in messages as a human-readable date, you can do so with liquid. There are a few different date filters you can use with this, but here we’re just going to show you month name, day, and year. Example If you had a customer attribute called expiration with a timestamp value of 1596153600, you can do the following: {{customer.expiration| date: "%B %-d, %Y"}} The result in your message would be July 31, 2020. The spaces, commas, etc between the date filters are included in the output. So if you want 31/07/2020 you’d do %d/%m/%Y. For the current date, you can use: {{ 'now' | date: "%B %-d, %Y" }} Displaying how many days left in a trial {% assign current_date = 'now' | date: '%s' %} {% assign future_date = customer.trial_end %} {{ future_date | minus: current_date | divided_by: 86400 }} Explanation You can get the current epoch time with 'now' | date: "%s". %s is a formatting option. The customer.trial_end date is in epoch time. Since epoch time is in seconds, we’re dividing by 86400 (number of seconds in a day) to get the number of days. Keep in mind that this is integer division so it’ll be rounded down. Compare two user or event attributes While it’s not possible to compare attributes when you create segments or when you set up an event filter for your campaign, you can use liquid code inside your campaign’s content to achieve this purpose. The following example checks for equality: {% if customer.attribute_1 == customer.attribute_2 %} Hello awesome person! {% endif %} But you can also use operators, such as >, <, and so on: {% if customer.lifetime_value > 100 %} Thanks for being a loyal customer! {% endif %} Emails: Using a customer name in the ‘To’ field This is done by using the following Liquid in your email’s ‘To’ field: {{customer.name}} <{{customer.email}}> Localizing date If you want to display the current date formatted like: 06:54 AM Février 21, 2017 you can use this code replacing the months with the proper names for your desired language: {{'now' | date: "%H:%M %p %B %d, %Y" | replace: "January","Janvier" | replace: "February","Février" | replace: "March","Mars" | replace: "April","Avril" | replace: "May","Mai" | replace: "June","Juin" | replace: "July","Juillet" | replace: "August","Aout" | replace: "September","Septembre" | replace: "October","Octobre" | replace: "November","Novembre" | replace: "December","Décembre"}} If you want to display the month in your customers’ language you can do this: {{ event.invoice_date | date: "%-d" }} {% case m %} {% when '1' %}Janvier {% when '2' %}Février {% when '3' %}Mаrs {% when '4' %}Avril {% when '5' %}Mаi {% when '6' %}Juin {% when '7' %}Juillet {% when '8' %}Août {% when '9' %}Septembre {% when '10' %}Octobre {% when '11' %}Novembre {% when '12' %}Décembre {% endcase %} {{ event.invoice_date | date: "%Y" }} Show a different message based on day of week {% assign day = 'now' | date: '%A' %} {% if day == 'Friday' %} Have a great weekend! {% else %} Cheers, {% endif %} Using data containing whitespace We recommend adding data to Customer.io without spaces. But if you do (for example, if you send us an attribute called current city), here’s how you’d refer to it: {{ customer["current city"] }} Escaping special characters in attribute names In general, you shouldn’t use periods, commas, numbers, or special characters in attribute names or event properties, because it can make it hard to find and use your attributes. But if your attribute names do include these kinds of characters, you can use brackets and quotes to reference your attribute in liquid. {{customer["attribute.name"]}} Filtering out default data or specific data {% if customer.first_name contains 'Visitor' %} {% else %}{{ customer.first_name | capitalize }} {% endif %} Advanced  Drag-and-drop editor users: If you’re writing more advanced Liquid including logical or comparison operators (&, >, or <, for example), use our Add Liquid dropdown option! Looping through attributes For this example, imagine each person has in their profile a list of friends, and you want to include this list in a message. Here’s an example of what the data you send to Customer.io might look like in JavaScript: _cio.identify({ id: "1", friends: ["Elaine", "George", "Kramer"] }); Say the user with an id of 1 is “Jerry”. When you’re composing messages for Jerry, you can display his friend list using a Liquid for loop: {% for person in customer.friends %} {{ person }}<br/> {% endfor %} And that’s it! That’ll display: Elaine George Kramer Looping through event data Let’s go back to our event data example above, where a user bought some socks. Instead of making just one purchase (socks), let’s say they bought multiple items (socks, toothpaste, and dental floss), and you want to list them all in a particular message. If you sent us data like this (this example’s in Ruby): customerio.track ( user_id, "purchase", :items => [ {:name => "socks", :price => "23.45"}, {:name => "toothpaste", :price => "3"}, {:name => "dental floss", :price => "2.97"} ] ) You can access that data to output a list by looping through it. This is how you’d do that: {% for item in event.items %} {{ item.name }} - {{ item.price }} <br/> {% endfor %} This will loop through your data and output the items that were passed in with the purchase event. Sorting looped data This utilizes our latest liquid. Check your message to see which liquid version you’re using. If you want to sort the data you’re looping through, you can use the sort filter. Here’s an example of how you might sort someone’s purchases per type. Array of purchases "purchases": [ { "title": "utensils", "type": "kitchen", "date": "today" }, { "title": "garlic press", "type": "kitchen", "date": "yesterday" }, { "title": "vacuum", "type": "household", "date": "yesterday" } ] Liquid to sort the groups and items in ascending order {% assign sorted_groups = customer.purchases | group_by: "type" | sort: 'name' %} {% for group in sorted_groups %} {{ group.name | capitalize }} {% assign sorted_items = group.items | sort: 'title' %} {% for item in sorted_items %} - {{ item.title }} {% endfor %} {% endfor %} Liquid to sort the groups and items in descending order {% assign sorted_groups = customer.purchases | group_by: "type" | sort: 'name' | reverse %} {% for group in sorted_groups %} {{ group.name | capitalize }} {% assign sorted_items = group.items | sort: 'title' | reverse %} {% for item in sorted_items %} - {{ item.title }} {% endfor %} {% endfor %} --- ## Personalize actions with JavaScript URL: https://docs.customer.io/journeys/js-in-actions/ In our *Create Event* and *Create or Update Person* actions, you can use JavaScript or Liquid to access and manipulate variables. This page shows JavaScript methods corresponding to common Liquid use cases, helping you take advantage of JavaScript in your workflow actions. How it works In our Create Event and Create or Update Person actions, you can select the JavaScript option to return a specific value from your Trigger data. JavaScript might be easier to use than Liquid when you want to modify incoming JSON. If you use the JSON editor in the JavaScript mode, you can return an object representing the data object for your event. These are properties you can reference in messages or other campaigns using 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}}.—e.g. {{event.purchase.total}}. We use the V8 JavaScript Engine, supporting EMCAScript standards to help you modify your data. However, you can’t perform network/HTTP calls.  You can’t use Liquid inside JavaScript When you use the JavaScript option, you must manipulate values with JavaScript. If you try to return a snippet value that contains Liquid, you’ll receive an error. Accessing variables As with Liquid, you access variables from your incoming data with JSON dot notation. JavaScript JavaScript return customer.first_name; Liquid Liquid {{ customer.first_name }} Capitalize the first letter of a string You might store your audience’s name as a lower case first_name and last_name strings but want to capitalize names when addressing a person. JavaScript JavaScript return `${customer.first_name[0].toUpperCase()}${customer.first_name.slice(1)}`; Liquid Liquid {{ customer.first_name | capitalize }} Split a string If you store your customer’s name as a single string called full_name (e.g. Cool Person), you might want to split string to return a person’s first or last names. The example below assumes that the full_name attribute contains a space between the first and last names. JavaScript JavaScript return customer.full_name.split(' ')[0]; Liquid Liquid {{ customer.full_name | split: " " | first }} Fallbacks and If statements In many cases, you should use an if statement, so you have a fallback value if a variable doesn’t exist. You can do this in JavaScript with an if statement. It’s very similar to what you can do in Liquid. For simple or inline cases, you can use the ?? operator. JavaScript JavaScript if (customer.plan_name != null) { return `You are currently on our ${customer.plan_name} plan.`; } else { return `Please choose a plan.`; } JavaScript with ?? JavaScript with ?? return customer.plan_name ?? 'free_trial'; Liquid Liquid {% if customer.plan_name != blank %} You are currently on our {{ customer.plan_name }} plan. {% else %} Welcome to your free trial! {% endif %} Compare attributes You can compare attributes in an if statement, creating conditions that determine the value(s) you set. JavaScript JavaScript if (customer.attribute_1 === customer.attribute_2) { return 'Hello awesome person!'; } Liquid Liquid {% if customer.attribute_1 == customer.attribute_2 %} Hello awesome person! {% endif %} Compare values with inequality You can check if values are less than or greater than an attribute etc. JavaScript JavaScript if (customer.lifetime_value > 100) { return 'Thanks for being a loyal customer!'; } Liquid Liquid {% if customer.lifetime_value > 100 %} Thanks for being a loyal customer! {% endif %} Convert dates You can convert dates using toLocaleString. Add an object of options after the locale string to re-format your date. We typically store date-time values as Unix timestamps in seconds; when you timestamps in JavaScript, you’ll convert them to milliseconds with * 1000. JavaScript JavaScript return new Date(customer.created_at * 1000).toLocaleDateString("en-US"); // if `created_at` is 1646936855, this returns "3/10/2022" JavaScript with options JavaScript with options return new Date(customer.created_at * 1000).toLocaleDateString("en-US", { year: 'numeric', month: 'long', day: 'numeric' }); // if `created_at` is 1646936855, this returns "March 10, 2022" Liquid Liquid {{ created_at | 'date: %B %d, %Y'}} // if `created_at` is 1646936855, this returns "March 10, 2022" Convert values to a percentage Math operations are fairly simple in JavaScript. In the example below, we’re generating a percentage by dividing two values and multiplying the result by 100. JavaScript JavaScript return (Math.floor((customer.purchase_total_to_date / customer.next_rewards_tier) * 100)) Liquid Liquid {{ assign percent = customer.purchase_total_to_date | divided_by: customer_next_rewards_tier }} {{ percent | times: 100 }} Loops You can loop through values in an array. However, the way you format your JavaScript depends on what you want to do with the resulting data. For example, imagine an attribute called friends, containing an array of strings. You could list the names like this: JavaScript JavaScript return customer.friends.join('<br/>'); Liquid Liquid {% for person in customer.friends %} {{ person }}<br/> {% endfor %} But, if you wanted to act on an array of objects, like showing the name and price of each item in an array of items, you could do something like this: JavaScript JavaScript return event.items.map((item) => { return `${item.name}: ${item.price} <br/>`; }); Liquid Liquid {% for item in event.items %} {{ item.name }}: {{ item.price }} <br/> {% endfor %} --- ## Reusing content with snippets URL: https://docs.customer.io/journeys/snippets/ *Snippets* are common chunks of content that you can share across your emails, text messages, and more. Creating Snippets You can create snippets through our API or in the Customer.io UI. To simplify things, let’s create your first snippet in the UI. Under Create content, click More > Snippets. Click Create New Snippet. Enter a Name and Value for the snippet. The liquid tag you’ll use to reference the snippet automatically updates as you enter name and value.  Avoid spaces in snippet names While snippet names can contain spaces, we recommend using underscores or hyphens to separate words, so they’re easier to use in messages and less prone to errors. Click Save. Using Snippets You can use snippets anywhere that you can use liquid, including layouts, email bodies, subjects, slack messages, and more. Reference a snippet using {{snippets.<name_of_your_snippet>}}. If your Snippet name contains spaces, you can use bracket notation instead: {{snippets.address}} {{snippets["main address"]}}  Did you just update a snippet? If you just updated a snippet used in your messages, make sure you wait a couple of minutes before activating your workflow to ensure all updates appear in your delivered messages. Advanced Example You can store JSON data in a snippet, and use liquid to iterate over the JSON. For example, here’s a snippet {{snippets.random_greetings}} containing an array of objects, each object representing a random greeting: [ { "version": "0", "greeting":"How is your day going?" }, { "version": "1", "greeting":"Hope your week is going well." }, { "version": "2", "greeting":"Hey there!" }, { "version": "3", "greeting":"Hope your day is going great so far." } ] You can use JSON dot notation to access variables in your snippet. You can also display a random greeting from the snippet in your message using the following liquid code: {% capture randomize%}{% random 3 %}{% endcapture %} {% for g in snippets.random_greetings%} {% if g.version == randomize %} {{g.greeting}} {% endif %} {% endfor %} Frequently asked questions Can I use Liquid within a Snippet? Yes, liquid within a Snippet will render in your message normally. For example, adding {{customer.id}} to your Snippet content will show a person’s id attribute wherever you use your snippet. You can even store liquid in JSON objects and strings, helping you store variable information. (You cannot store liquid in a JSON array in a snippet. Liquid inside a JSON array will simply render as text when you use your snippet.)  Snippets can’t reference variables from a message body While snippets can contain liquid, you cannot reference variables that you set in the message body. Message body variables are out of the snippet scope and won’t code be parsed. Can I change the name of a Snippet? No, you cannot change the name of a snippet. To use a different name, copy the snippet body and create a new snippet. Can I use HTML in a Snippet? Yes! HTML in a snippet renders normally in anywhere where HTML is allowed. Just make sure that you intend to use a snippet in a place that renders HTML first. For example, HTML in a subject line will not render. If you use a snippet including HTML in your subject line, your audience will see raw code. Is there a size limit for a snippet? Yes, the default max size for an individual snippet is 16 KB, which can be increased on a case-by-case basis. The sum of all snippets in a workspace must be less than 5 MB. Contact win@customer.io to request an increase. --- ## Composer errors URL: https://docs.customer.io/journeys/composer-errors/ When you compose messages and use Liquid, you may sometimes see the little **Review Errors** button turn red and animate. If you click it, the Review Errors modal will let you know what’s wrong. Inside this modal, there can be a variety of outputs. This is a run-down of the most common ones, when you see them, and what you can do to fix them! Variable is missing!  Error found in From Variable ‘customer.eemail’ is missing And it happens when the variable you’re trying to use in your email doesn’t exist for the customer whose sample data you’re using. This might be due to a simple misspelling (you’re trying to use {{ customer.eemail }} instead of {{ customer.email }}). It could also be something a little more complicated. For example, you might see this error when only some People in your workspace have the attribute you’re trying to use and other People do not. In that case, using a fallback is a good solution. can’t be blank  Error found in Subject subject can not be blank This one’s pretty self-explanatory. Certain fields — like your email subject, or a custom header’s name — can’t be left blank when you’re composing your message. Fill them in, and this one will go away. cannot be set to a custom value This might look like:  Error found in Headers header ‘Content-Type’ cannot be set to a custom value This means that you’ve tried to add a custom mail header that we have denylisted, and youll have to remove it. can’t contain whitespace  Error found in Headers Header name can’t contain whitespace This means that you’ve tried to add a header with a name that has a space in it. Remove the space and the error will disappear! tag was never closed  Error found in Body if tag was never closed This happens when you open a liquid tag (something like if, unless, or case), but forget to tell the composer where it ends. So, this liquid would result in a ’never closed’ error: {% if customer.trial_expires != blank %} Your trial ends on {{ customer.trial_expires | capitalize }} To fix it, close your tag! {% if customer.trial_expires != blank %} Your trial ends on {{ customer.trial_expires | capitalize }} {% endif %} does not expect ’else’ tag Check out this liquid code: {% capture about_me %} I am 28 years old and my favourite drink is coffee! {% else %} I am 28 years old and my favourite drink is tea! {% endcapture %} That’ll give you this error:  Error found in Body capture tag does not expect else tag This just means that the {% else %} tag inside the {% capture %} tag I’m trying to use doesn’t belong there! To fix it, I need to either get rid of the {% else %}, or change the tag it’s in! Unidentified method This error typically means that the liquid tag(s) you’re using don’t work with the specified variable. This often happens when you try to perform a math operation with an attribute or an assigned variable, because liquid treats attributes and assigned variables as strings! You probably need to convert your variable to an integer/number to use the specific liquid tag using plus 0, like the purchased_items variable in the example below. {% assign purchased_items = 0 | plus: 0 %} {% for purchases in event.purchases %} {% assign purchased_items = purchased_items | plus: 1 %} {% endfor %} Syntax Error in… This is a general error letting you know that the way you’ve written or formatted a particular tag or variable isn’t quite right. You have to do a bit of diagnosing here, because the error is quite generic; it just informs you that your syntax isn’t correct. Here’s the specific cases, with some examples: ‘if’ This one looks something like:  Error found in Body Syntax Error in tag ‘if’ - Valid syntax: if [expression] You’ll see it if you try to use Liquid like this, for example: {% if %} Your trial ends on {{ customer.trial_expires | capitalize }} {% endif %} In the above case, the condition is missing! “If,” what? To correct it, add your condition. In this case, it’s customer.trial_expires != blank: {% if customer.trial_expires != blank %} Your trial ends on {{ customer.trial_expires | capitalize }} {% endif %} ‘assign’ This syntax error means that you’ve done something wrong when trying to assign a variable. It looks like:  Error found in Body Syntax Error in tag ‘assign’ - Valid syntax: assign [var] = [source] {% assign = "apple" %} The above creates a syntax error because I’m not specifying what “apple” should be assigned to. To fix it: {% assign customer.favorite_food = "apple" %} ‘capture’  Error found in Body Syntax Error in tag ‘capture’ - Valid syntax: capture [var] You haven’t formatted your {% capture %} correctly. Remember that it’s meant to grab a string and assign it to a variable. For example, this would create a syntax error: {% capture %} I am {{customer.age}} and my favourite drink is {{customer.favourite_drink}}! {% endcapture %} I’m not telling the code what variable to assign the string to. In this case, I want the string to go into a variable called about_me. Let’s fix it: {% capture about_me %} I am {{customer.age}} and my favourite drink is {{customer.favourite_drink}}! {% endcapture %} ‘case’ ‘case’ creates specific outputs based on specified variable values. A syntax error looks like:  Error found in Body Syntax Error in ‘case’ - Valid syntax: case [condition] So this code is wrong (note the {% case %} without a condition): {% case %} {% when 'USA' %} Your order should be with you soon! {% when 'Canada' %} Your order should arrive in 3–4 days, eh? {% else %} Thank you for your order! {% endcase %} To get rid of the syntax error, I need to tell the liquid which customer attribute to look at for each ‘when’. So if I want it to look at the customer’s country… {% case customer.country %} {% when 'USA' %} Your order should be with you soon! {% when 'Canada' %} Your order should arrive in 3–4 days, eh? {% else %} Thank you for your order! {% endcase %} Fixed! Within case, remember that you can also have errors with the when or else condition. They’ll look like this, and that just means you have to fix your when or else syntax specifically:  Error found in Body Syntax Error in tag ‘case’ - Valid else condition: {% else %} (no parameters) ‘cycle’ Cycle loops through and outputs strings in the order they were passed, and it has to be used in a for loop. It’s especially cool for HTML and CSS. The syntax error looks like this:  Error found in Body Syntax Error in ‘cycle’ - Valid syntax: cycle [name :] var [, var2, var3 …] Below is an example of correct syntax. Note that cycle is in a for loop, and that odd and even will alternate for each product in the array. {% for item in customer.products %} <div class="product-{% cycle 'odd', 'even' %}"> {{ item }} </div> {% endfor %} This will output the following HTML (depending on how many products there are): <div class="product-odd">Product 1</div> <div class="product-even">Product 2</div> <div class="product-odd">Product 3</div> If I tried to do this, though: {% for item in customer.products %} <div class="product-{% cycle | three %}"> {{ item }} </div> {% endfor %} It creates a syntax error because I haven’t told cycle what to do each time it moves through the array. With this tag, you’re more likely to get strange outputs than syntax errors. ‘for’ loops These have a couple of errors associated with them: Syntax error Say I’ve got some products stored in a {{ customer.products }} variable: ["anvil", "rollerskates", "birdseed"] and I want to loop through them in my message. For loops are a way to do that, but the following code throws a syntax error: {% for all {{customer.products}} %} <div class="product-{% cycle 'odd', 'even' %}"> {{ product }} </div> {% endfor %}  Error found in Body Syntax Error in ‘for loop’ - Valid syntax: for [item] in [collection] I need to be a bit more specific, telling liquid what to loop through: {% for product in {{customer.products}} %} <div class="product-{% cycle 'odd', 'even' %}"> {{ product }} </div> {% endfor %} Unknown tag Tags are specific, as you can probably tell from this doc! This error means that you’ve tried to use a tag that Customer.io doesn’t recognise. Trying to use {% coyotes %}in your email would result in: Unknown tag ‘coyotes’ ’end’ is not a valid delimiter This error usually comes with a hint. For example:  Error found in Body ’end’ is not a valid delimiter for if tags. use endif The above tells me that I haven’t closed my {% if %} properly. {% if customer.trial_expires != blank %} Your trial ends on {{ customer.trial_expires }} {% end %} I’ve used {% end %} instead of {% endif %} was not properly terminated Check out this liquid code: {% if customer.trial_expires != blank %} Your trial ends on {{ customer.trial_expires } {% endif %} The reason that would result in this error:  Error found in Body Variable {{ customer.trial_expires } was not properly terminated with regexp: /}}/ is that {{ customer.trial_expires } is missing its second curly bracket! Add it, the variable will be correctly closed, and the error will go away. The same goes for tags; if you haven’t closed a tag properly, you get a version of the same error:  Error found in Body Tag ‘{%’ was not properly terminated with regexp: /\%}/ Questions? This isn’t exhaustive, of course, but hopefully it’s a good run-down of the errors you might expect to see in the composer. If you have any questions, drop us an email! --- ## Metrics Overview URL: https://docs.customer.io/journeys/analytics/ Metrics throughout Customer.io show you how well your messaging strategies are working or if there's anything unexpected happening. We provide a variety of views so you can learn about everything from workspace-wide performance to Campaign journeys. What does Customer.io measure? We primarily focus on two kinds of metrics. You may find a combination of both of these things on the Home dashboard, Analysis, and Campaign Overview pages. While they often correspond, it’s important to note that we measure two different things. Campaign journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. metrics, which help you learn about how people progress through campaigns. Message/deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. metrics, which help you determine how well your messages perform. On your Home dashboard, we also calculate: Segment membership metrics, which show how membership in segments changes over time. Test metrics, showing how your A/B tests have performed and which entry in the test won. Link metrics, which help you understand which of your links your audience clicks most often. How we calculate campaign journey and message metrics A journey is the instance of a campaign that a person experiences. A message is an individual message in the campaign. So, a person who receives a message might be in progress in their campaign journey, but have opened or clicked a message in that campaign. Their interaction with the message is done, but they’re still working their way through the journey. Metrics do not report actions on the date they occur. Instead, metrics are reported against the date a journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. or a messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. is created. It may be helpful to think of metrics as an answer to the question: of messages and/or journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. created on a particular date, how many achieved a particular status?. This also means that corresponding metrics for journeys and messages may report on different dates because journeys can take a while and messages are more immediate. For example, imagine that you have a campaign that sends messages over a five day period. A person enters the campaign on January 1st; they open and convert based on the last message—sent and received on January 6th. The converted metric for this journey is reported on the 1st—because the journey, created on the 1st, has converted. The converted metric for the message is reported on January 6th, because the message was created on the 6th.  Dashboard metrics are measured in Eastern time (Etc/GMT+4). This means metrics aggregate based on EST, not your system’s time zone. For example, say a message was sent at 03:59:59 UTC. That message metric would be tracked against the previous day’s metrics totals, since it’s before 00:00:00 Etc/GMT+4 (04:00:00 UTC). Learn more about dates and time in Customer.io. Message Performance and Delivery Metrics Customer.io generates the following metrics to determine the performance of your messages. By default, we calculate metrics based on the number of deliveriesThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. that successfully make it to recipients, though you can change this setting if you don’t send messages using our default sending domain and you don’t send delivery metrics to Customer.io from your delivery provider. Sent Your message was successfully sent from Customer.io to the delivery provider. It is their responsibility to ensure the message is properly delivered, and we’re waiting for further information to determine if the user interacted with it. Delivered Delivered means that we’ve received confirmation from the delivery provider that a message was delivered to the recipient’s email service provider (ESP). Opened For email, we count an open when the recipient’s email client loads an invisible image (tracking pixel) or when the recipient clicks one of the links in the email. If a customer has images disabled in their email client and an open is tracked based on a click event, the “Opened Email” and the “Clicked Email” events will show as having occurred at the same time. Email open metrics can be misleadingly high or low due to email client behavior. Use email opened metrics to track trends over time and investigate potential inbox placement issues. For in-app-messages, the opened metric means the message has been delivered and displayed to a person. For push notifications, our SDKs automatically track when they’re opened. If you don’t use our SDKs, you need to send us events to track push notification opens. Human opened This metric is available for emails opened since March 20, 2025. Compared to the opened metric, the human opened metric excludes emails opened by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, user agents identified as bots, and known scanners—based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. Machine opened This metric is available for emails opened since March 20, 2025. This metric includes opens by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, and user agents identified as bots. It also captures opens identified as known scanners, based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. In the UI, this metric is calculated as the difference between Opened and Human Opened to ensure Opened reflects the total. Clicked The user clicked on any link in a message where link tracking was enabled. We also track clicks on individual links. Human clicked This metric is available for tracked links in emails since April 20, 2025. Compared to the clicked metric, the human clicked metric excludes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. Machine clicked This metric is available for tracked links in emails since April 20, 2025. This metric includes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. In the UI, this metric is calculated as the difference between Clicked and Human Clicked to ensure Clicked reflects the total. Click-to-open This measures the rate at which people click tracked links in messages that they open, and is calculated by dividing Clicked metrics by Opened metrics. Converted A delivered message is marked as converted when a person achieves your campaign’s goal within the conversion criteria window. The message must have been sent, opened, or had a tracked link clicked within a specified time period. Learn more about goals and conversion timing. Unsubscribed A message is counted as unsubscribed when a user unsubscribes from all messages. You can read more about how global unsubscribes work in Customer.io. Unsubscribed from topics Unsubscribed from topics counts each subscription topic that a user unsubscribed from through a delivered message. As such, you may see a percentage above 100; say you have 4 subscription topics and 5 messages are delivered to five users. Two users unsubscribe from 3 topics each (6 total). We would divide this by the number of delivered messages (5) to get 1.2 or 120% unsubscribed from topics. You can read more about how our subscription center works in Customer.io. Marked-as-spam For emails, this means that the end-user marked the email as ‘Spam’ via their email service provider. All subsequent emails are then marked “suppressed.” Failed A failed message never made it to the delivery provider. We have a separate article to help you diagnose why your message never left Customer.io. Bounced For email, this means the user’s email address wasn’t valid. This could be a hard bounce (usually a permanent issue like a non-existent email address) or a soft bounce (a temporary issue like a full mail box). In the case of hard bounces for emails, subsequent messages to those addresses will be given the “suppressed” status. Suppressed A message is marked as suppressed if: the email address we tried to send it to was previously a hard bounce (a permanent failure such as an invalid address), or the end-user marked any previous email message as spam. In this case, all subsequent email messages after the “spam” mark will be marked as “suppressed”.  We don’t record metrics for messages that are more than 6 months old. If someone opens your message more than 6 months after you sent it, we won’t attribute an open to your message. We ignore metrics for older messages to keep your workspace running quickly. Set the denominator for message metrics By default, we calculate message metrics based on the number of messages delivered. This helps you gauge message performance based on the number of messages that actually reach your audience’s inboxes. However, you should change your metrics to calculate based on the number of messages sent if: You don’t use our default sending domain You don’t send Delivery metrics to Customer.io from your delivery provider. To change the denominator (Delivered or Sent) for your metrics: Go to Settings > Workspace Settings > Email and click Settings. Go to the Custom SMTP tab and click Delivered or Sent to determine how your metrics are calculated. What we track for each message type Different types of messages produce different metrics. If we can’t track a metric for a channel, you’ll either see a ‘-’ or 0.0% in its place, depending on the location in Customer.io.   Delivered Opened Clicked Converted Bounced Suppressed Spammed Unsubscribed Unsubscribed from topics Email check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle check_circle SMS check_circle highlight_off check_circle check_circle check_circle check_circle highlight_off highlight_off highlight_off Customer.io Push check_circle1 check_circle1, 2 highlight_off check_circle check_circle highlight_off3 highlight_off highlight_off highlight_off In-App highlight_off check_circle check_circle check_circle highlight_off highlight_off highlight_off highlight_off highlight_off Slack Message highlight_off highlight_off check_circle highlight_off highlight_off highlight_off highlight_off highlight_off highlight_off Webhook highlight_off highlight_off check_circle check_circle4 highlight_off highlight_off highlight_off highlight_off highlight_off 1If you don’t use our SDKs, you need to send us events to track push notification opens. 2“Opened” is the major way that people engage with notifications. You might consider “opened” similar to a clicks for other channels. 3While we don’t track suppressions for device tokens, we do automatically suppress tokens that experience bounces from the delivery provider. 4You must enable the Allow conversion from this webhook setting to track conversions from a webhook. How we track human opens and clicks In your workspace, you’ll see Human and Machine metrics for Opens and Clicks. This only applies for emails. While Opened and Clicked metrics aggregate human and machine metrics, Human Opened and Human Clicked metrics only track interactions attributed to people. And Machine Opened and Machine Clicked only track interactions with bots and automated systems. Human opened: This metric is available for emails opened since March 20, 2025. Compared to the opened metric, the human opened metric excludes emails opened by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, user agents identified as bots, and known scanners—based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. Machine opened: This metric is available for emails opened since March 20, 2025. This metric includes opens by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, and user agents identified as bots. It also captures opens identified as known scanners, based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. In the UI, this metric is calculated as the difference between Opened and Human Opened to ensure Opened reflects the total. Human clicked: This metric is available for tracked links in emails since April 20, 2025. Compared to the clicked metric, the human clicked metric excludes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. Machine clicked: This metric is available for tracked links in emails since April 20, 2025. This metric includes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. In the UI, this metric is calculated as the difference between Clicked and Human Clicked to ensure Clicked reflects the total. Human metrics are designed to help you understand how your audience is really engaging with your messages. As email clients, privacy tools, and security systems increasingly auto-open messages or register clicks, traditional open and click rates have become less reliable. Human metrics give you a clearer view of interactions that are most likely to reflect real people, not automated systems. Check out our blog to learn how to incorporate human metrics into your email marketing strategy. Consider the following when interpreting these metrics: Apple’s Mail Privacy Protection hides IP addresses and preloads images to obfuscate open tracking, so all opens from Apple Mail are counted as non-human. However, we can identify human opens if someone clicks a link in the email. This makes the Human opened metric more accurate, but it will still be undercounted since we can’t detect human opens without a click. We calculate machine metrics in the UI differently from machine metrics in data-out integrations. Opened and Clicked metrics include both human and machine activity, but we don’t double count. That is, if a single email was opened by both a machine and a human, that counts as one in the aggregate metric. See examples below. Viewing metrics in the UI When you hover over graphs related to email, you’ll see all three of our Opened and Clicked metrics-the aggregate metric, human-specific metric, and machine-specific metric. Machine Opened is the difference between Opened and Human Opened metrics. Similarly, the Machine Clicked metric is the difference between Clicked and Human Clicked. This ensures aggregate metrics always add up to 100% of human and machine metrics in the UI. Retrieving metrics from data-out integrations You can also retrieve human and machine metrics from our APIs, reporting webhooks, and data-out integrations. The sum of machine and human metrics may not equal the sum of aggregate metrics because of how we log this data. For instance, if you were to retrieve an email’s metrics from our App API, human_opened plus prefetch_opened may not equal opened. The following examples help explain how we count opens and clicks across machines and humans. Scenarios 1 and 2 demonstrate how we track opens when both a bot and human opened an email. Scenario 3 demonstrates that if a machine opens an email then a human clicks on it, we update the human opened and human clicked metrics and no longer attribute an open to a machine. Scenario 1: Machine opens first, human opens second A machine first opens Email A: prefetch_opened = 1 human_opened = 0 opened = 1 A human subsequently opens Email A: prefetch_opened = 1 human_opened = 1 opened = 1 Scenario 2: Human opens first, machine opens second A human first opens Email B: prefetch_opened = 0 human_opened = 1 opened = 1 A machine subsequently opens Email B: prefetch_opened = 1 human_opened = 1 opened = 1 Scenario 3: Machine open, human click A machine first opens Email C: prefetch_opened = 1 human_opened = 0 opened = 1 A human subsequently clicks Email C: prefetch_opened = 0 human_opened = 1 opened = 1 Journey Metrics A journey is the instance of a campaign workflow that an individual person experiences. Journey metrics are calculated based on the total number of people who triggered your campaign within the selected time frame. However, a campaign trigger doesn’t necessarily represent someone who experienced your campaign. People can trigger a campaign, but be filtered out before they start the campaign (Started) or receive a message (Messaged). Triggered People who matched the campaign trigger. People who trigger your campaign can be filtered out before they start it, representing a discrepancy between this metric and Started. You may want to compare this metric to Started to determine if the scope of your campaign is too wide or your filter is too narrow. You can only see this metric when you use the raw Count. In the Journey Metrics export, this metric is started. Started People who matched the campaign trigger, filter(s) and completed a non-delay action. Because you can potentially filter out people after they meet your campaign trigger, the Started metric effectively represents the total number of people eligible to experience your campaign. In the Journey Metrics export, this metric is activated. Messaged People who experienced a message action in your campaign—an email, SMS, push notification, webhook, or Slack message. This includes drafted messages. In Progress People who have Triggered your workflow, and are waiting in a delay in your workflow, but have not yet exited, converted, or been filtered out. Completed People who completed your campaign workflow by making their way to an exit action. Filtered Out People who matched your campaign trigger, but did not experience a journey/workflow. These are people who exited the campaign before experiencing a Started or Messaged state due to your campaign filters or other criteria during a delay action at the start of your campaign. In the Journey Metrics export, this metric is never_activated. Exited Early People who exited a campaign before completing an exit action. This may happen if a person stops matching the trigger and filter conditions while in a delay, their profile is deleted, or you manually remove them from a campaign. Converted People who matched the conversion criteria for the campaign. People are “converted” when they enter or exit a designated segment in response to, or within a set time of, a delivery. If your campaign does not have conversion criteria, this metric is empty. Links, Segments, and Tests Your workspace Home dashboard also shows information about your segments, links, and tests. Links The Links card shows the total link clicks for every link in the workspace. This allows you to view click metrics for links over any time range, and across any channel. Segments The Segments card allows you to see how membership counts for specific Segments have changed over time. You can choose any Segment you’d like from the drop down and your selection will be saved the next time you view the page. Pin the two Segments you care most about and keep an eye on them every time you login. Tests The Tests card shows all running tests in the workspace at the current time. This helps highlight tests that have been running longer than necessary, or that might be ready for completing based on their stats. Dive directly into the details of the test by clicking it in the list. Newsletter Metrics After sending a newsletter, view its metrics in the Overview tab. While campaigns and API-triggered broadcasts report journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. metrics, newsletters are single-send broadcasts and only produce message delivery metrics. Click Analyze to open the agent and ask questions about the metrics. For instance, you could ask “What’s the click-to-open rate for this newsletter?” or “Of my product update newsletters, which had the lowest deliverability this month?” --- ## Goals URL: https://docs.customer.io/journeys/goals/ Goals are measurable business outcomes that help you strategize around your messaging and data. Unlike campaign-specific conversion goals, *Goals* give you a single view of performance *across* your campaigns and broadcasts. How it works From the Goals page, you can define a business outcome—like starting a paid plan or completing onboarding—and attribute campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. and broadcastsA message sent to a group of people at the same time. Unlike campaigns, where individuals can enter campaigns and receive messages on their own time, you’ll trigger a broadcast for everybody meeting your criteria at once. When you send a broadcast, you can personalize broadcasted messages for individual members of your audience; everybody doesn’t have to get exactly the same message. to achieving the goal. Instead of measuring success campaign-by-campaign, Goals give you a single view of what’s driving an outcome across your messaging. Goals have a few key components: A person achieves a goal. The Total metric represents unique people—those who’ve achieved your goal at least once. We don’t aggregate every time a person achieves your goal. Goal achievement is attributed to a message sent from a source—a campaign or broadcast related to your business outcome. If a goal is achieved separate from your sources, it’s considered unattributed. Source attribution is tied to the last message a person received from one of your sources. You can tie goal achievement to revenue impact by assigning a monetary value alongside your goal criteria. We track goal achievement across message channels so you can see exactly what content is powering your outcomes. Goals are unique to each workspace. To track the same goal across multiple workspaces, you must create a goal in each one. Goals vs campaign conversion criteria Historically, workspaces have come with workflow-specific goals, which let you track conversions within a campaign, newsletter, etc, but not across them. Goals enable you to define an outcome for your business and look across your marketing communications to see your progress. Use cases Goals work well for tracking outcomes across your messaging: Trial to paid conversion: define a plan_activated event as your goal and connect your onboarding campaigns to see which messages drive upgrades. Activation and onboarding: track when new users complete key setup steps and attribute it to your welcome series. Reactivation: measure whether win-back campaigns bring dormant users back to your product. Expansion and upsell: track plan upgrades or add-on purchases driven by in-app messages or email campaigns. Set up a goal To get started, go to Goals. Click Create Goal. Add the Criteria for meeting your goal. You can set conditions for events, attributes, segments, or message data. Note, you can’t define goal criteria by custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., but you could create a segment that references the object conditions you want to evaluate. (Optional) Set the monetary Impact value. Each goal supports a single currency. There’s no automatic multi-currency conversion—if you track revenue in multiple currencies, create separate goals for each. If people achieve the goal when they perform an event, you can choose to set the impact based on an event attribute or a static value. For other goal criteria, you can only set a static value. Click Save. At first, you’ll see a revenue report. To refine how your goal is tracked, go to Attribution. Under Attribution, click Add attribution source to choose which campaigns and broadcasts count towards your goal. Learn more about sources. Click Edit settings to configure your attribution settings. Learn more about attribution. Review the message metrics associated with goal achievement. Modify as you see fit. By default, attribution is set to Sent and Delivered metrics across message channels. Review the time window for attribution. The window can be up to 90 days. Add a Name and Description so you know what you’re tracking. Consider adding Tags to help you organize related goals and workflows. By default, you’ll see the last 30 days of attributed data after you initially set up your goal, but you can modify this by clicking the calendar dates.  You can’t track goals for data processed before November 2025 You can filter for the last year of data; however, Goals only track data since November 2025. Add a source & edit attribution settings You can add campaigns and broadcasts (newsletters only) as Sources to any goal. We’ll attribute goal achievement to the last message from a source that meets your attribution settings, like an email opened within the last 30 days. You can’t track goals against transactional messages, anonymous in-app messages, or API-triggered broadcasts. Under Attribution, click Add source to get started. Filter for the campaigns and broadcasts you want to track. Results are paginated so you may need to click or to find what you’re looking for. You can filter by Status (whether the workflow is running). You can also filter broadcasts by Channel. You might include draft campaigns if you’re preparing a goal ahead of a launch. You won’t see attributed data until you activate your campaigns or send your broadcasts. Save the sources. Click Edit settings to configure the message metrics and time window for attribution. Review the Attribution criteria—the message metrics that determine attribution, and change them as you see fit. For instance, maybe you want to attribute achieving a goal to an email that’s clicked, not delivered. You can learn more about message metrics below: Email Push notifications In-app messages SMS messages WhatsApp messages LINE messages Review the Window—the timeframe the attribution must take place. By default, this is 7 days. It can be between 0 and 90 days. Save your settings. Review your updated sources and attribution data (if your sources have sent messages). At the top, you’ll see a breakdown of attribution by source. At the bottom, you can learn more about unattributed data: people who achieved the goal outside of your sources. Attributed vs unattributed goal achievements Goals use last-touch attribution—when someone achieves a goal, the system looks back and credits the last message from one of the goal’s sources that meets your attribution criteria. If the same person achieves the goal multiple times without receiving a newer qualifying message, each achievement credits the same source. Every achievement counts as a separate impact, and each one attributes to the last source message the person received. You may also see data for unattributed achievements, which reflect people who met your goal criteria independently of your goal’s sources. These could be goals that were organically achieved or driven by workflows you didn’t add as sources to the goal. For example, let’s say your goal tracks people who convert from a trial to a paid plan. You add your onboarding campaigns as sources. They include email and in-app messages. When you view attribution settings, you leave in-app messages as “Sent”, but decide to only attribute achieving a goal to opened, not simply delivered, emails. You increase the time window to 30 days given the length of onboarding to make sure you capture all goal achievements. Let’s look closer at the change in attribution setting. If a person achieves the goal and their most recent email from a source campaign was only delivered, not opened, that email is not attributable. Your workspace finds the most recent message (across all channels and sources) that meets the attribution criteria. If that qualifying message occurred within the 30-day window, the goal achievement is attributed to it. If no qualifying message exists within the window, it’s unattributed. Track your goal performance You can track goal achievements and revenue impact over time from your goal’s page. By default, you’ll see the last 30 days of attributed data, but you can modify this by clicking the calendar dates.  You can’t track goals for data processed before November 2025 You can filter for the last year of data; however, Goals only track data since November 2025. Total vs. impact metrics When you look at your goal, you’ll see two key numbers that count different things: Total is the number of unique people who achieved the goal at least once over your selected time period. Each person counts only once, regardless of how many times they met the goal criteria. Impact is the total revenue associated with goal achievements. If you set a monetary impact value, every achievement adds to this number—even if the same person achieves the goal multiple times. For example, say your goal tracks purchases with a $50 impact value. Person A purchases once, Person B purchases twice, and Person C purchases three times. Your metrics show a Total of 3 people and an Impact of $300 (6 achievements × $50). The Total metric also shows the sum of the data reflected in the Progress chart and the goal criteria you set earlier. The Impact metric only populates if you assigned a monetary value during goal setup. Progress The Progress chart lets you track goal achievements over time and by source. If you set both goal criteria and an impact value, the Progress chart will show the total revenue impact of the goals achieved over the selected time period. Otherwise, you’ll only see the count of goals achieved over time. Total view By default, you’ll see a Count view which tracks achievement count per day. This can help you track fluctuations over periods of time, and see what days drive the most goal achievements. Switch to the Cumulative view to check the overall growth trajectory of your goal over time. This shows whether goal achievement is accelerating, decelerating, or staying steady, rather than period-by-period fluctuations. From either view, you can modify a few settings: Switch from a Bar chart to Line graph to focus in on the growth trend over time. Switch from Daily to Weekly periods depending on the level of granularity you want. By Source view By default, this view shows the total goals achieved per attribution source over the selected time period. This can help you track which sources are driving the most achievements, and which are driving the least. In the view dropdown, select whether you want unattributed sources to show in the chart. Learn more about how unattributed achievements are tracked. Switch from Daily to Weekly periods depending on the level of granularity you want. Historical data is always preserved If you stop, archive, or delete a campaign, your workspace preserves historical data. People who achieved the goal based on these campaigns will continue to count towards your historical performance. The same is true for broadcasts—you’ll see historical data for broadcasts after you delete them. On the Sources table, you’ll see the data is attributed to a deleted campaign. This data does not move to the unattributed category. If you delete people, their achievements will still count towards your goals as well. Edit a goal From a goal’s page, you can edit all aspects of a goal: Click the calendar dates to modify the time period in view. Click Edit goal to modify goal criteria and impact values. Click Edit sources to add or remove attribution sources. Click Edit settings to modify attribution criteria and time windows. Delete a goal To delete a goal, go to the Goals dashboard: Click . Select Delete goal. Remove a source from a goal To remove a source, click Edit sources, select the sources you no longer want to track, and save your changes. Results are paginated so you may need to click or to find what you’re looking for. Organize your goals You can add Tags to organize your goals, like you can segments, campaigns, and other data objects in your workspace. You might use them to flag related workstreams and easily filter for them from other dashboards. Learn more about Tags. Working with goals in external tools You can already send event data and message-level conversions to external tools via API and data-out integrations. However, you can’t export Goals-specific data: the attribution model, the number of people who achieved a goal, and impact calculations generated by Goals. --- ## Campaign and Broadcast Metrics URL: https://docs.customer.io/journeys/campaign-metrics/ When you start a campaign or trigger a broadcast, it begins generating metrics on the overview page. This page focuses on your Campaign Metrics, which provides a complete overview of the journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. and messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. metrics for a campaign, but Customer.io also presents you with metrics in a few different places: The Dashboard, which shows aggregate metrics for all of your campaigns. The Analysis page, where you can manipulate metrics and compare campaigns, newsletters, broadcasts, etc. The Campaign Workflow tab, where you can see metrics for the individual actions in your campaign. When you click a campaign, you land on the Overview page, which contains high-level metrics and configuration details that can help you understand how your campaign has performed overall. The Metrics page contains more specific information about how people progressed through your campaign, and how the individual messages in your campaign performed-were people clicking links in them, or did they convert? How we calculate metrics A journey is the instance of a campaign that a person experiences. A message is an individual message in the campaign. So, a person who receives a message might be in progress in their campaign journey, but have opened or clicked a message in that campaign. Their interaction with the message is done, but they’re still working their way through the journey. Metrics do not report actions on the date they occur. Instead, metrics are reported against the date a journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. or a messageThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery. is created. It may be helpful to think of metrics as an answer to the question: of messages and/or journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. created on a particular date, how many achieved a particular status?. This also means that corresponding metrics for journeys and messages may report on different dates because journeys can take a while and messages are more immediate. For example, imagine that you have a campaign that sends messages over a five day period. A person enters the campaign on January 1st; they open and convert based on the last message—sent and received on January 6th. The converted metric for this journey is reported on the 1st—because the journey, created on the 1st, has converted. The converted metric for the message is reported on January 6th, because the message was created on the 6th. Dates and frequency of metrics The date-picker determines the period of time over which we show charts and metrics. By default, we show metrics for the past 30 days on a daily basis, but you can change the scope of metrics to show how your campaign has performed over a longer or shorter period. If you set a long enough period, you can also set the metric frequency to weeks or months rather than days. Use the date picker to select the specific dates you want to see metrics for. Select Daily or Weekly to set the frequency for metric calculations—the X-axis in charts, the number of messages calculated in performance metrics, etc. Campaign Overview The Overview tab shows metrics about messages in your campaign, to help you understand if your messages reach their audience and if they’re having the desired affect on your audience. You can pick the channelsA messaging channel—email, SMS, etc. Each channel has its own delivery provider(s) and settings in your workspace. you want to see metrics for on this page, if you only want to see metrics for some of the messages in your campaign. The page also shows your campaign Details, to remind you how people trigger your campaign and your campaign goal. Click Edit to change any of your campaign settings. Sent, opened, clicked, and converted charts These charts reflect message metrics for the channelsA messaging channel—email, SMS, etc. Each channel has its own delivery provider(s) and settings in your workspace. you selected; active channels appear in the upper right of each chart. Outside of the Sent and Delivered metrics themselves, we try to calculate metrics based on the number of messages we can confirm were Delivered, where possible. Delivered represents messages that verifiably reached a person, where sent represents messages that left Customer.io, but we may not have information from the delivery provider to verify that the message actually reached a person. Sent reflects the total number of messages sent from your workspace in the time frame you selected. Opened shows the percentage of messages that your audience opened, out of the total number of messages delivered or sent, if you did not disable open tracking at the workspace level. We calculate opened messages based on messages delivered where possible. Hover over the chart to view Opened vs Human opened metrics. Clicked shows the percentage of messages that your audience clicked, out of the total number of messages delivered or sent. Hover over the chart to view Clicked vs Human clicked metrics. Converted reflects the percentage of messages that resulted in a conversion, if your campaign has conversion criteria. We recommend using conversions to determine the effectiveness of your campaigns: are your campaigns having their desired result? Are people making purchases, joining your service, etc based on the messages you send? Empty Charts Some of the charts in your campaign overview may be empty at times. If a chart is empty, it might mean that we don’t have data for the time period you selected, but there are also other reasons that we might not have data to populate your charts. The Opened chart might be empty if you disabled open tracking at the workspace level, and your campaign only includes emails. The Clicked chart might be empty if you didn’t include any tracked links in your messages. The Converted chart is empty if your campaign doesn’t have conversion criteria. Message Leaderboard The leaderboard shows how your messages performed in terms of sends, opens, clicks, or conversions. Click the tabs to see how your campaign’s messages perform across the different metrics. The leaderboard shows the number of channels you send messages to in your campaign. Click or to change the sort order for the leaderboard. Active Tests If your campaign included A/B tests, the Active Tests card shows the metric splits for each test-how each message in the test performed. The winner for each metric in a test is marked with a . Determining the winner in an A/B test helps you learn about the tactics your audience appreciates, so you can format your future messages to improve message performance and maintain a relationship with your audience. Metrics The Metrics tab shows more in-depth stats about your campaign. This tab can help you better understand how your campaign, and specific messages in your campaign, have performed over a specific time frame. At the top of the Metrics tab, you can select whether you want to see a metrics by Percentage or in raw Count totals. Analyze metrics with the agent In the Metrics tab, click Analyze to open the agent and ask questions about the metrics. For instance, you could ask “What’s the click-to-open rate for this broadcast?” or “Of my onboarding and welcome campaigns, which had the lowest deliverability this month?” Copy Links to Metrics Charts the Metrics tab have icons. Click this icon to copy a link to your chart, exactly as you have it configured, and share it with someone else. When you change chart settings, we add query parameters to the URL in your browser. These parameters represent your settings on the metrics page, so that you can bookmark or share your insights with other members of your organization. These links help you and your team learn how your messages perform, so you can improve communications to better support your audience. Delivery Metrics This chart shows how your messages in each channelA messaging channel—email, SMS, etc. Each channel has its own delivery provider(s) and settings in your workspace. perform, in terms of the metrics each channel generates-sent, delivered, clicked, converted, etc. If you’re on the Journey Metrics chart, click Delivery Metrics to see this chart. By default, the chart shows Aggregate metrics for all messages in the channel. Change this to Individual to compare messages in the same channel-e.g. compare emails in the campaign. When you switch to individual metrics, you can select the metric you want to compare. In the top right of the chart, you can change the channel of messages you want to see Performance and Delivery metrics for. Click any metric to turn it off or on in the chart. Sent Your message was successfully sent from Customer.io to the delivery provider. It is their responsibility to ensure the message is properly delivered, and we’re waiting for further information to determine if the user interacted with it. Delivered Delivered means that we’ve received confirmation from the delivery provider that a message was delivered to the recipient’s email service provider (ESP). Opened For email, we count an open when the recipient’s email client loads an invisible image (tracking pixel) or when the recipient clicks one of the links in the email. If a customer has images disabled in their email client and an open is tracked based on a click event, the “Opened Email” and the “Clicked Email” events will show as having occurred at the same time. Email open metrics can be misleadingly high or low due to email client behavior. Use email opened metrics to track trends over time and investigate potential inbox placement issues. For in-app-messages, the opened metric means the message has been delivered and displayed to a person. For push notifications, our SDKs automatically track when they’re opened. If you don’t use our SDKs, you need to send us events to track push notification opens. Human opened This metric is available for emails opened since March 20, 2025. Compared to the opened metric, the human opened metric excludes emails opened by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, user agents identified as bots, and known scanners—based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. Machine opened This metric is available for emails opened since March 20, 2025. This metric includes opens by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, and user agents identified as bots. It also captures opens identified as known scanners, based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. In the UI, this metric is calculated as the difference between Opened and Human Opened to ensure Opened reflects the total. Clicked The user clicked on any link in a message where link tracking was enabled. We also track clicks on individual links. Human clicked This metric is available for tracked links in emails since April 20, 2025. Compared to the clicked metric, the human clicked metric excludes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. Machine clicked This metric is available for tracked links in emails since April 20, 2025. This metric includes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. In the UI, this metric is calculated as the difference between Clicked and Human Clicked to ensure Clicked reflects the total. Click-to-open This measures the rate at which people click tracked links in messages that they open, and is calculated by dividing Clicked metrics by Opened metrics. Converted A delivered message is marked as converted when a person achieves your campaign’s goal within the conversion criteria window. The message must have been sent, opened, or had a tracked link clicked within a specified time period. Learn more about goals and conversion timing. Unsubscribed A message is counted as unsubscribed when a user unsubscribes from all messages. You can read more about how global unsubscribes work in Customer.io. Unsubscribed from topics Unsubscribed from topics counts each subscription topic that a user unsubscribed from through a delivered message. As such, you may see a percentage above 100; say you have 4 subscription topics and 5 messages are delivered to five users. Two users unsubscribe from 3 topics each (6 total). We would divide this by the number of delivered messages (5) to get 1.2 or 120% unsubscribed from topics. You can read more about how our subscription center works in Customer.io. Marked as spam For emails, this means that the end-user marked the email as ‘Spam’ via their email service provider. All subsequent emails are then marked “suppressed.” Failed A failed message never made it to the delivery provider. We have a separate article to help you diagnose why your message never left Customer.io. Bounced For email, this means the user’s email address wasn’t valid. This could be a hard bounce (usually a permanent issue like a non-existent email address) or a soft bounce (a temporary issue like a full mail box). In the case of hard bounces for emails, subsequent messages to those addresses will be given the “suppressed” status. Suppressed A message is marked as suppressed if: the email address we tried to send it to was previously a hard bounce (a permanent failure such as an invalid address), or the end-user marked any previous email message as spam. In this case, all subsequent email messages after the “spam” mark will be marked as “suppressed”. Journey Metrics A journey is the instance of a campaign workflow experienced by an individual person. The Journey Metrics chart shows how many people have experienced different aspects of your campaign within a timeframe-how many people started a journey, received a message, etc. If you’re on the Delivery Metrics chart, click Journey Metrics to see this chart. Remember that journey metrics are calculated against the day that a journey is created. The Journeys chart tells you, for all people who triggered a campaign journey on a particular day, how many are messaged, in-progress, converted, etc.? A campaign trigger doesn’t necessarily represent someone who experienced your campaign. People can trigger a campaign, but be filtered out before they start the campaign (Started) or receive a message (Messaged). Click the individual metrics to turn them on or off. The chart can show: Triggered People who matched the campaign trigger. People who trigger your campaign can be filtered out before they start it, representing a discrepancy between this metric and Started. You may want to compare this metric to Started to determine if the scope of your campaign is too wide or your filter is too narrow. You can only see this metric when you use the raw Count. In the Journey Metrics export, this metric is started. Started People who matched the campaign trigger, filter(s) and completed a non-delay action. Because you can potentially filter out people after they meet your campaign trigger, the Started metric effectively represents the total number of people eligible to experience your campaign. In the Journey Metrics export, this metric is activated. Messaged People who experienced a message action in your campaign—an email, SMS, push notification, webhook, or Slack message. This includes drafted messages. In Progress People who have Triggered your workflow, and are waiting in a delay in your workflow, but have not yet exited, converted, or been filtered out. Completed People who completed your campaign workflow by making their way to an exit action. Filtered Out People who matched your campaign trigger, but did not experience a journey/workflow. These are people who exited the campaign before experiencing a Started or Messaged state due to your campaign filters or other criteria during a delay action at the start of your campaign. In the Journey Metrics export, this metric is never_activated. Exited Early People who exited a campaign before completing an exit action. This may happen if a person stops matching the trigger and filter conditions while in a delay, their profile is deleted, or you manually remove them from a campaign. Converted People who matched the conversion criteria for the campaign. People are “converted” when they enter or exit a designated segment in response to, or within a set time of, a delivery. If your campaign does not have conversion criteria, this metric is empty.  Journey metrics are scrollable Depending on the height of your screen, you may not see all of the metrics listed above. The list scrolls, so you can find and turn each metric on or off-without taking up your whole screen. See recent journeys On any campaign, you can go to the Journeys tab to see people who have started your campaign. Click an entry in the table to see the actions that the person has completed and what’s next. This list is organized by how recently people started a campaign, and can provide you a general sense of what happens when people start your campaign. If you want to find a specific person’s journeyTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s., you can find that person on the People page and go to their Journeys tab. Charts with Dashed Lines Metrics graphs show dashed lines when you opt to view metrics on a weekly basis, but your date range includes an incomplete week. The dashed line shows that an increment on the graph doesn’t represent a full week of data. For example, if you start your date range on a Friday and end on a Thursday, all of your metrics will show solid lines because your date range does not contain an incomplete week. But, if you start your metric range on that same Friday and end on a Wednesday, your graphs will show a dotted line for the final, incomplete week of metrics. This is an example with an incomplete week of metrics. The start date is Friday, Oct. 8 and the end date is Monday, Oct. 25: Here is an example of metrics with complete weeks-it starts Friday, Oct. 8 and the end date is Thursday, Oct. 21. Links The links chart shows the total clicks for each tracked link in your campaign. This list helps you determine how well links in your campaign perform. Tracked Responses If your campaign or broadcast includes in-app messages and the Track Clicks option is enabled for actions in your message, you’ll see a Tracked Responses section in the metrics tab. This section shows the ways that people respond to your in-app messages and whether they engage with your calls to action, broken down by message and Action Name. Learn more about Tracked Responses. Message Metrics The Message Metrics table shows stats for each message in your campaign for the time frame you set at the top of the page (defaulting to 30 days). Click to select up to five metrics that you want to see in the chart, and use the Channel drop down to determine the types of messages you want to see in the chart. Click any of the metrics in the table header to sort messages by that metric in ascending or descending order. Hover over any metric to see how we calculated it. Where possible, we base metrics on the number of messages Delivered-messages that actually reached a person-because it produces the most trustworthy metrics. If we don’t have a Delivered metric, we calculate metrics based on messages Sent. You can toggle the following metrics in the chart, up to five at a time. Sent (default) Your message was successfully sent from Customer.io to the delivery provider. It is their responsibility to ensure the message is properly delivered, and we’re waiting for further information to determine if the user interacted with it. Delivered (default) Delivered means that we’ve received confirmation from the delivery provider that a message was delivered to the recipient’s email service provider (ESP). Opened (default) For email, we count an open when the recipient’s email client loads an invisible image (tracking pixel) or when the recipient clicks one of the links in the email. If a customer has images disabled in their email client and an open is tracked based on a click event, the “Opened Email” and the “Clicked Email” events will show as having occurred at the same time. Email open metrics can be misleadingly high or low due to email client behavior. Use email opened metrics to track trends over time and investigate potential inbox placement issues. For in-app-messages, the opened metric means the message has been delivered and displayed to a person. For push notifications, our SDKs automatically track when they’re opened. If you don’t use our SDKs, you need to send us events to track push notification opens. Human opened This metric is available for emails opened since March 20, 2025. Compared to the opened metric, the human opened metric excludes emails opened by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, user agents identified as bots, and known scanners—based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. Machine opened This metric is available for emails opened since March 20, 2025. This metric includes opens by Apple’s Mail Privacy Protection, Gmail’s prefetching of images, and user agents identified as bots. It also captures opens identified as known scanners, based on the MX hosts from the recipients’ email domains, within a few seconds of delivery. In the UI, this metric is calculated as the difference between Opened and Human Opened to ensure Opened reflects the total. Clicked (default) The user clicked on any link in a message where link tracking was enabled. We also track clicks on individual links. Human clicked This metric is available for tracked links in emails since April 20, 2025. Compared to the clicked metric, the human clicked metric excludes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. Machine clicked This metric is available for tracked links in emails since April 20, 2025. This metric includes clicks from automated security scanners, bot user agents, known proxy services, and repetitive click patterns identified by our proprietary algorithm as non-human interactions. In the UI, this metric is calculated as the difference between Clicked and Human Clicked to ensure Clicked reflects the total. Click-to-open This measures the rate at which people click tracked links in messages that they open, and is calculated by dividing Clicked metrics by Opened metrics. Converted A delivered message is marked as converted when a person achieves your campaign’s goal within the conversion criteria window. The message must have been sent, opened, or had a tracked link clicked within a specified time period. Learn more about goals and conversion timing. Unsubscribed (default) A message is counted as unsubscribed when a user unsubscribes from all messages. You can read more about how global unsubscribes work in Customer.io. Unsubscribed from topics Unsubscribed from topics counts each subscription topic that a user unsubscribed from through a delivered message. As such, you may see a percentage above 100; say you have 4 subscription topics and 5 messages are delivered to five users. Two users unsubscribe from 3 topics each (6 total). We would divide this by the number of delivered messages (5) to get 1.2 or 120% unsubscribed from topics. You can read more about how our subscription center works in Customer.io. Marked as spam For emails, this means that the end-user marked the email as ‘Spam’ via their email service provider. All subsequent emails are then marked “suppressed.” Failed A failed message never made it to the delivery provider. We have a separate article to help you diagnose why your message never left Customer.io. Bounced For email, this means the user’s email address wasn’t valid. This could be a hard bounce (usually a permanent issue like a non-existent email address) or a soft bounce (a temporary issue like a full mail box). In the case of hard bounces for emails, subsequent messages to those addresses will be given the “suppressed” status. Suppressed A message is marked as suppressed if: the email address we tried to send it to was previously a hard bounce (a permanent failure such as an invalid address), or the end-user marked any previous email message as spam. In this case, all subsequent email messages after the “spam” mark will be marked as “suppressed”. Export campaign metrics On the Metrics tab, you can export metrics for your campaign to CSV files. Go to the Metrics tab in the campaign you want to export metrics for. Click Create Export and select the type of metrics you want to export. All-time metric totals: aggregated message metrics over the life of your campaign Message metrics: Message metrics per day over the date range Journey metrics: Journey metrics per day or hour over the date range Tracked responses: Tracked responses per day or hour for each action that had Track Click enabled For message, journey and tracked responses metrics, select the date range of metrics you want to export and click Export. Metrics are organized daily by timestamp from oldest (row 2) to newest. Under Configure data, click More > Exports to download your export. Metrics in the Workflow View On your campaign’s Workflow tab, you can click Measure to see deliverability stats within the context of your visual workflow. In this view, each metric appears on the bottom of the workflow item. You can choose up to four metrics and the time period. For email metrics, hover over Opened or Clicked to compare human and machine metrics to human-only metrics. Unsubs represents unsubscribing from all messages, not unsubscribing from individual subscription topics. Total Path Metrics The Path Totals workflow items at the beginning of each branch path show the cumulative metrics for every item from the beginning of the path through to the reconnection point or exit block. This allows you to compare the performance of each path. Each metric total is calculated using only the workflow items that have a value for that metric. For example, if a workflow item does not track clicks, the total path Clicked metric will not include that item. --- ## Home dashboard URL: https://docs.customer.io/journeys/dashboard/ Use the Home dashboard to start where you last left off, explore message performance, or determine your next step with suggested action items.  Use the Agent to understand your workspace data and assist in managing your workflows. Click anytime to chat with your Agent, get help, and get started. Learn more in AI Features. Continue where you left off Under Ready to keep going, you’ll see a carousel of pages you last edited. If you haven’t made any changes in the last 90 days, you’ll see a list of actions you can take to get started. Explore your message performance Under How’s your workspace doing, you can track high-level message delivery over the 30 days so you can see trends in workspace performance and respond if things aren’t trending the way you want them to. You can switch between your message channels to hone in on how email vs push notifications, etc are performing. For more information on what these metrics mean, check out our metrics docs: Email Push notifications In-app messages SMS messages WhatsApp messages LINE messages To dive deeper into how messages are performing based on your workflows—campaigns, broadcasts, etc—click Analysis. This takes you to the Analysis page where you can run reports. Review suggested action items Under What should we work on, you’ll see a list of actions suggested by AI. Use these insights to help guide your next action and keep your workspace as performant as possible. Click one to open the Agent and get started. Learn more about how Customer.io uses AI and what features are available to you in AI Features. Track your teammates’ activity Under What’s your team up to, you’ll see a list of your teammates’ activity over the past week. This can help you identify opportunities to collaborate or support your team. --- ## Workspace Performance dashboard URL: https://docs.customer.io/journeys/health/ Your Workspace Performance dashboard helps you monitor workspace health, data processing, and messaging performance. It's the first place to go when troubleshooting delays or checking on the status of your workspace. Click > Workspace performance to go to your dashboard. Start troubleshooting To start troubleshooting, review your Active issues on the Overview tab and dive into data or messaging metrics. Data metrics showcase info for data coming into your workspace. Messaging metrics showcase data for messages leaving your workspace. Check your Overview tab for Active issues. Review your daily metrics on the Data tab to help diagnose data ingress issues. Review your daily metrics on the Messaging tab to help diagnose messaging output and delivery issues. Health status Your dashboard shows the relative health of your workspace with a status indicator: Healthy—workspace is healthy and operating normally. Busy—Your workspace is processing a higher volume of events than normal. There may be delays in processing activities and sending messages. Slow performance—Your workspace is experiencing degraded performance. There are major delays in processing and at least one error that needs your attention. If the status shows a problem, check out Active issues on the Overview tab to learn more. Suggested optimizations Your dashboard surfaces recommendations to improve performance across your workspace. These suggestions help you reduce processing delays and keep your workspace running efficiently before issues pop up. Common recommendations include: Archive unused segments. Review campaigns with no recent sends. They may be candidates for pausing or archiving. Throttle incoming data if your workspace is processing a high volume. Act on these suggestions proactively to prevent performance issues from impacting your data and deliveries. Click Optimize with agent to open the Agent and get started. Overview The Overview tab gives you a snapshot of your workspace’s current performance. It’s the best starting point when you need to quickly understand what needs your attention. Active issues lists problems affecting your workspace. Click an issue to learn more about it. Live metrics gives you insight into the current day’s incoming data, outgoing messages, and the speed of processing. Hover over to learn more about each metric. Processing delay shows you the last 24 hours of processing delays for ingress and egress queues—data processing and outgoing messages respectively. Services The Services tab breaks down the status of individual services that power your workspace. It captures processing of data coming into your workspace and messages going out. Review them to determine which service is affected when the health status shows a problem. Hover over to learn more about each service. There are four possible statuses for each service: Normal—The service has no backlog and any delay is under 10 seconds. Slight delay—The service is delayed between 10 to 60 seconds. Delayed—The service is delayed 1 to 5 minutes. Major delay—The service is delayed over 5 minutes. Data The Data tab helps you understand how your workspace processes incoming data. Data throughput shows your current processing speed. Data processed today compares today’s data volume against yesterday’s so you can quickly spot unusual patterns. Pending signals whether there’s a backlog of data waiting to be processed. Data related issues raises any issues that should be addressed by your team. Tasks The Tasks section helps you monitor background tasks that might take some time to complete, like segment creation and imports. This is the same as the Tasks page; go to those docs to learn more about task types and priority levels. Data Index overview Within the Data tab, you can see a glimpse of the quantity of attributes, events, and custom objects in your workspace, which can help signal whether it’s time to audit your data. Attributes only surface customer attributes, not those for events, objects, or relationships. Here are a few ways you could think about this data: Consider reviewing and/or removing attributes that are unused. Consider consolidating duplicate attributes. If you have unused events, consider reviewing your integrations and determining if you actually need to store these events in your workspace. Objects count towards billing. If the count seems high, check for objects with no relationships to people. This could signal unused objects you could delete. Click View all to go to your Data Index and learn more about attributes and events. From here, you can also filter for object and relationship attributes to see where they’re used. Messaging The Messaging tab tracks the status of message sending. It’s useful for troubleshooting message delays or verifying that campaigns are sending as expected. Outgoing throughput shows the current rate of messages leaving your workspace. Use this to verify that messages are flowing. If throughput drops unexpectedly, check Alerts for more information. Sent today shows the total messages sent today across all channels, excluding anonymous messages. Compare this against your previous day’s volume by hovering over the percentage. A sudden drop will help you spot a problem. Alerts tell you when workflows or message channels experience delays. Check the affected workflow or channel to identify whether high volume, a service issue, or a configuration problem is the cause. Towards the bottom are workflows and channel specific information: Campaigns shows you at a glance whether campaigns are being processed—that is, the number of workflow steps like a journey start, webhook action, etc that your workspace has processed. API-triggered broadcasts shows you whether API-triggered broadcasts are being processed. Pending means your workspace is evaluating recipients and queuing messages. With errors indicates you should take a closer look at your workflow. Push notifications shows you whether this message type is configured. Hover over to learn more. --- ## Analysis page & reports URL: https://docs.customer.io/journeys/run-reports/ On the **Analysis** page, you can define the specific report criteria you want to see and compare your campaigns, newsletters, and broadcasts. Reports let you retrieve data from Campaigns and Broadcasts based on common tags or naming conventions. With reports on the Analysis page, you might: Locate your top performing marketing campaigns. Analyze deliverability of all your transactional campaigns together. Compare journey volume and performance of two similar campaigns. Each report might take a few seconds to run and will automatically run again when you change the report criteria. After results are returned, however, you can toggle any of the display options you’d like without re-running the full report. Take a closer look with our walkthrough: Table view The table view of a report displays every Campaign, Newsletter, or Broadcast that met the report criteria in a table format. The top row of each report shows the cumulative total for all rows in the table. This allows you to understand the performance of an entire category of messages. In the table view you can display any five metrics columns at a time, for any channel you prefer. Each column can be sorted, giving you the ability to locate top and bottom performing Campaigns or Broadcasts. Chart view The chart view of a report allows you to view metrics over time for the Campaigns, Newsletters, and Broadcasts that met the report criteria. You can view one metric over time for each Campaign, Newsletter, and Broadcast using the “Individual” chart type, or you can view the cumulative metrics of all results over time using the “Aggregate” chart view. Both chart types can be viewed with percentage values or raw counts, grouped by daily, weekly, or monthly totals, and filtered by channel type. --- ## Email deliverability metrics URL: https://docs.customer.io/journeys/deliverability-metrics/ The **Deliverability** tab in the **Analysis** section of your workspace shows key email metrics by recipient provider. These metrics represent all outbound emails from your workspace over a timeframe you select. This data can help you identify providers you're performing poorly with so that you can take action to improve deliverability and engagement with your emails. The Deliverability tab in the Analysis section of your workspace shows key email metrics by provider. These metrics represent all outbound emails from your workspace over a timeframe you select. This data can help you identify providers you’re performing poorly with so that you can take action to improve deliverability and engagement with your emails. Metrics The Provider column shows the provider or provider group name: the email services that your recipients use. The Outbound column adds up to 100%—because it represents all outbound emails from your workspace over the timeframe. But for each row, you’ll see that the metrics aren’t additive. Each metric is calculated as a percentage of the total outbound emails sent to a provider. For example, in the screenshot above, you’ll see that 62.1% of outbound emails went to a Google Workspace. Of those emails, 99% were delivered. Delivery: The percentage of emails successfully delivered to people within a provider Bounce: The percentage of emails that were rejected or bounced by a provider Open: The percentage of emails that were opened by users within a provider Click: The percentage of emails that were clicked by users within a provider Spam: The percentage of user-generated spam complaints for a provider Metrics thresholds and color codes You may notice some delivery metrics highlighted in red or yellow. These colors indicate metrics that are below ideal thresholds. Hover over any color-coded value in the interface to see the ideal values for that metric. Red represents an “at-risk” threshold. It means the metric is actively harming your sender reputation and negatively impacts deliverability. Yellow indicates a “needs improvement” status. While not immediately critical, performance at this level may lead to a decline in reputation or deliverability over time if not addressed. — indicates that the metric is not applicable for that provider. In particular, Google doesn’t publish spam complaint data, which prevents us from calculating and displaying your spam complaint rate for Google-based providers. Metric Good Yellow: needs improvement Red: at-risk Delivery ≥ 98% 95-97.9% < 95% Bounce ≤ 1% 1-5% > 5% Spam < 0.05% 0.05-0.1% > 0.1% Open ≥ 30% 15-29.9% < 15% Click ≥ 2% 1-1.9% < 1% Click-to-Open Rate ≥ 10% 5-9.9% < 5% When you come across troublesome metrics, you may want to prune your people list. Making sure that your list of people only includes people who still want, and engage with, your emails is a great way to achieve high delivery metrics and reduce bounce/spam rates. See our Deliverability Best Practices to learn more about the factors that affect deliverability and engagement. Sorting and date ranges Click the calendar in the upper-right corner of the page to change the date range for your metrics. Click Sort or a column header to sort metrics by a different metric/column. Update the date range Change the sorting column Frequently Asked Questions Why are some providers grouped into broader/regional categories? We group smaller providers together by region or provider to increase statistical significance because individual recipient domains often represent a small market share within a region or provider. See the list of providers below for more information. I’ve sent emails to a provider, but it doesn’t show up in the list. Why not? You must send at least 1000 emails to a provider for us to surface data for that provider. Or if you’ve sent emails to a domain belonging to a provider we don’t surface data from, you won’t see it in the list. Provider Groups Below is a list of the specific providers we surface data from, grouped under broad regional designations. Mailbox Provider Associated Domains Apple icloud.com mac.com me.com privaterelay.appleid.com Gmail gmail.com googlemail.com google.com Google Workspace GSuite (Business Domains) Googlers google.com Canadian bell.net cogeco.ca rogers.com sasktel.net shaw.ca sympatico.ca telus.net videotron.ca Chinese 123.com 126.com 163.com easystack.cn foxmail.com qq.com sina.com vip.126.com vip.163.com vip.qq.com German arcor.de freenet.de gmx.at gmx.ch gmx.com gmx.de gmx.net mail.com online.de t-online.de web.de French bbox.fr free.fr laposte.fr laposte.net neuf.fr orange.fr sfr.fr Italian alice.it blu.it fastwebnet.it inwind.it iol.it libero.it pec.libero.it tin.it virgilio.it Japanese docomo.ne.jp ezweb.ne.jp nifty.com yahoo.co.jp Other EU abv.bg bluewin.ch centrum.cz hetnet.nl home.nl mail.bg onet.pl planet.nl seznam.cz skynet.net telenet.be ukr.net wp.pl ziggo.nl Other US ameritech.net att.net bellsouth.net charter.net comcast.com comcast.net cox.net currently.com earthlink.net flash.net mindspring.com optonline.net pacbell.net prodigy.net sbcglobal.net snet.net swbell.net Outlook 365 O365 (Business Domains) Outlook EU hotmail.be hotmail.ch hotmail.co.uk hotmail.com.tr hotmail.de hotmail.dk hotmail.es hotmail.fi hotmail.fr hotmail.gr hotmail.it hotmail.no hotmail.se live.at live.be live.co.uk live.co.za live.com.pt live.de live.dk live.fr live.ie live.it live.nl live.no live.se outlook.be outlook.cz outlook.de outlook.dk outlook.es outlook.fr outlook.hu outlook.it outlook.pt Outlook US hotmail.com live.com microsoft.com msn.com outlook.com outlook.jp passport.com Proton pm.me proton.me protonmail.ch protonmail.com Rackspace rackspace.com Russian bk.ru corp.mail.ru inbox.ru internet.ru list.ru mail.ru my.com rambler.ru tut.by ya.ru yandex.com yandex.ru UK btinternet.com ntlworld.com talktalk.net United Online juno.com mybluelight.com mysite.com netzero.com netzero.net untd.com Yahoo EU sky.com yahoo.at yahoo.be yahoo.bg yahoo.co.uk yahoo.co.za yahoo.com.hr yahoo.com.ua yahoo.cz yahoo.de yahoo.dk yahoo.ee yahoo.es yahoo.fi yahoo.fr yahoo.gr yahoo.hu yahoo.ie yahoo.it yahoo.lv yahoo.nl yahoo.no yahoo.pl yahoo.pt yahoo.ro yahoo.se yahoo.sk Yahoo US aim.com aol.co.uk aol.com aol.de aol.fr citlink.net cs.com epix.net frontier.com frontiernet.net myyahoo.com netscape.net rocketmail.com verizon.net y7mail.com yahoo.ca yahoo.co.nz yahoo.com yahoo.com.ar yahoo.com.au yahoo.com.br yahoo.com.mx ymail.com --- ## Understanding your A/B test results URL: https://docs.customer.io/journeys/understanding-ab-results/ Note: the information in this document only applies to testing campaign messages. See A/B Testing Newsletters for more on testing newsletters. What does ‘Chance to Beat Original’ mean? Chance to beat original (CTBO) is the likelihood that the variation will outperform the original. It determines statistical significance by answering the question: Is the difference observed between the original and variation greater than a difference due to random chance? What does a CTBO of 50% mean? A CTBO of 50% means that the variation will outperform the original 50% of the time, which is the same as random chance. Therefore, there is no difference between your original and variation. The closer your CTBO is to 50% (e.g., 40% or 60%), the less significant the difference between your original and variation. The further your CTBO is from 50% (e.g., 5% and 90%), the more likely it is that there is a true difference between your original and variation. What does “Original is beating variation” or vice versa mean? Customer.io uses a significance level of 95%, meaning that CTBO has to be less than 5% or greater than 95% in order for us to report that one version is outperforming another. If CTBO is > 95%, you can infer that your variation is outperforming your original. If CTBO is < 5%, you can infer that your original is outperforming your variation. What does “Not significant, need more data” mean? If your CTBO is between 5% and 95%, it does not meet our threshold for statistical significance. If your sent volume is low, gathering more data may prove that a significant difference exists. How is ‘Chance to Beat Original’ CTBO calculated? There are 3 steps to calculating CTBO: 1. Calculate the standard error for the original and variation. x = number of messages with the desired behaviour (e.g. clicks) n = proportion of all messages with the desired behaviour (x / total, between 0 and 1) standardError = SQRT(( n * (1 - n) / x )) 2. Calculate the Z-score for the original and variation. p_o = mean success rate for the original p_v = mean success rate for the variation se_o = standard error for the original se_v = standard error for the variation Z-score = (p_o-p_v)/SQRT(POWER(se_o,2)+POWER(se_v,2)) 3. Based on the Z-score use a statistics table to determine the p-value and CTBO. CTBO = 1 - p-value --- ## Getting conclusive results from A/B tests URL: https://docs.customer.io/journeys/conclusive-ab-results/ Testing messages sent to your user base is a fantastic way to learn about your consumers and continue to improve your messaging strategies. But how can you be sure that your tests are actually telling you what you think they’re telling you? When conducting tests, you should aim to maximize the level of confidence you have in the results of the test. This is where statistical significance comes in. For instance, how do you know that your most engaged users didn’t happen to end up getting the same variation by random chance? Calculating statistical significance is a way of determining how confident you can be that the results of a test are because one variation is more effective than the other. Let’s take an example: If I want to test whether one email subject line causes more opens than another, and variation A has a 60% open rate and variation B has a 40% open rate, then variation A is the more successful subject line, right? Not necessarily. Understanding a result requires looking at how many people have received each variation and how many performed the desired action. Imagine that I only sent each variation to 5 people and 3 of the 5 opened variation A, while 2 of the 5 opened variation B. Is a difference of only 1 out of 5 enough to indicate that the opens were actually because of the subject line and not just due to chance? Definitely not. Now imagine that I sent each variation to 5,000 people. This means that 3,000 opened variation A and 2,000 opened variation B. I’m starting to build a bit more confidence in the fact that the subject line of variation A is the reason that 1,000 more people opened A than B. It’s intuitively clear that the larger numbers provide more confidence, but how confident? 50% chance it wasn’t just random? 90% certain A is better than B? In an ideal testing world, testers typically aim for 95% confidence in their tests to declare it valid. The more people interact with your messages over time, the higher your confidence level can be. In a marketing world where your primary goal is to get a message out to your users in a timely fashion, you can also choose to accept a lower confidence level in order to make a decision faster. The question you should ask yourself is, “if I aim for an 80% level of confidence, can I really tolerate a 1 in 5 chance that my test is misleading me?” If nothing else, statistical significance can help your team to determine which tests are worthwhile, and which can be skipped because you cannot get a high enough confidence level in the result. Calculating Statistical Significance When testing a newsletter with Customer.io, all of the information you need to calculate the confidence level of your test results is provided on the Test tab once the test has started (if you’re testing campaign messages, see Understanding your A/B test results). There are a number of statistical significance calculators that you can use with the information provided. We’ve chosen to use Neil Patel’s calculator for the following example. Note: If you have more than 4 variations in your test, here is one that accommodates up to 10 variations. If you are testing different subject lines on your emails, it is most likely that success will be defined as the email with the highest open rate. To determine if the difference in the open rates is not by chance, go to the Test tab for the newsletter you are testing and open the statistical calculator of your choice. Enter your test result numbers into the calculator following the guide below: What does the result mean? Each calculator will have a slightly different way of communicating results, but this calculator uses the phrasing, “I am 90% certain that the changes in Test “A” will improve your conversion rate.” This mean there is a 10% chance that the higher open rates in variation A were just random chance and not a pattern. How can I increase the likelihood of statistical significance? We all love being able to declare a winner of a test with high confidence. So how can you increase the number of times your tests are statistically significant? The math that determines significance is driven by 2 primary factors: sample size and test duration. Sample size The larger the test’s sample size, the quicker it will achieve the number of email actions required to meet your desired level of confidence. In our world of marketing messages, the quicker we can achieve valid results, the better. If we don’t have large sample sizes, the tests will likely need more time to achieve statistical significance (and if the sample sizes are small enough, they may never get there). A rule of thumb is a sample size of at least 500 recipients per variation, but it’s worth taking the time to calculate sample sizes specific to your needs and confidence levels using a sample size calculator. Test duration Although it is not used in the statistical significance calculation, the duration of a test impacts confidence in the results because the more time users have to act on the email (open it, click it, or convert), the more information you have for calculating the results. When planning how long the test should run, first consider the likelihood that the action will be taken at all. Opening emails is the easiest of the 3 metrics for a user to complete and thus you can expect a greater number of people to do it faster. As you can imagine, the amount of time it takes to gain confidence in the results of a test measuring open rate is much lower than for one measuring conversions. Armed with at least a basic knowledge of statistical significance and what it means for A/B Testing newsletters, you should be able to increase the productivity of your messaging. Though the concepts and calculations might seem intimidating at a glance, remembering that larger sample sizes and attention to test duration will increase the effectiveness of your tests is a great place to start. --- ## Failed and attempted messages URL: https://docs.customer.io/journeys/message-failed/ If you see ‘Attempted’ or ‘Failed’ as a status on your message, there are a few reasons why that might be the case. Attempted means that your message has been created and we’ve tried to send it to the delivery provider, which then would send to your end user. If you’re sending an email, for example, that means that your email has been put together in Customer.io, and we’ve tried to send it to an ESP such as SendGrid. If a message status is showing as attempted, it means that we’ll try to send it again soon. Eventually, the message will send successfully, or it will fail. Customer.io will retry your request with exponential backoff—up to 11 times over a period of approximately 1 hour. Failed means that we weren’t able to successfully send your message to the delivery provider. It never left Customer.io. Fix a failed message Follow the steps below to diagnose and fix the issue. Often, a liquid tag is the reason a message failed to send, but even if it’s not, these steps can help you move forward. 1. Locate the failed message. You can go to the Deliveries & Drafts page then filter for the failed message. You can also find them in the Sent tab of your campaign or broadcast. Click on the subject of the message. Here you’ll find the reason for the issue: In this example, the customer’s name was missing from their profile, so we couldn’t render and send your email to the customer. Click Fix to find the issue and edit the message template. 2. Fix the problem. Locate the error in your message template. There are a couple of ways to fix this particular error. Option 1: You can edit your Liquid code so that it includes a fallback option. A fallback tells Customer.io what to do when that attribute doesn’t exist, so your message will still be sent. If your template uses our latest liquid, you could use this fallback: {{ customer.name | default: "there" }} If your template uses legacy liquid, you could use this fallback: {% if customer.name != blank %} {{ customer.name }} {% else %} there {% endif %} This way, if the recipient doesn’t have the name attribute, the email will default to “Hey there!” instead of failing! Option 2: Add the missing attribute and value to your recipient. In case this particular person is missing that attribute and it should be there, you can add it to their profile. 3. Send again! After you’ve fixed the error, go back to the delivery detail page of the failed message. Click Retry in the top right to send it again. If you retry a webhook action, we send a new request. Other errors Different message types can fail with different error messages; it doesn’t always have to do with liquid. In the case of push notifications, for example, a device may fail because the device token on the profile is incorrect: Or, for SMS, a phone number may be missing. No matter what, you can find this information on that message’s delivery page. Some errors may simply instruct you to contact us. If you encounter more attempts or fails than expected, let us know and we’ll help you troubleshoot! --- ## Integration Directory URL: https://docs.customer.io/integrations/directory/ Search for the integrations we support. Click a card below to go to documentation for an integration in our directory! content --- ## Quick start guide URL: https://docs.customer.io/integrations/getting-started/quick-start-guide/ While there are plenty of ways to get data into, and out of, Customer.io, our JavaScript integration is one of our most popular and easy-to-use integrations. On this page, we'll walk you through setting it up so you can identify your website visitors and send them in-app messages. 1. Set up your JavaScript integration Our JavaScript client feeds data from your website into Customer.io. You’ll add *data inAn integration that feeds data into Customer.io. integrations for every system or touchpoint you want to collect data from. Go to Integrations and click Add Integration. Select our JavaScript integration. Enter a Name for the integration and click Submit. You should give your integration a name that helps you and your teammates understand the resource you’re capturing data from—like “company website” or “community forums.” Under Installation Instructions, you’ll find the JavaScript snippet that you need to add to your website. Copy this code and paste it in your website’s <head> tag. Click Test connection to make sure that everything is setup correctly. If you want to skip this step, click Save & complete later. Otherwise, if your test is successful, click Complete Setup. That’s it! Now you’re ready to capture data from your website. 2. Make a page call Go to the website where you installed the JavaScript snippet in the steps above and visit a few pages. In Customer.io, go back to the Data In tab. You should see page events! These events represent the pages people visit on your website. But these events are still anonymous. You’ll need to identify your audience to associate anonymous data with people you know. That’s what we’ll do next.  Do you have a single page app? The snippet automatically calls cioanalytics.page() when the snippet loads. If you have a single page app (SPA), it’s likely that each route isn’t necessarily a new page, so you’ll need to call cioanalytics.page() manually for each route in your app. 3. Identify website visitors You can identify people when they provide their email address on a form, log in to your website, and so on. In your console or in your code, you can call cioanalytics.identify() to associate anonymous data with a person. // What a call typically looks like: // cioanalytics.identify('userId or email address', { // attribute1: 'value1', // attribute2: 'value2', // }); // Realistic example: cioanalytics.identify('cool.person@example.com', { first_name: 'cool', last_name: 'person', favorite_color: 'blue', }); Now refresh your page and look at the API Calls tab in your source. You should see an identify call and that your page calls are now associated with that person! Beyond identifying people and seeing the pages they visit, you can also send custom events, associate people with custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and more. See our JavaScript source for help implementing the full array of events you can (and should) capture from your source. 4. (Optional) Set up in-app message support Now that you’ve identified people and captured page views, you can also set up and send your first in-app message! Go to Settings > Workspace Settings > In-App, and Enable in-app messages. Click Send a test message in the in-app message settings and enter the email address or ID of the person you identified earlier. It may take up to a minute to get your first test message because we poll on a longer interval until you receive your first message, but you should see it shortly. When you receive your first message, you’ll know that you’re properly set up to send in-app messages to the rest of your audience. Your JavaScript integration is complete, and working from end to end!  We support anonymous in-app messages too! If you’re on a Premium or Enterprise plan, you can also send in-app messages to anonymous visitors to your website. See anonymous messages for more information. --- ## Understanding Integrations in Customer.io URL: https://docs.customer.io/integrations/getting-started/how-it-works/ We make it easy to connect data from sources to destinations—including Journeys—so that you can take advantage of your data, no matter where it comes from or where you want it to go. Why integrate with Customer.io? While you can add people manually, upload CSV files, and get data into and out of Customer.io other ways, integrating with Customer.io unlocks the ability to respond to your audiences in near real-time. For example, imagine that you want to remind people to complete their purchases when they abandon their carts. Integrating with Customer.io makes this easy: your integration sends events to Customer.io when people add items to their carts, which you can then use to send messages to your audience if they don’t complete their purchases within a certain amount of time. If you don’t integrate with Customer.io, you’d need to go to your sales platform, export a list of users who haven’t finished a purchase, and upload it to Customer.io. And you’d need to repeat this process every time you want to encourage people to finish their purchases, which is obviously a tedious process that doesn’t scale with your business. Integrations unlock automated, personalized communications with your audience so you can engage people at scale. Integrations are directional When you look at our integrations directory, you’ll see that we offer two major categories of integrations: Data In integrations help you feed data into Customer.io. Data Out integrations help you send data to all the places where you want to activate or use your data. You can send your incoming data downstream to as many services as you want. This means you can use Customer.io as a Customer Data Platform (CDP) in addition to your messaging needs! flowchart LR subgraph Data-in direction LR a(Website) b(Mobile App) c(Database) end d((Customer.io)) a-->d b-->d c-->d subgraph Data-out direction LR e(CRM) f(Data Warehouse) g(Analytics Provider) end d-.->e d-.->f d-.->g Data in To get started with Customer.io, you’ll want to send data into your workspace from your data sources so you can automate communications with your audience. Check out our integrations directory to see “data source” integrations. JavaScript: In general, you’ll want to install our JavaScript library in your website(s). It’s one of the easiest ways to feed data into Customer.io and it supports in-app messages for your website visitors. Mobile SDKs let you send data to Customer.io from your mobile app and support push and in-app notifications! Server-side libraries (NodeJS, Python, Go) help you send data directly from servers when you can’t gather data from your client. APIs: Use our REST APIs directly to send data to Customer.io. Our Pipelines API is ideal for sending customer data and events, while the App API handles fetching data and managing your workspace. Reverse ETL integrations (Amazon RedShift, Snowflake, etc) help you feed data into Customer.io directly from your database. Salesforce: Premium This feature is available for Premium plans. Incorporate leads, contacts, accounts, and other Salesforce objects in your workspace. HubSpot: Premium This feature is available for Premium plans. Beta This feature is available for Premium plans. Activate your HubSpot data in Customer.io and send it to other downstream integrations! Forms: We support a number of form provider integrations, including Typeform, Google Forms, and more. These integrations use a JavaScript library to capture data from your forms and send it to Customer.io. Data out Unlike data in integrations, you don’t need to install anything to send data out to another service. You’ll set up a data-out integration in Customer.io, typically with credentials for your destination service, and we’ll send your data downstream. This lets you send data to Customer.io and downstream to other services. This makes Customer.io easy to fit into your existing stack, and lets you use your engagement data to enrich data in downstream services. We have a whole catalog of outbound integrations. Check it out! Understanding our APIs Customer.io offers three APIs; two for data in and one for data out. Pipelines API: This is our primary data ingestion API, designed for sending customer data, events, and managing people in your workspace. Most of our SDKs and libraries are built around the Pipelines API, making it the go-to choice for feeding data into Customer.io. App API: This API focuses on fetching data and managing your Customer.io workspace configuration. Use the App API when you need to retrieve information about your campaigns, segments, or other workspace data. The key difference is direction: the Pipelines API is optimized for sending data to Customer.io, while the App API is designed for getting data from Customer.io. When choosing an integration approach, consider whether you’re primarily sending data (use Pipelines API or our SDKs) or retrieving data (use App API). Data Residency: US and EU regions We offer US and EU regions. While your region accounts for the location of the data you keep in Customer.io, it doesn’t account for your integrations. For example, you could be in our EU region but send data to a downstream service in the US, and vice versa. Some of the services we integrate with, like Mixpanel, have regional settings that you can configure to make sure that your data stays in the region you want it to. When you set up a data out integration, make sure the service you integrate with stores data in the right region for you! --- ## Troubleshooting URL: https://docs.customer.io/integrations/getting-started/troubleshooting/ When data doesn't flow into Customer.io or out to other services, you may need to determine if you have a problem with your data-in integration, your data-out actions, or your connection with your incoming and outgoing services. This page helps you check the various areas of your pipeline to find and solve problems. General troubleshooting tips Check your Integrations page to determine if an integration has a problem. You can hover over each integration to see if we’ve noticed a problem. Check your incoming integration’s Data In tab: are you sending the right calls to Customer.io? Check your outgoing integration’s Data Out tab: are you sending data to its ultimate destination? Pinpoint the problem If you have a problem with an integration, the first thing you’ll need to do is figure out where the problem is—whether your data reaches the appropriate system, is it formatted correctly, and so on. Is data reaching Customer.io? If not, the problem is with your data-in integration. Is data reaching its expected destination? If not, the problem may have to do with your data outAn integration that sends data out of Customer.io. integration’s credentials. If you’ve connected a data-in integration to multiple outgoing integrations: Is the problem limited to a single outgoing integration? If yes, the problem is likely in a specific data-out integration. Do all outgoing integrations have the same problem? If yes, your problem is likely in the data-in integration. If data reaches its final destination, but doesn’t look quite right, then the problem is in your data-out integration’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination.. flowchart LR a{Do you see incoming data?} a-....->|no|c(The problem is in your data-in integration) a-->|yes|b{Does some data reach the destination} e--->|yes|f(The problem is in your data-in implementation) b-...->|no|d(Check your outgoing integration's credentials) b-->|yes|e{Does data fail at multiple destinations?} e-.->|no|g{Is data missing entirely?} g-->|yes|h(The problem is a data-out action trigger) g-.->|no, data is partially missing/wrong|i(The problem is a data-out action mapping) Troubleshooting data-in problems Go to your data-in integration’s page and check the Data In tab. Now you can examine your incoming data: Do you see any data at all? If not, check your integration’s credentials and make sure that you’ve added the correct library to your client, server, or app. Do you see all the calls you expect to see? If not, you may need to make sure that your integration sends the correct methods—identify, track, and so on. Do your calls contain the correct data? If not, check that your integration includes the correct information in the payload. flowchart LR a(Go to your integration's Data In tab) a-->b{Do you see any data?} b-...->|no|c(There might be an auth problem in your code) b-->|yes|d{"Is a call missing (e.g. group, track)?"} d--->|yes|e(There's a call missing in your code) d-.->|no|f{Do calls contain the right data?} f-.->|no|g(Update your calls tosend the right properties) f-->|yes|h{Are you sure you don't have a data-out problem?} Troubleshooting data-out problems Go to your data-out integration and check the Data Out tab and observe the data flowing out of Customer.io: Do you see data at all? If not, make sure that your integration credentials are correct. Do you not see data of a specific type? Check your integration’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to make sure that they’re triggered correctly. Do your actions not contain the correct data? You likely have an issue with the data mapped to one or more actions. flowchart LR a(Go to your integration's Data Out tab) a-->b{Do you see any data?} b-...->|no|c(Make sure your data-in integrations are connected to the right destination)-.->i(Check your data-out integration credentials) b-->|yes|d{Are you missing an action?} d---->|yes|e(Check that the action trigger is correct) d-.->|no|f{Do calls contain the right data?} f-..->|no|g(Make sure data is mapped correctly) f-->|yes|h{Do you see failures on the Overview tab?} h-->|yes|j(Your service might be down) h-.->|no|k{Are you sure you don't have a data-in problem?} Use the Data Out Tester to pinpoint issues Your outgoing integration’s Tester tab can help you send test calls to find problems in your integration’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination.. When you go to your integration’s Tester tab, you’ll see a Test Action area. Here you can mimic API calls to test your actions. For example, if you trigger an action when a Track Event Name is Clicked Button, you can set the Type to Track and change the event field to Clicked Button to test your action. If you see a response, your Trigger is set up correctly! The Response area shows the call as sent to your destination. Here you can check that your action’s Mappings are set correctly. For example, if you map a property called firstName to fName, and fName is correctly set in your response, then your data is mapped correctly! Troubleshooting data-out actions Your outgoing integration’s actions tell us when to send data, and what data to send, to its ultimate destination. Each action has two major components: The Trigger tells us when to send data out The Mappings tells us what data to send out If you go to your outgoing integration’s Data Out tab, you can quickly see your problems. If you don’t see a particular action (and you know that the appropriate data comes into your integration pipeline), there’s probably an issue with your action’s trigger. If you don’t see specific data in an action, there’s an issue with your data mapping. Problems with triggers A trigger determines when we send data out of Customer.io to a particular integration. If you used one of the default triggers that Customer.io already set up for you, the trigger probably isn’t your problem! But, if your action doesn’t trigger at all, or it does so inconsistently, then you likely have a trigger issue to troubleshoot. But if you use a custom trigger, we occasionally see the following issues: Make sure your trigger conditions don’t conflict Some parameters aren’t compatible with each other. For example, if you set a trigger to fire when All conditions are true, and you set conditions where Type is Track and an Identify or Group Trait condition, your action will never fire. Track calls don’t contain identify/group traits, so your trigger conditions never occur together. This means that your action will never fire. Make sure you’ve spelled conditions correctly When you add certain conditions, like Identify or Group Trait and Track Event Name, you’ll provide us the names/values of some of your properties, and your spelling and capitalization matter! Make sure that you enter the names and values of trigger values exactly as they appear in your code or integration. Do you want to trigger when all conditions are true, or some conditions are true? When you set up a trigger with multiple conditions, we default to All. This means that all your conditions have to be true to trigger the action. But if you want to perform the action when any of the conditions are true, you need to change the top-value under Trigger! Problems with mappings If data makes it to its ultimate destination, but it’s formatted incorrectly or missing specific properties, you probably have an issue with mappings! Make sure you’ve spelled your property names and traits correctly When you map values to an outbound integration, you often have to type out the names of properties. For each action, we look for properties exactly as you provide them. If the incoming property uses a different case or is spelled differently than your mapping, we’ll ignore it! Set a fallback for empty values (using coalesce) If you map a value, but it isn’t always populated in incoming call(s), it may be empty, null, or missing in outbound data. You can set a fallback using the coalesce function in your mappings. If the value you want to map is empty or absent, we’ll use the coalesce fallback to make sure that the value is populated correctly. --- ## Data Compliance and Privacy URL: https://docs.customer.io/integrations/getting-started/data-compliance/ We want to help you stay in compliance with GDPR and other privacy regulations. If you're a premium customer, we can also help you maintain HIPAA compliance. GDPR and regulatory compliance GDPR is the EU’s General Data Protection Regulation, and provides rules for handling customer data within the EU. But, even if you don’t have customers in the EU, you may want to abide by these rules—more or less—to prepare for data privacy rules in other locations and to respect your audience’s privacy. To help you maintain GDPR compliance, we: Store and transfer data securely. Information in our North America data center does not leave North America unless you send it to a service outside the US. Provide a way to suppress and remove customer information. Per GDPR and other regulations, your audience has a right to be forgotten. Should they revoke consent to data collection, you can permanently delete people and prevent the system from collecting data about them in the future. Provide schemas and a record of your data, helping you understand exactly what data you’re collecting from data-in integrations and what you send on to each of your data-out integrations. But, beyond that, you must obtain and manage consent to collect data from users of your websites and services. For example, our JavaScript client library manages user information in cookies and local storage. You should obtain consent before invoking calls from our JavaScript snippet that identify your audience. HIPAA compliance Customer.io integrations are HIPAA-ready, meeting the privacy and security requirements of both the healthcare industry and your valued customers. Contact your Customer.io representative if you’d like more information about Customer.io’s HIPAA compliance or want to sign a Business Associate Agreement (BAA) with us. For best practices on sending HIPAA-compliant SMS and MMS messages, see our HIPAA compliance and privacy regulations guide. Suppressions: respect your audience’s right to be forgotten When people unsubscribe, they might request that you stop collecting data and delete all the data about them. When this happens, you can suppress a person’s identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. to comply with their wishes. Suppressing a person: Ensures that you cannot send the person messages. Prevents integrations from collecting data for the person or triggering integration Actions. Prevents the person’s activities from appearing in the Data In log. Prevents us from replaying the person’s data to new integrations. If a user opts into data collection later, you can unsuppress them, resuming data collection for that person.  Wait to collect data until you have unambiguous consent The ability to suppress users is not a substitute for user consent to collect data. You should not identify your users or collect un-anonymized data until your audience opts into data collection. This isn’t just a way to abide by various regulations; it’s a way to maintain your audience’s trust. Suppress a person When a person invokes their right to object or right to erasure under GDPR or CCPA respectively, you can suppress their profile through our API or on the People page. It may take a few minutes for us to process a suppression request. Go to the People page. Select the people you want to suppress. Click Delete forever and toggle the option to Delete and Suppress. Confirm the operation. Unsuppress a person If a person opts back into data collection after being previously suppressed, you can unsuppress their userId and resume data collection by sending a track event called Unsuppress Person. For example, with our JavaScript client library, you’d send this call to unsuppress a person: cioanalytics.track("Unsuppress Person", { // if you identify by email, pass the email address as the userId userId: "person-i-want-to-unsuppress" }); Suppressions don’t affect Web-only integrations Web-only integrations send traffic directly to a website or service from the client, bypassing Customer.io servers. Because the data doesn’t pass through Customer.io, we can’t check that it belongs to a suppressed person. For example, if you suppress a person in Customer.io, you could still inadvertently send data downstream to your Meta (Facebook) Pixel integration, because the data you send to Meta doesn’t pass through Customer.io’s servers. It goes directly from your JavaScript client to Meta. This means that you may need to implement suppressions or a blocklist in any services you connect to using a Web-only integration. --- ## Introduction URL: https://docs.customer.io/integrations/data-in/getting-started/ A *data-in* integration is a website, server library, mobile SDK, or cloud application that sends data into Customer.io. It’s where your data originates. What is a data-in integration? A data-in integration is a website, mobile app, database, or service that you want to capture data from—it’s a “source” of data! These integrations help you collect data from your various customer touchpoints so you can route it to Customer.io and other outgoing integrations, so you can maintain an unbroken chain of customer data throughout your stack. You’ll add a different data-in integrations for each website or app you want to collect data from. Each integration has an API Key, which lets you send data to Customer.io.  Just getting started? Try out the JavaScript Client Snippet! Our JavaScript client snippet is the easiest to get started with and is universally supported as a source of data for our data-out integrations. See our Getting Started guide, or check out the video below, for help setting up our JavaScript integration. How many incoming integrations should I have? You should create a different “integration” for each application that you want to collect data from. For example, your website and your mobile app are different integrations; they might represent the same services, but are separate applications. Having a different integration per app: Makes your implementation flexible: granular integrations help you control the specific data that you want to send to different destinations. If you use one API key for all your data, it’ll be harder to filter the data you send out of Customer.io! Helps you debug integrations: different API keys per integration makes it easy to see when and where problems occur. Types of data-in integrations We organized our incoming into categories to help you find the right integrations. We’ll add new integrations periodically, but they’ll typically fall into these categories: Website: You can install our JavaScript library to capture information about people, events, and so on. Mobile: Use our mobile SDKs to capture information about people, events, and so on. Server: Server-side integrations let you send data directly from your servers. Data Warehouse: Use your data warehouse as a source of data for Customer.io. You might do this if you collect data in a single place before you send it to Customer.io. Database: Use your database as a source of data. CRM 🎉New : Activate your Salesforce data in Customer.io and send it to other downstream integrations! Your workspace is also a data-in integration! Customer.io generates metrics from the messages you send, the messages people open, and so on. You can send this data out to various integrations as well. This makes Customer.io its own “data-in” integration! When you set up a Data Out integration, you can click Add Source and select your workspace as a source of data to send events reflecting the messages you send out of Customer.io --- ## Understanding incoming data URL: https://docs.customer.io/integrations/data-in/source-spec/incoming-data/ We map the data you send into Customer.io to your outbound integrations. Your incoming data represents people who visit your website and use your services. This page helps explain the kinds of data you'll send into Customer.io. How it works When you send data into Customer.io, it only takes a few different shapes—identify, track, and so on. But the services you connect your data to have different requirements and use your data in different ways! So we map incoming data to the places where you use it with what we call Actions. Actions are the things your incoming data will do when it reaches its destination, like creating people, updating people, tracking custom events, etc. Actions are unique to each data-out integration. This page describes the different kinds of calls, so you can: Properly implement our SDKs, libraries, and calls. Understand the data that you can capture and forward to places outside of Customer.io.  Check out our API See our Pipelines API reference for descriptions of all the calls on this page, a list of fields supported by each call, and code samples for our libraries! You can send data in any order In most cases, people who use your site or app begin as anonymous users. You can still track their activities, but they’re attributed to anonymous people (by anonymousId). When someone logs into your app, provides their email, and so on, you should send an identify request. Most integrations—including Customer.io—handle this situation gracefully and associate anonymous activity with the identified person. This means that you don’t need to manage the order of operations: you don’t have to identify someone before you send a track event, and so on. flowchart LR a(track call)-->b{Does the person the event represents exist?} b-->|yes|c(attribute event to person) b-.->|no|d(Customer.io automatically creates this person)-.->c Identify The identify method represents a known person. You’ll typically send identify calls when a person makes themselves known to you—like when they log into your app, sign up for an account, or provide their email address. With every identify call, you can pass traitsInformation that you know about a person, captured from identify events in Data Pipelines. Traits are analogous to attributes in Customer.io Journeys.—things that you know about a person, like their first name, their interests, etc. { "type": "identify", "traits": { "first_name": "cool", "last_name": "person", "email": "cool.person@example.com", "plan": "premium" }, "userId": "97980cfea0067", "created_at": "1679407797", } Group A group call associates a person with a group—like an account, an online class, or recreational league. When you send a group call, you’ll send a groupId. If the groupId doesn’t already exist, downstream integrations typically create it—like when someone creates a new account or starts a new recreational league. You can also set traits for the group. These are things you know about the group, like the account’s billing date, the name of the teacher for the class, or the name of the recreational league. { "messageId": "4vl6zh", "timestamp": "2022-11-24T22:56:14.144Z", "type": "group", "traits": { "name": "Example Co, Inc.", "industry": "edtech", "plan": "enterprise" }, "groupId": "example-company", "userId": "97980cfea0067" }  In Customer.io Journeys, we refer to groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. That means that we map incoming group calls map directly to objects. Track event The track method helps you represent your audience’s activities—the things that they do in your app or on your website. Track calls can contain properties—extra data about on event beyond the event name. For example, if someone adds a product to their cart, you might keep a list of properties about the product they added to their cart. If someone starts an online course, you might capture additional information about the course. Properties are like traits for an event! { "messageId": "i6yatl", "timestamp": "2022-11-24T22:48:17.593Z", "type": "track", "email": "cool.person@example.com", "properties": { "class_name": "Customer.io Basics", "class_code": "cio101", "start_date": 1679410730 }, "userId": "97980cfea0067", "event": "Course Enrolled" } Page The page method represents the pages that people visit on your website. It helps you monitor the places that people do, and don’t, visit in your app or website. Page calls can help you follow up with people, to see if they’re still interested in a particular product, online class, and so on. Our JavaScript client automatically captures page events on load. But you’ll need to invoke the page method manually if: You use our server-side libraries. You have a single-page app. You want to augment the call with special properties. { "messageId": "efxqsi", "timestamp": "2022-11-24T22:55:59.498Z", "type": "page", "email": "cool.person@example.com", "properties": { "session_started": 1679410730, "url": "https://www.example.com" }, "userId": "97980cfea0067", "name": "home" } Screen The screen method is like the page method, but for your mobile app(s). Screen calls represent the screens that people use in your app, helping you better understand which parts of your app people use. Like page and track calls, you can send properties about the screen that you might want to use in your downstream integrations. { "messageId": "9zk6c", "timestamp": "2023-03-20T22:56:06.259Z", "type": "screen", "email": "cool.person@example.com", "properties": { "session_started": 1679410730 }, "userId": "97980cfea0067", "name": "home" } Alias The alias method is only supported by Mixpanel’s original API. If you use Mixpanel, you can enable simplified identity merging to associate anonymous activity with an identified user without sending alias calls. The alias method reconciles identifiers in systems that don’t automatically handle identity changes—like when a person graduates from an anonymous user to an identified user. For example, a person has an anonymousId until you identify them by userId. Most integrations will automatically associate data representing an anonymous ID with the new user ID when you send an identify call, but some may not! The alias call tells these kinds of services to represent the anonymousId (as the previousId) with the new userId. { "previousId": "sqsv42VjV1e2d8ha2SHxM6", "userId": "97980cfea0067" } If you need to use the alias method, you’ll want to call it before you first identify someone by their userId. For example, using our JavaScript snippet, your flow might look something like this: // the anonymous user does actions under an anonymous ID cioanalytics.track('92734232-2342423423-973945', 'Anonymous Event') // the anonymous user signs up and is aliased to their new user ID cioanalytics.alias('92734232-2342423423-973945', '1234') // the user is identified cioanalytics.identify('1234', { 'plan': 'Free' }) // the identified user does actions cioanalytics.track('1234', 'Identified Action') --- ## Identify URL: https://docs.customer.io/integrations/data-in/source-spec/identify-spec/ The identify method represents a person and their traits, helping you send information about users or customers in your system to your destinations.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. How it works The Identify method helps you represent a user and their traits — the things you know about them, like their name, email address, and so on. When you use most of our libraries, like our JavaScript client, identifying a person keeps them in memory, so that future calls reference the identified person. This saves you the trouble of sending identifying information with every call.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. When to send an identify call While things vary based on the SDK or library you use, you should send an identify call when someone: First registers with your service Logs into your service or app Update their information—like when they change their email address, add a phone number, or set their subscription preferences In general, you shouldn’t send an identify call on every page load. Beyond the three cases above, additional identify calls are unnecessary and could cause you to hit your plan’s limits. If you use our JavaScript client, we’ll automatically apply the right identity to calls after you identify a person. We store identity information in a cookie and local storage; if these values are present, you don’t need to send another identify call unless you want to update the person’s traits. If you use our other libraries, you’ll need to add the anonymousId or userId to your calls, but making additional identify calls won’t fetch this information for you. A typical call A typical identify call is relatively straightforward. It contains a userId and a traits object. The userId is a unique identifier for the person you’re identifying. The traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. object contains information about the person, like their name, email address, and so on. While this is the general shape of your call, our libraries actually capture much more information. See the full payload below. You can send additional objects in your call, overriding the information that our libraries typically capture, but you’ll need to find documentation for your specific library for details. cioanalytics.identify('97980cfea0067', { firstName: 'cool', lastName: 'person', email: 'cool.person@example.com' }); The full payload While your requests are typically short, our libraries capture much more information. This helps us provide context, not only for the person you identify, but the service you identify them from. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "type": "identify", "traits": { "name": "Cool Person", "email": "cool.person@example.com", "likes_baseball": true, "games_attended": 5 }, "userId": "97980cfea0067", "integrations": { "All": true }, "messageId": "string", "receivedAt": "2019-08-24T14:15:22Z", "sentAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", "version": 0, "context": { "active": true, "ip": "string", "locale": "string", "userAgent": "string", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string" }, "page": { "name": "string", "path": "string", "referrer": "string", "search": "string", "title": "string", "url": "string", "keywords": [ "string" ] } } } integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. type string The event type. This is set automatically by the request method/endpoint.Accepted values:identify version number The version of the API that received the event, automatically set by Customer.io. Identifiers: User ID and Anonymous ID You identify people with a unique value: either a userId or an anonymousId. The userId is typically a UUID/ULID that represents that person across your backend services, though it can take any form you like as long as it’s unique. An anonymousId is a value you assign to someone you don’t know yet. Every identify call must have a User ID or an Anonymous ID. User ID User IDs are a more permanent identifier for a person, like a database ID that represents a person across all your backend services. Since these IDs are consistent across a customer’s lifetime, you should use them to identify a person as soon as you know who they are. Email as a userId Customer.io lets you identify people by email address. So, as long as email addresses are unique to individual people in your use case, you can pass an email address as the userId. We’ll parse emails and automatically set them as email attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for you. This provides a handy way to identify people as leads when they’ve just provided their email address and then pass a database identifier for them when they become a customer, user, etc. While this works really well for Customer.io, it doesn’t necessarily work for all integrations. Some services don’t support email addresses as userIds, and some require a unique identifier that’s consistent across all your backend services. If you do pass email as a userId, you should check that your integrations support email addresses as userIds. If you need to send email as an identifier, and some of your integrations don’t support that kind of model, you can filter the places you send your data toAn integration that sends data out of Customer.io. using the integrations object. Or you can modify your integration’s actions to exclude calls that use an email address as a userId. Anonymous ID In many cases, you may not know who a person is yet — like when someone browses your website before they sign up for an account or log in. But in these cases, you may still want to capture information they voluntarily provide, events they perform, and pages they view. In these cases, you can reference a person with an Anonymous ID. The Anonymous ID can be any pseudo-unique identifier. If you use our JavaScript library, we automatically generate anonymous IDs for you. But for our server and other libraries, you might use a session id. If you don’t have any readily available identifier, you can always generate new UUIDs. Because our JavaScript library automatically generates anonymous IDs, you can send a request without the identifier and our library automatically inserts it. Here’s an example of a JavaScript identify call for an anonymous user: cioanalytics.identify({ subscriptionStatus: 'inactive' }); Customer.io Journeys doesn’t support anonymous identify calls yet While many integrations support anonymous identify calls, Customer.io does not support anonymous identify calls yet. Journeys will ignore anonymous identify calls because anonymous activity in Journeys is based on events. This doesn’t mean you shouldn’t send anonymous identify calls, even if you only connect to Journeys though. For example, if you use our JavaScript client, anonymous identify calls store traits in local storage and attach them to subsequent identify calls. This means that when you formally identify a person later, you don’t need to capture a bunch of traits at the same time; the JavaScript client library will automatically associate their traits with their userId. Traits Traits are things you know about your audience, like their email addresses, names, the plan they’re on, and so on. You’ll include traits in identify calls. We reserve some traits that have semantic meanings for users because we handle them in special ways. For example, we always expect email to be a string of the user’s email address. We’ll send this on to outbound integrations that require an email address for their tracking. You should only use reserved traits for their intended meaning. Trait Type Description address Object Street address of a user optionally containing: city, country, postalCode, state, or street age Number Age of a user avatar String URL to an avatar image for the user birthday Date User’s birthday company Object Company the user represents, optionally containing: name (String), id (String or Number), industry (String), employee_count (Number) or plan (String) createdAt Date Date the user’s account was first created. description String Description of the user email String Email address of a user firstName String First name of a user gender String Gender of a user id String Unique ID in your database for a user lastName String Last name of a user name String Full name of a user. If you only pass a first and last name we automatically fill in the full name for you. phone String Phone number of a user title String Title of a user, usually related to their position at a specific company. Example: “VP of Engineering” username String User’s username. This should be unique to each user, like the usernames of Twitter or GitHub. website String Website of a user Some integrations might represent these traits with slightly different names. For example, Mixpanel recognizes a $created trait when a user is first created, while Intercom recognizes the same trait as created_at. We attempt to handle all the integration-specific conversions for you automatically. You can pass reserved traits using camelCase or snake_case. For example, in JavaScript you can match the rest of your camel-case code by sending firstName, while in Python you can match your snake-case code by sending first_name. That way the API never seems alien to your code base. However, you should keep in mind that some destinations might not support all reserved traits; if an integration doesn’t support a reserved trait and you send it in camelCase and snake_case from different integrations, you may see both versions of your trait in the service you integrate with. --- ## Group URL: https://docs.customer.io/integrations/data-in/source-spec/group-spec/ The group method is how you'll associate a person with a group, like the company they work for or the school they attend. It also lets you record custom traits about the group, like industry or number of employees.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. How it works The group method is how you’ll associate a person with a group, like the company they work for or the school they attend. It’s a bit like an identify call, but for an organization instead of a person. When you send a group call, you’ll include a groupId with an optional userId. Sending a userId associates the user with the group. A user can belong to multiple groups, so you can send group calls associating the same person with different groups. When you send a group call, you can also include traits about the group. For example, if you use groups to represent companies, you might include the company’s industry, the number of employees, or the company’s billing date. You can send a group call without a userId if you simply want to update the traits for the group.  In Journeys, we typically refer to groups as objects If you use Customer.io as a destination, you’ll see your groups under Custom Objects by default, though you can rename your object type. A typical call A typical group call contains a groupId, userId, and traits.objectTypeId. The groupId is the identifier for the group. Often times this is a UUID or ULID, but it could be any string. Thetraits.objectTypeId represents the type of object in Customer.io. Customer.io supports different kinds of groups, called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., with a type ID beginning at 1. For example, if you create two different kinds of objects called Companies and Accounts, they’d likely have objectTypeId values of 1 and 2. If you don’t provide an objectTypeId, we’ll assume that the object type is 1. The userId represents the person you want to associate with the group. The traits object contains information about the group. If you use our client-side JavaScript library, you’ll typically only provide an objectTypeId, groupId, and traits. The library will automatically take care of the userId (or anonymousId, if you haven’t identified a person yet) for you, associating the current user with the group. cioanalytics.group("0e8c78ea9d97a7b8185e8632", { objectTypeId: 2, name: "Acme", industry: "Technology", wile_e_coyote_accidents: 329, plan: "enterprise", total_billed: 830 }); If you use one of our server-side libraries, and you want to relate a person to a group, you’ll have to provide the userId or anonymousId yourself. Here’s an example from our NodeJS server-side library: cioanalytics.group({ userId: '019mr8mf4r', groupId: '0e8c78ea9d97a7b8185e8632', traits: { objectTypeId: 2, name: "Acme", industry: "Technology", wile_e_coyote_accidents: 329, plan: "enterprise", total_billed: 830 } }); While this is the general shape of your call, our libraries actually capture much more information. See the full payload below. The full payload While your requests are typically short, our libraries capture much more information. This helps us provide context, not only for the group and the person you associate with the group, but the service you send the call from. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "userId": "97980cfea0067", "groupId": "0e8c78ea9d97a7b8185e8632,", "traits": { "objectTypeId": 2, "name": "Acme", "industry": "Technology", "wile_e_coyote_accidents": 329, "plan": "enterprise,", "total_billed": 830 }, "integrations": { "All": true }, "messageId": "022bb90c-bbac-11e4-8dfc-aa07a5b093db,", "timestamp": "2015-02-23T22:28:55.111Z,", "context": { "active": true, "ip": "8.8.8.8", "locale": "string", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string" }, "page": { "name": "string", "path": "string", "referrer": "string", "search": "string", "title": "string", "url": "string", "keywords": [ "string" ] } }, "anonymousId": "507f191e810c19729de860ea,", "channel": "browser,", "receivedAt": "2015-02-23T22:28:55.387Z,", "sentAt": "2015-02-23T22:28:55.111Z,", "type": "group", "version": 1.1 } groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:group version number The version of the API that received the event, automatically set by Customer.io. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:group version number The version of the API that received the event, automatically set by Customer.io. Group ID A Group ID is a unique identifier, like a UUID or ULID, that represents the group in your database or backend. Traits Like people in Customer.io, groups can have traits. Group traits are things you know about a group that you pass along in any group call, like industry, employees, or website. We’ve reserved the following traits. We handle these traits in special ways across data-out integrations. You should only use reserved traits for their intended meanings. Trait Type Description address Object Street address of a group. This should be a dictionary containing optional city, country, postalCode, state, or street avatar String URL to an avatar image for the group createdAt Date Date the group’s account was first created description String Description of the group, like their personal bio email String Email address of group employees String Number of employees of a group, typically used for companies id String Unique ID in your database for a group industry String Industry a user works in, or the group a person belongs to name String Name of a group phone String Phone number of a group website String Website of a group plan String Plan that a group is in relationshipAttributes Object Attributes that describe the relationship between a person and an object in Customer.io Journeys. See Relationship Attributes for more information. objectTypeId Integer The type of object that the group represents. This is a required trait for objects in Customer.io Journeys. If you don’t include this value, we’ll assume it’s 1 (the first kind of object you create). See Object Type for more information. objectId String The ID of a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io Journeys. Some data-out integrations might represent these traits with slightly different names. For example, Mixpanel recognizes a $created trait when a user is first created, while Intercom recognizes the same trait as created_at. We attempt to handle all these conversions for you automatically. Traits are case-insensitive, so in JavaScript you can match the rest of your camel-case code by sending createdAt, and in Ruby you can match your snake-case code by sending created_at. That way the API never seems alien to your code base. You can pass reserved traits using camelCase or snake_case. For example, in JavaScript you can match the rest of your camel-case code by sending createdAt, while in Python you can match your snake-case code by sending created_at. That way the API never seems alien to your code base. Groups are called objects in Customer.io In Customer.io Journeys, we refer to groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. The principle is the same: objects are non-people entities that people are related to, like accounts, companies, and online classes. But there are a few differences: You can have different kinds of objects! In Customer.io, we differentiate between objects using the traits.objectTypeId key. You can set traits for the object itself, and for the relationship between the person and the object. Items under traits are for the object, while items under traits.relationshipAttributes are for the relationship between the person and the object. Object type You can have different kinds of objects in Customer.io. For example, you might have objects for online classes, schools, client companies, and so on. When you create a new object type, we’ll assign it an objectTypeId. This ID is a number that represents the kind of object you’re creating. Your first object is objectTypeId 1, your second object is objectTypeId 2, and so on. In calls representing objects, you’ll send the object type to tell us what kind of object you want to create, update, or delete. For group calls, object type is represented by traits.objectTypeId. For track calls, object type is represented by properties.objectTypeId. If you don’t provide traits.objectTypeId in a group call, we’ll assume that the object type is 1. Object and relationship attributes When you send a group call, you can set traits for the object itself, and for the relationship between the person and the object. Items in the traits object are for the object, while items in the traits.relationshipAttributes object are for the relationship between the person and the object. For example, if you have a group representing a company, you might set the company’s name, industry, and employees in the traits object. You might also set the person’s role in the company, the dateJoined, and the isPrimaryContact in the traits.relationshipAttributes object. { "userId": "wileecoyote", "groupId": "0e8c78ea", "traits": { // determines the kind of object you're affecting "objectTypeId": 2, // these traits apply to the object "name": "Acme", "industry": "Technology", "employees": 329, // everything under relationshipAttributes applies to the // relationship between the person and the object "relationshipAttributes": { "role": "sponsor", "dateJoined": "2021-01-01", "isPrimaryContact": true } } } Deleting objects and relationships You’ll use group calls to create and update objects and relationships. But what if you want to delete an object or a relationship? We use special events to delete objects and relationships in Customer.io. These events are called semantic events. They let you send track calls with a specific event name to perform actions in Journeys. For objects, that means you’ll send Delete Object and Delete Relationship to remove objects and relationships from Customer.io respectively. Track calls are formatted differently from group calls. In this case, you’ll provide the userId at the top of the call, and your objectId (the groupId from your group calls!) in the properties object. { "userId": "wileecoyote", "event": "Delete Object", "properties": { "objectId": "0e8c78ea", "objectTypeId": 2 } } --- ## Page URL: https://docs.customer.io/integrations/data-in/source-spec/page-spec/ The page method represents pageviews: it's a method you'll invoke when people visit pages on your website. How it works The page method represents pageviews. It helps you record the pages that people visit on your website. By recording page events, you can better understand the parts of your website that people use. You might even follow up with people who’ve visited particular pages to see if they’re still interested in a product, online class, and so on. Our JavaScript client automatically captures page events on load, but you’ll need to invoke page calls manually if you use our server-side libraries, you have a single page app, or you want to augment the call with special properties. You’ll also send your own page calls if you use one of our server-side libraries.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. A typical call The page method changes significantly depending on whether you use our JavaScript client or send page calls another way. If you use the JavaScript client, we’ll automatically capture page events on page load with the page URL and other common properties. You can invoke page calls manually if you have a single page app or want to override properties that we typically capture. By default, the call is simply cioanalytics.page(). You can also send the page category, name, and so on. But, in general, page calls are relatively straightforward and, more importantly, automatic. If you use one of our server-side libraries (NodeJS, Python, and Go), you’ll need to send your own page calls. You’ll need to include a userId or anonymousId in your call, and you can also include a properties object with information about the page—including a number of reserved properties that we’ve enumerated below. Below is an example from our NodeJS library, but you should check the documentation for the SDK you install for more information about the page method. cioanalytics.page({ userId: '019mr8mf4r', category: 'Docs', name: 'Customer.io Pipelines', properties: { url: 'https://customer.io/cdp/', path: '/cdp/', title: 'Pipelines', referrer: 'https://customer.io' } }); The full payload While your requests are typically short, our libraries capture much more information. This helps us provide context, not only for the person performing the event, but the source of the call. The example below shows the full payload as you’ll see it in Customer.io. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "userId": "97980cfea0067", "type": "page", "name": "Home", "properties": { "category": "string", "url": "https://www.example.com/page", "title": "Welcome | ACME, Inc.", "referrer": "http://www.google.com/search/?q=sfgiants", "path": "/page", "search": "?q=sfgiants" }, "integrations": { "All": true }, "messageId": "022bb90c-bbac-11e4-8dfc-aa07a5b093db", "receivedAt": "2015-02-23T22:28:55.387Z", "sentAt": "2015-02-23T22:28:55.111Z", "timestamp": "2015-02-23T22:28:55.111Z", "version": 1.1, "context": { "active": true, "ip": "8.8.8.8", "locale": "string", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML like Gecko) Chrome/40.0.2214.115 Safari/537.36", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string" }, "page": { "name": "string", "path": "string", "referrer": "string", "search": "string", "title": "string", "url": "string", "keywords": [ "string" ] } }, "anonymousId": "507f191e810c19729de860ea", "channel": "browser" } integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:page version number The version of the API that received the event, automatically set by Customer.io. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:page version number The version of the API that received the event, automatically set by Customer.io. Page properties In page calls, you can send properties that describe the page. We have a number of reserved properties that we’ve defined below. For example, we always expect path to be a page’s URL path, and referrer to be the previous page’s URL. As with track calls you can also send custom properties beyond the reserved properties described below. You should only use reserved properties for their intended meaning.  Our JavaScript library automatically captures most properties Our analytics.js library automatically captures the title, path, url, referrer, and search properties in page calls. keywords array of [ strings ] A list/array of keywords describing the page’s content. The keywords are likely the same as, or similar to, the keywords you would find in an HTML meta tag for SEO purposes. This property is mainly used by content publishers that rely heavily on pageview tracking. This isn’t automatically collected. name string The name of the page. Reserved for future use. path string The path portion of the page’s URL. Equivalent to the canonical path which defaults to location.pathname from the DOM API. referrer string The previous page’s full URL. Equivalent to document.referrer from the DOM API. search string The query string portion of the page’s URL. Equivalent to location.search from the DOM API. title string The page’s title. Equivalent to document.title from the DOM API. url string A page’s full URL. We first look for the canonical URL. If the canonical URL is not provided, we’ll use location.href from the DOM API. --- ## Screen URL: https://docs.customer.io/integrations/data-in/source-spec/screen-spec/ The screen method represents screen views: it's a method you'll invoke when people view screens in your mobile app. How it works The screen method represents screenviews in your mobile apps—like a page call, but specifically for apps. It helps you record the screens that people visit as they use your app. By recording screen events, you can better understand the parts of your app that people use. You might even follow up with people who’ve visited particular screens to see if they’re still interested in a product, need help with something, and so on.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. A typical call A screen call is relatively straightforward. It contains a userId and a name for the screen. The userId is a unique identifier for the person viewing the screen. The name is the screen the person viewed. You can also send properties with any screen event. These are just additional details about the screen that you want to know about in your downstream destinations. For example, if you have a screen that shows a list of items, you might send the number of items in the list as a property. { "type": "screen", "userId": "97980cfea0067", "name": "Activity Feed", "properties": { "Feed Type": "public" } } The full payload While your requests are typically short, our libraries capture much more information. This helps us provide context, not only for the person performing the event, but the source of the call. The example below shows the full payload as you’ll see it in Customer.io. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "userId": "97980cfea0067", "name": "feed", "properties": { "feed_count": 45, "unseen_count": 10, "subscriptions": [ "baseball", "basketball" ] }, "integrations": { "All": true }, "messageId": "022bb90c-bbac-11e4-8dfc-aa07a5b093db", "timestamp": "2015-02-23T22:28:55.111Z", "context": { "active": true, "ip": "8.8.8.8", "locale": "string", "userAgent": "string", "channel": "browser", "app": { "name": "string", "version": "string", "build": "string", "namespace": "string" }, "device": { "id": "string", "advertisingId": "string", "manufacturer": "string", "model": "string", "name": "string", "type": "android", "version": "string" }, "network": { "bluetooth": true, "carrier": "string", "cellular": true, "wifi": true }, "os": { "name": "string", "version": "string" } }, "anonymousId": "507f191e810c19729de860ea", "receivedAt": "2015-02-23T22:28:55.387Z", "sentAt": "2015-02-23T22:28:55.111Z", "type": "screen", "version": 1.1 } messageId string A unique identifier for a Data Pipelines call, ensuring that each individual event is unique. This is set by Customer.io name string Required The name of the screen the person visited. properties object Additional properties for your screen. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string The event type. This is set automatically by the request method/endpoint.Accepted values:screen version number The version of the API that received the event, automatically set by Customer.io. messageId string A unique identifier for a Data Pipelines call, ensuring that each individual event is unique. This is set by Customer.io. name string Required The name of the screen the person visited. properties object Additional properties for your screen. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string The event type. This is set automatically by the request method/endpoint.Accepted values:screen version number The version of the API that received the event, automatically set by Customer.io. --- ## Track URL: https://docs.customer.io/integrations/data-in/source-spec/track-spec/ The track method is how you'll record actions your users perform. How it works The track method helps you record events: the things your users do on your site, in your app, and so on. Each track call records a single event. Each event has a name and properties. For example, if you send a track call when someone starts a video on your website, the name of the event might be Video Started, and the properties might include the title of the video, the length of the video, and so on.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. A typical call A typical track call is relatively straightforward. It contains an event name and a properties object. The name is the name of the event you want to track, and the properties are custom values providing extra information about the event. The call below is based on our JavaScript library, and doesn’t contain the userId or anonymousId of the person performing the event because our JavaScript library automatically captures that information for you. If you use our HTTP API or server libraries, you’ll need to include an identifier for the person performing the event in your request. While this is the general shape of your call, our libraries actually capture much more information. See the full payload below. You can send additional objects in your call, overriding the information that our SDKs and libraries typically capture, but you’ll need to find documentation for your specific library for details. cioanalytics.track("Course Started", { course_in_series: 1, course_format: "pass/fail", title: "Intro to Customer.io" }); The full payload Our libraries capture much more information than you send with each call. This helps us provide context, not only for the person performing the event, but the source of the call. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "userId": "AiUGstSDIg", "type": "track", "event": "Course Started", "properties": { "course_in_series": 1, "course_format": "pass/fail", "title": "Intro to Customer.io" }, "integrations": null, "messageId": "ajs-f8ca1e4de5024d9430b3928bd8ac6b96", "receivedAt": "2015-12-12T19:11:01.266Z", "sentAt": "2015-12-12T19:11:01.169Z", "timestamp": "2015-12-12T19:11:01.249Z", "version": 0, "context": { "active": true, "ip": "108.0.78.21", "locale": "string", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML like Gecko) Chrome/46.0.2490.86 Safari/537.36", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string", }, "page": { "name": "string", "path": "/", "referrer": null, "search": null, "title": "Customer.io Docs", "url": "https://customer.io", "keywords": [ "string" ] }, "library": { "name": "analytics.js", "version": "2.11.1" } }, "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", "originalTimestamp": "2015-12-12T19:11:01.152Z" } event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:track version number The version of the API that received the event, automatically set by Customer.io. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean messageId string A unique identifier for a Data Pipelines event, ensuring that each individual event is unique. properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. receivedAt string  (date-time) The ISO-8601 timestamp when Data Pipelines receives an event. sentAt string  (date-time) The ISO-8601 timestamp when a library sends an event to Data Pipelines. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. type string Required The event type. This is set automatically by the request method/endpoint.Accepted values:track version number The version of the API that received the event, automatically set by Customer.io. Identifying the person who performed the event If you use our JavaScript library, track calls automatically use the userId of the identified person or the anonymousId if you haven’t identified a user yet. If you use our HTTP API or server libraries, you’ll need to include an identifier for the person performing the event in your request. You can use either a userId or an anonymousId. See the specific library documentation for more information about identifying people in requests. See the Identify page for more information about User IDs and Anonymous IDs. Event names Every track call records a single event—something a person did on your website, in your app, and so on. The event must have a name. We recommend that you use human-readable names for events, so that everyone on your team knows what each event represents. Don’t use nondescript names like Event 12 or TMDropd. Instead, use unique but recognizable names like Video Started and Order Completed. Deduplicate events By default, we automatically assign a messageId to each call you make to Customer.io. But, you can set your own messageId if you need to deduplicate calls to Customer.io, ensuring that you don’t bog down your workspace with unnecessary traffic or trigger unnecessary downstream actions. We’ll accept the first instance of any operation with a given messageId and ignore any operations with the same messageId for the next 12 hours. The messageId is can be any string value, but we recommend a hash of the event data or a UUID/ULID to ensure that you don’t inadvertently deduplicate events. If you backdate events, you’ll need to deduplicate them before you send them to Customer.io. We deduplicate the messageId for 12 hours after we receive the operation—not the timestamp on the event itself. Properties Properties are information you want to record for the events you track. They can be anything that will be useful while analyzing the events later. We recommend that you send properties whenever possible because they give you a more complete picture of what your users do. We’ve reserved some properties with semantic meanings in Customer.io, and handle them in special ways. For example, we always expect revenue to be a currency value that we send to your downstream integrations that handle revenue tracking. You should only use the following reserved properties for their intended meaning. Property Type Description revenue Number Amount of revenue an event resulted in. This should be a decimal value, so a shirt worth $19.99 would result in a revenue of 19.99. currency String Currency of the revenue an event resulted in. This should be sent in the ISO 4127 format. If this isn’t set, we assume the revenue to be in US dollars. value Number An abstract “value” to associate with an event. This is typically used in situations where the event doesn’t generate real-dollar revenue, but has an intrinsic value to a marketing team, like newsletter signups. Different data-out integrations may use these special properties differently. For example, Mixpanel has a track_charges method for accepting revenue. Luckily, you don’t have to worry about those inconsistencies. Just pass along the reserved property (in this example,revenue) and Customer.io will handle these conversions for you automatically. --- ## Alias URL: https://docs.customer.io/integrations/data-in/source-spec/alias-spec/ The alias method helps you merge two user profiles. It's typically useful when you need to merge a person's anonymous activity with their identified profile.  See our API documentation for code samples This page can help you better understand when and how to use this API method. But, if you already know how it works and are ready to get started, you can go straight to our API documentation and start writing code. How it works An alias call helps you merge two user profiles—a userId and a previousId. The userId inherits the traits and activity of the previousId, so you can track activity going forward against the canonical userId. Alias calls are typically useful in two situations: You send data out to a service that doesn’t automatically associate anonymous activity (under an anonymousId) with an identified person (a userId). A person generates multiple user IDs and you need to unify them under a single userId value. You should send the alias call immediately before the identify call for a person. flowchart LR subgraph z [Anonymous Activity] a(Person visits website)-->b(page views) end c(Person logs in)-->d(Alias anonymousId to userId)-->e(Identify user by userId) z-->f[(Identified User Activity)] e-->f z-->c What comes first: alias or identify? You should send the alias call immediately before the identify call for a person. This ensures that the anonymous activity is associated with the identified user before the identified user starts generating new activity. Associating anonymous activity with a user ID Most of the services we integrate with automatically associate anonymous activity with a userId when you identify a person, so you won’t need to implement Alias calls. You should send the alias call immediately before the identify call for a person. This ensures that the anonymous activity is associated with the identified user before the identified user starts generating new activity.  Mixpanel users: check your Identity Merge settings Before March 2023, Mixpanel required you to use the alias method to merge anonymous data with an identified person. Now Mixpanel has a Simplified API for identity merging, so it’ll automatically merge anonymous and identified data! If you created your Mixpanel account before April 2024, you’ll need to enable the Simplified API to support identity merging without alias calls. A typical call A typical identify call is straightforward. It contains a userId and a previousId object. The userId is a person’s canonical identifier and the previousId is the identifier you want to remove or ignore in your downstream integrations. cioanalytics.alias("userId","previousId"); The full payload While your requests are typically short, our libraries capture much more information. This helps us provide context, not only for the person you aliased, but the service you aliased them from. Customer.io and our libraries typically populate integrations and timestamp values as shown in the payload below. If you use our JavaScript or mobile libraries, they’ll also populate context for you. If you use our server-side libraries, you’ll need to populate context yourself. See common fields for more information about context, integrations, and timestamps in source payloads. { "type": "identify", "userId": "97980cfea0067", "previousId": "97980cfea0067", "integrations": { "All": true }, "messageId": "string", "receivedAt": "2019-08-24T14:15:22Z", "sentAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", "version": 0, "context": { "active": true, "ip": "string", "locale": "string", "userAgent": "string", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string" } } } previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## Common fields URL: https://docs.customer.io/integrations/data-in/source-spec/common-fields/ All calls are structured the same way. While you'll only send _some_ information in your calls, our libraries and API capture *implicit* information about the source of the call, the person the call represents, and timing. How it works When you send a call to Customer.io, we capture additional information from our libraries and information implicit in each call, like: context for the source of your calls the ip and locale of the person the call represents timestamps that help you understand the chronology of your data The added context these common fields provide can help you debug issues with your data or add special handling in actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. for downstream applications. A full payload Imagine you send the track call (using our JavaScript client library) below. The call itself simply contains the event name and a properties object: cioanalytics.track("Report Submitted", { reportType: "sales", reportFormat: "csv" }) But, when you look at your call in Customer.io, you’ll see the complete payload below. Our libraries and API automatically capture the context, integrations, messageId, receivedAt, sentAt, version, type (which is implicit in the method/endpoint you call), and timestamp properties. { "userId": "AiUGstSDIg", "event": "Report Submitted", "properties": { "reportType": "sales", "reportFormat": "csv" }, "integrations": { "All": true }, "messageId": "ajs-f8ca1e4de5024d9430b3928bd8ac6b96", "timestamp": "2015-12-12T19:11:01.249Z", "context": { "active": true, "ip": "108.0.78.21", "locale": "string", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML like Gecko) Chrome/46.0.2490.86 Safari/537.36", "channel": "browser", "campaign": { "name": "string", "source": "string", "medium": "string", "term": "string", "content": "string", }, "page": { "name": "string", "path": "/", "referrer": null, "search": null, "title": "Customer.io Docs", "url": "https://customer.io", "keywords": [ "report" ] }, "library": { "name": "analytics.js", "version": "2.11.1" } }, "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", "receivedAt": "2015-12-12T19:11:01.266Z", "sentAt": "2015-12-12T19:11:01.169Z", "type": "track", "originalTimestamp": "2015-12-12T19:11:01.152Z" } The context object The context object contains useful information about the origin of a call, like the page the call originates from, or the ip address of the user the call represents. You’ll notice slightly different context depending on whether you use a mobile SDK or another SDK. Our mobile libraries capture information about a device rather than a page. If you use our server libraries, you’ll need to set this information yourself. Make sure that you use these properties for their originally-intended purpose; setting values that don’t make sense for the property can result in unexpected behaviors in downstream applications that rely on context. Automatically-collected fields Our JavaScript, mobile, and server libraries capture the information below. For now, our mobile libraries collect very few fields but we’re working on changes that will increase the context they capture. Our server-side libraries generally require you to send context and other common fields yourself. They’ll set the context.library property, but you should check with the appropriate documentation for the server library you use for help adding context to your calls. Context Field JavaScript (Web) Mobile (iOS/Android) Server app.name ✅ app.version ✅ app.build ✅ campaign.name ✅ campaign.source ✅ campaign.medium ✅ campaign.term ✅ campaign.content ✅ device.type ✅ device.id ✅ device.advertisingId ✅ device.adTrackingEnabled ✅ device.manufacturer ✅ device.model ✅ device.name ✅ journeys.identifiers ✅ library.name ✅ ✅ ✅ library.version ✅ ✅ ✅ ip1 ✅ ✅ locale ✅ ✅ network.bluetooth network.carrier ✅ network.cellular ✅ network.wifi ✅ os.name ✅ os.version ✅ page.path ✅ page.referrer ✅ page.search ✅ page.title ✅ page.url ✅ screen.height ✅ screen.width ✅ traits ✅ userAgent ✅ ✅ userAgentData2 ✅ timezone ✅ 1Our libraries don’t capture IP address. Our servers fill in this information when we receive calls from our JavaScript or mobile libraries. 2We only collect userAgentData when the Client Hints API is available in the browser. The integrations object When you connect a data source to an integration, we send all the data for which we have actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to that integrated service. But you can filter calls you make to Customer.io directly from your data source to a specific subset of downstream integrations using the integrations object. In general, we recommend that you filter calls for each data-out integration using actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination., because they’re easier to change and manage than your source code. But you might want to modify the integrations object if: You have an integration that bills you for incoming API calls and you want to limit traffic to that service. if you know that information in the individual call is only useful in specific downstream integrations. To limit a call to specific integrations, you need to set "All": false and set true for the integrations you want to send to. Within the integrations object, you need to use the integration name exactly as we show it at the top of every data-out integration in our integration directory. For example, if you simply used Mixpanel below, we may not recognize the integration and send the call to your Mixpanel destination. { "integrations": { "All": false, "Actions Customerio": true, "Google Analytics 4 Cloud": true, "Mixpanel (Actions)": true } } messageId: deduplicate calls When you send a call to Customer.io, we generate a messageId for the call. This messageId is a value that uniquely identifies a call to Customer.io. If you think you might send duplicate calls or events to Customer.io, you can generate your own messageId string and send it as a part of your calls. We’ll accept the first instance of any operation with a given messageId and ignore any operations with the same messageId for the next 12 hours. The messageId is can be any string value, but we recommend a hash of the event data or a UUID/ULID to ensure that you don’t inadvertently deduplicate events. If you backdate events, you’ll need to deduplicate them before you send them to Customer.io. We deduplicate the messageId for 12 hours after we receive the operation—not the timestamp on the event itself. Timestamps Every call includes ISO-8601 timestamps, originalTimestamp, timestamp, sentAt, and receivedAt, each with different purposes. When analyzing your data, we recommend that you use receivedAt when chronology doesn’t matter and timestamp when it does. This is because receivedAt is typically correct but not guaranteed to be in chronological order, but the formula we apply to timestamp ensures that it is always in chronological order. Timestamp Calculated Description originalTimestamp The date-time on the client device when you invoked a call or a value that you pass manually from a server-side library. Used to calculate timestamp. sentAt The date-time on client device when you invoked a call or a value that you set manually in your call. Used to calculate timestamp. receivedAt The date-time when Customer.io received a call. Used to calculate timestamp and used as sort key in data warehouse and database integrations. timestamp Calculated by Customer.io to correct client-device clock skew using the formula: receivedAt - (sentAt - originalTimestamp). Used to send data to downstream integrationss and in data replays. Reserved traits and properties in Customer.io Some traits and properties in semantic events have special meanings within Customer.io. You should only use these fields for their intended purposes when you send data into Customer.io. Trait/Property Purpose Set by Customer.io id A synonym for userId acting as a unique identifier for people. If your userId isn’t an email address, we automatically set the userId value as a person’s id. email A person’s email address. In most cases, we use email as another way to identify people. If you pass an email address as a userId, we’ll automatically set it as a person’s email trait. cio_id A unique, immutable identifier set by Customer.io, set automatically when you add a person. You cannot set this value yourself. ✅ created_at We recommend that you set this value when you identify someone for the first time so you can take advantage of timestamp operators in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions.. It also helps you determine the real age of a person’s profile. _created_in_customerio_at The date-time when Customer.io recognizes a new person. This value can be different from created_at, especially if you backdate people you identify. ✅ unsubscribed Determines whether a person is subscribed or unsubscribed from email, SMS, and push messages. cio_object_id A unique, immutable identifier for objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. set automatically by Customer.io. ✅ object_id A unique identifier for objects (like accounts, companies, etc). objectId An analog for object_id in some Customer.io integrations. relationshipTraits Used to reference relationships to objects. relationship Used to reference relationships to objects. _relationship Used in relationship-triggered campaigns to reference audience members who did not trigger the campaign. Cannot be used as the name of a trait. --- ## Custom events URL: https://docs.customer.io/integrations/data-in/custom-events/ The most flexible method is `track`. It lets you send custom events representing your users' activities on your site. This page contains a few examples to help you capture the information you really want from your data sources. How it works Most of our API methods have a defined purpose: the identify call identifies users; page calls track pageviews. But the track call lets you send custom events, so that you can track the activities that are meaningful to you. Track calls take a name representing the name of the event you want to track and a set of custom properties. For example, when someone adds an item to their cart, you might send an added_to_cart event with properties representing the item that your customer added to their cart. If you run an online class and you want to track progress, you might send a call when people begin a new module in the course or watch a video. The names of your events might be module_started and video_started; and the properties would contain information about the module that a person began or the video they watched respectively.  Examples on this page are based on our JavaScript snippet The same principles apply to all our server-side integrations, but the examples will look slightly different if you’re using Go, Python, or Node.js. Events can be anonymous You can send track calls before or after you identify someone—when they sign up, login, provide their email, etc; the order of calls doesn’t matter. Most outbound integrations automatically resolve anonymous activity to the “identified” user after you send an identify call. flowchart LR a(person visits site)-->|generate anonymousId|z subgraph z [anonymous activity] direction LR b(visit a page) b-->|page|c(add item to cart) c-->|track|d(person logs in) end subgraph y [identified activity] direction LR e(person initiates checkout)-->|track|f(person finishes transaction) end z-->|anonymous activity associated with identified person|y The power of properties Track calls take a custom dictionary of properties. Properties are simply nested JSON and can contain any data you want to pass along to your outgoing integrations. This includes nested objects, arrays, and arrays of objects. When you set up events, you should send all the data that you think might be useful—even if you won’t use this data in Customer.io or all of your outgoing integrations. You can always ignore data at various destinations, but you can’t send data out of the system if you don’t send it to Customer.io first! Here’s the complete, JSON representation of an event. See the properties object? That’s information you can use downstream. { "userId": "AiUGstSDIg", "type": "track", "event": "Course Started", "properties": { "course_name": "Intro to Customer.io", "courseId": "cdp101", "progress": 0, "modules": [ { "moduleId": "cdp101-1", "moduleName": "track calls", "started": false }, { "moduleId": "cdp101-2", "moduleName": "identifying people" "started": true } ] }, "integrations": null, "messageId": "ajs-f8ca1e4de5024d9430b3928bd8ac6b96", "receivedAt": "2015-12-12T19:11:01.266Z", "sentAt": "2015-12-12T19:11:01.169Z", "timestamp": "2015-12-12T19:11:01.249Z", "version": 0, "context": { ... }, "page": { "name": "string", "path": "/", "referrer": null, "search": null, "title": "Customer.io Docs", "url": "https://customer.io", "keywords": [ "string" ] }, "library": { "name": "analytics.js", "version": "2.11.1" } }, "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", "originalTimestamp": "2015-12-12T19:11:01.152Z" } Example use cases The following use cases are examples of the things you might send with track calls. You can copy these examples if they fit your use case, but you’ll get more out of your integration if you tailor calls to fit your needs! Logging in and logging out When someone logs in, or creates an account, you’ll send an identify call. But in these cases, you’ll also send a track call. Track calls on login and log out can help you track the length of sessions in your app, understand the times when your users are most active, and other information independent of identify calls! Your log in and log out track events will look something like this: login login cioanalytics.track("login", { username: "cool.person" session_started: 1683250025 platform: "chrome" login_page: "https://fly.customer.io/pipelines/my-pipelines" }); logout logout cioanalytics.track("logout", { username: "cool.person" session_ended: 1683361625 logout_from: "session_expred" }); Added to cart If you run an online store, you probably want to keep track of the products that your audience is interested in, and the items they add to their cart for things like cart abandonment campaigns and to help you anticipate changes in your inventory.  Format your events using our ecommerce specification We’ve created default actions for our data-out integrations that take ecommerce events. If you format your events according to our ecommerce specification, you’ll be able to take advantage of these defaults. In many cases you won’t even have to map your own events and properties! So, when someone clicks Add to cart, you might send a call that looks like this: cioanalytics.track("Product Added", { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" cart_id: "usercart-789" });  Some integrations have purchase event actions Data-out integrations that have dedicated events typically filter on the event name. You’ll want to make sure that you send the right event name and properties that the integration expects. See the docs for your specific integration to make sure you’ve got the right information in your track calls! Media started/ended If you run a media operation, you’re in education technology, or you deliver webinars, you might want to know when your audience plays videos (so you can track progress) and online lectures. These kinds of things help you better understand when people use your videos, whether or not they finish them, and other engagement needs—like play speed. You might attach track calls to the buttons in a video player to capture play, pause/stop, and other things about media. cioanalytics.track("video_watched", { video_name: "Cool New Video" length: 1.56 speed: 1.25 finished: false }); Semantic events Many of our data-out integrations’ default actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. are based on the name of the event you send. With these semantic events, we expect a certain set of properties that we use to map to multiple integrations. While you can send any properties and events that you want, taking advantage of our pre-defined semantic events can save you time and effort, helping you take advantage of our default actions—so you don’t have to manually map data to these integrations. We have a set of standard, semantic events for the following purposes: Mobile A/B Testing Ecommerce Email Live Chat Video Ecommerce events Ecommerce track events typically contain the same information across platforms—products, brands, orders, cart information, etc. However, each integration might map this information differently. We created an ecommerce specification to help you produce uniform data, making it easy to set up integrations. Our data-out integrations that take ecommerce events have default actions that expect data conforming to our ecommerce specification. When you send data using our ecommerce specification, it maps right to your integrations, without having to map your own data structures. You don’t have to use this spec. You’re welcome to send ecommerce events in any format you want. But if you send data that does not conform to our ecommerce specification, you’ll need to map actions yourself. cioanalytics.track('Product Viewed', { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" currency: "USD" value: 119.99 }) --- ## Understanding Semantic Events URL: https://docs.customer.io/integrations/data-in/semantic-events/getting-started/ While events can have any name and `properties` you want, we've predefined a number of events and sets of properties. These *semantic events* are already mapped to our data-out integrations, so you can get up and running quickly. Many of the actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. for outbound integrations are based on the name of the event you send. With these semantic events, we expect a certain set of properties that we use to map to multiple integration destinations. While you can send any properties and events that you want, taking advantage of our pre-defined semantic events can save you time and effort, helping you take advantage of our default actions—so it’s easier to set up data-out integrations. We have sets of standard semantic events for the following purposes: Customer.io Journeys Mobile A/B Testing Ecommerce Email Live Chat Video Semantic Properties Within these events, we also have semantic properties—properties that we expect to see in certain events. These properties are also mapped to our data-out integrations, so you can get up and running quickly. For example, the product_viewed event in our ecommerce specification typically contains product_id and brand properties, so you know which product people viewed and the brand of that product. --- ## A/B Test events URL: https://docs.customer.io/integrations/data-in/semantic-events/a-b-test/ A/B tests help you test changes variations to improve on your websites, apps, services, and so on. If you send A/B tests that conform to our specifications, they'll automatically trigger downstream actions so you can capture test results in any number of outbound integrations. How it works A/B tests help you determine how variations in your websites, apps, and services perform with your audience. We use this specification to support default actions—any we have now and ones we might add in the future. So, if you send A/B tests that conform to this specification, you’ll be ready to trigger downstream actions in outbound integrations that rely on an A/B testing framework. We refer to A/B tests as experiments. Each experiment can have multiple variants that you present to random samples of your audience. So, each track call that you send to Customer.io should contain the experiment_id and variant_id properties, so you can track the efficacy of variants in your tests. Each event can also contain an experiment_name and variant_name properties that help you understand your events at a glance—without having to look up what each ID represents. Experiment Viewed There’s only one event in our specification, called Experiment Viewed. You’ll send this event whenever you present someone with a variant in an A/B test. It helps you track which experiments you’re running and which variants a person sees. You can track the total number of variants within an experiment to see how many people have been involved in the experiment. A typical event contains the following properties: experiment_id string The ID of the A/B test. experiment_name string The A/B test’s human-readable name. variation_id string The ID of the individual test variant. variation_name string The A/B test variant’s human-readable name. Example payload An example payload for an Experiment Viewed event might look like this: { "type": "track", "userId": "97980cfea0067", "event": "Experiment Viewed", "properties": { "experiment_id": "12345", "experiment_name": "Homepage Hero", "variant_id": "67890", "variant_name": "Hero B" } } --- ## Customer.io events URL: https://docs.customer.io/integrations/data-in/semantic-events/cio-journeys/ Customer.io events are events that perform specific actions in Journeys—our messaging automation tool. These events can remove people from your environment, delete relationships, suppress users, and so on. How it works Your workspace supports a number of actions that aren’t immediately apparent from the API. For example, I can add a person using the identify function, but what if I want to remove a person? That’s what semantic events are for: they let you send track calls with a specific event name to perform actions in Customer.io. In general, the event names map directly to the thing you want to do: like Create Device or Delete Person. Create or Update Device The Device Created or Updated event creates a new device if device.token doesn’t exist and updates the device if it does. A “device” is a mobile device or browser that a person uses to interact with your app or website. You might send this event when someone logs into your app or website. These events require a device object. The device object must contain a token. cioanalytics.track("Device Created or Updated", { device: { token: "string", type: "ios" } }); context object Required Information about the device performing the event. device object Required Device information. token string Required The device token. type string The device type.Accepted values:ios,android * any type event string Required The event name.Accepted values:Device Created or Updated userId string Required The user’s unique identifier. Delete Device This event removes a device. You might send this event when someone logs out of your app. As with the Create Device and Update Device events, this event requires a device object. The device object must contain a token. cioanalytics.track("Device Deleted", { device: { token: "string", type: "ios" } }); context object Required Information about the device performing the event. Our SDKs typically collect these properties automatically. device object Required Device information. token string Required The device token. type string The device type.Accepted values:ios,android * any type event string Required The event name.Accepted values:Device Deleted userId string Required The user’s unique identifier. Delete Person This event removes a person from your Customer.io workspace. You might do this when someone cancels their subscription with you or otherwise leaves your service. cioanalytics.track("User Deleted); event string Required The event name.Accepted values:User Deleted properties object Properties for the event. These aren’t typically useful in Customer.io when deleting people, but you might use them in other places—like an analytics tool (e.g. Mixpanel). userId string Required The user’s unique identifier or their email address. If you provide an email address, we’ll look up the person by their email address and delete them. Delete Object This event removes a group (also called an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) from Customer.io Journeys. Groups/objects represent things like accounts, companies, and online classes—non-people entities that people can be related to. You might delete a group when an account is closed, a company is acquired, or you stop offering a class. This event requires an objectId and an objectTypeId. If you don’t provide an objectTypeId, we’ll assume it’s 1, but if the objectId with the associated objectTypeId (defaulting to 1) does not exist, the event will not find and delete the objectId. cioanalytics.track("Object Deleted", { objectId: "Acme", objectTypeId: 1 }) anonymousId string Required While you must set a value, it can be anything; we don’t use it. Customer.io requires events to be performed by people, but the “performer” is irrelevant to this event. event string Required The event name.Accepted values:Object Deleted properties object Required Properties for the event. In this case, you’ll need to provide the objectId. You should also provide the objectTypeId if you have more than 1 object type; if you don’t include it, we’ll assume it’s 1. objectId string This is the group/object you want to remove. objectTypeId integer Default: 1 The type of group/object the objectId represents. In Customer.io, each type of group/object has a an integer value, starting at 1 and incrementing. For example, if you have two types of groups/objects, you might have 1 represent accounts and 2 might represent companies. If you leave this value blank, we’ll assume it’s 1. Delete Relationship This event removes a relationship between a person and an object in Customer.io. This is basically the opposite of the group function in Customer.io Journeys. Groups (or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) represent things like accounts, companies, or online classes, you might delete a relationship when a person is no longer on an account, leaves a company, or drops a class. This event requires an objectId and an objectTypeId. If you don’t provide an objectTypeId, we’ll assume it’s 1, but if the objectId with the associated objectTypeId (defaulting to 1) does not exist, the event will not remove a person from the group. cioanalytics.track("Relationship Deleted", { objectId: "Acme", objectTypeId: 1 }) event string Required The event name.Accepted values:Relationship Deleted properties object Required Properties for the event. In this case, you’ll need to provide the objectId. You should also provide the objectTypeId if you have more than 1 object type; if you don’t include it, we’ll assume it’s 1. objectId string Required A person is related to a group/object. This is the group/object you want to remove the person from. objectTypeId integer Default: 1 The type of group/object the objectId represents. In Customer.io, each type of group/object has a an integer value, starting at 1 and incrementing. For example, if you have two types of groups/objects, you might have 1 represent accounts and 2 might represent companies. If you leave this value blank, we’ll assume it’s 1. userId string Required The user’s unique identifier or their email address. If you provide an email address, we’ll look up the person by their email address. Suppress Person Remove a person from your Customer.io workspace and prevent them from being added back to your workspace using the same identifier. In general you should only send this event for compliance reasons, like when someone invokes their right to be forgotten in accordance with GDPR or CAN-SPAM regulations. cioanalytics.track("Suppress Person"); event string Required The event name.Accepted values:User Suppressed properties object Properties for the event. These aren’t typically useful in Customer.io when suppressing people, but you might use them in other places—like an analytics tool (e.g. Mixpanel). * any type timestamp string  (date-time) The ISO-8601 timestamp when the event occurred. userId string Required The user’s unique identifier. Unsuppress Person Allow a userId to be added back to your Customer.io workspace. This does not restore information from a person you previously suppressed. It only allows you to add a person back to your workspace using the same identifier. You might do this if a person explicitly asks to be resubscribed to your messages after having been suppressed. cioanalytics.track("Unsuppress Person", { userId: "person-i-want-to-unsuppress" }); event string Required The event name.Accepted values:User Unsuppressed properties object Properties for the event. These aren’t typically useful in Customer.io when unsuppressing people, but you might use them in other places—like an analytics tool (e.g. Mixpanel). * any type timestamp string  (date-time) The ISO-8601 timestamp when the event occurred. userId string Required The user’s unique identifier. Report Delivery Event In general, we use this event with our JavaScript snippet to report delivery events for in-app messages back to your Customer.io workspace. While Customer.io knows about messages that are sent, this event reports back to Customer.io when an in-app message is delivered, clicked, and so on. Each message contains a unique deliveryId that we trace back to the person, campaign/broadcast, and other items in your Customer.io workspace. You can use this event to report delivery events for other message types, like push notifications. You might do this if you’re using a third-party push notification service and want to report delivery events back to Customer.io. cioanalytics.track("Report Delivery Event", { deliveryId: "delivery-id", metric: "clicked", recipient: "device-token", actionValue: "enable push", href: "myApp://settings/push" }); event string Required The event name.Accepted values:Report Delivery Event properties object Required Properties for the event. These aren’t typically useful in Customer.io when suppressing people, but you might use them in other places—like an analytics tool (e.g. Mixpanel). actionValue string For in-app messages that are clicked, this value represents the value of the action the recipient clicked. deliveryId string The ID of the message delivery. href string For in-app messages that are clicked, this value represents the URL/link the recipient clicked. metric string The metric you’re reporting. Remember, these are metrics that occur outside of Customer.io—after the message is sent. Because this event typically tracks in-app messages, you’ll generally see metrics like delivered, opened, clicked, etc.Accepted values:delivered,clicked,converted,opened reason string If the message failed for some reason, this field contains the reason why. recipient string The recipient of the message. This field changes depending on the type of message. For in-app messages, this is the person’s deviceId; for SMS it’s the recipient’s phone number; for push notifications, it’s their device token. * any type timestamp string  (date-time) The ISO-8601 timestamp when the event occurred. --- ## Ecommerce Events URL: https://docs.customer.io/integrations/data-in/semantic-events/ecommerce/ Ecommerce `track` events typically contain the same information across platforms—products, brands, orders, cart information, etc. Send events conforming to our ecommerce specification to use our out-of-the-box actions without having to map your own data to each outbound integration. How it works Ecommerce track events typically contain the same information across platforms—products, brands, orders, cart information, etc. However, each place you send this data to might map this information differently. Our ecommerce specification helps you send events in a uniform format that maps to the actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. for any of our ecommerce-supporting integrations. This means can use our out-of-the-box actions without having to map your incoming data to each outbound integration. Handling arrays of products Some data-out integrations don’t take arrays. In these cases, you may need to flatten arrays of products. See the documentation for your integration to learn more information about supported events and properties. Event lifecycles You can see a complete list of ecommerce events and all associated properties on our Pipelines API reference page. But it can also help to understand the typical events that a person may perform and the order they’re likely to perform them in, as they browse and buy products.  Examples in this section are based on our JavaScript library To simplify the examples on this page, we’ve based everything on our JavaScript client. But you can send ecommerce events from any of our libraries. Browsing products Browsing events represent key events that a customer might have while browsing for products. Action Description Products Searched User searched for products Product List Viewed User viewed a product list or category Product List Filtered User filtered a product list or category Products SearchedProduct List ViewedProduct List Filtered Products Searched cioanalytics.track('Products Searched', { query: "pepperoni pizza" }) query string The search query the customer entered. Product List Viewed cioanalytics.track('Product List Viewed', { list_id: 'hot_deals_1', category: 'Deals', products: [ { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" price: 139.99 position: 1 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" }, { product_id: "coolshoes-456" sku: "abc-456-xyz" category: "shoes" name: "Even Cooler Shoes" brand: "A Different Shoe Brand" price: 159.99 position: 2 url: "https://www.example.com/product/456" image_url: "https://www.example.com/product/456.jpg" } ] }); Product List Filtered cioanalytics.track('Product List Filtered', { list_id: "all_shoes", category: "shoes", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ], filters: [ { type: "string", value: "string" } ], sorts: [ { type: "string", value: "string" } ] }) Promotions overview Promotion events let you know when someone sees or interacts with offers within your app. For example, you might send a Promotion Viewed event when your app shows a banner advertisement to a user. If the user clicks the ad, you’d send the Promotion Clicked event. Action Description Promotion Viewed User viewed promotion Promotion Clicked User clicked on promotion Promotion ViewedPromotion Clicked Promotion Viewed cioanalytics.track('Promotion Viewed', { promotion_id: "promo-123", creative: "top_banner_2", name: "75% store-wide shoe sale", position: "banner_slot_1" }) Promotion Clicked cioanalytics.track('Promotion clicked', { promotion_id: "promo-123", creative: "top_banner_2", name: "75% store-wide shoe sale", position: "banner_slot_1" }) Product order overview These events represent the typical lifecycle of a product order. Action Description Product Clicked User clicked on a product Product Viewed User viewed a product details Product Added User added a product to their shopping cart Product Removed User removed a product from their shopping cart Cart Viewed User viewed their shopping cart Checkout Started User initiated the order process (a transaction is created) You should send this event on the page that the customer lands on after they click Checkout (or a similar button). Checkout Step Viewed User viewed a checkout step. You can have as many checkout steps as you want. Checkout Step Completed User completed a checkout step. You can have as many checkout steps as you want. Payment Info Entered User added payment information Order Completed User completed the order Order Updated User updated the order Order Refunded User refunded the order Order Cancelled User cancelled the order Product ClickedProduct ViewedProduct AddedProduct RemovedCart ViewedCheckout StartedCheckout Step ViewedCheckout Step CompletedPayment Info EnteredOrder CompletedOrder UpdatedOrder RefundedOrder Cancelled Product Clicked cioanalytics.track('Product Clicked', { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" currency: "USD" value: 119.99 }) Product Viewed cioanalytics.track('Product Viewed', { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" currency: "USD" value: 119.99 }) Product Added cioanalytics.track('Product Added', { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" currency: "USD" value: 119.99 }) Product Removed cioanalytics.track('Product Removed', { product_id: "coolshoes-123" sku: "abc-123-xyz" category: "shoes" name: "Cool Shoes" brand: "A Shoe Brand" variant: "red" price: 139.99 quantity: 1 coupon: "NEWUSER20" position: 3 url: "https://www.example.com/product/123" image_url: "https://www.example.com/product/123.jpg" currency: "USD" value: 119.99 }) Cart Viewed cioanalytics.track('Cart Viewed', { cart_id: "cool_persons_cart_123", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ] }) cart_id string The cart ID a person viewed. products array of [ objects ] The products in the cart. Each object in the array represents a product. Checkout Started cioanalytics.track('Checkout Started', { order_id: "cool_persons_cart_123", affiliation: "Shopify", revenue: 139.99, shipping: 5, tax: 10, discount: 20, coupon: "NEWUSER20", currency: "USD", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ] }) Checkout Step Viewed cioanalytics.track('Checkout Step Viewed', { checkout_id: "cool_persons_checkout_123", step: 2, shipping_method: "ground", payment_method: "Visa" }) checkout_id string The checkout/transaction ID. payment_method string The payment method selected by the user. shipping_method string The shipping method selected by the user. step integer The step number of the checkout process. Checkout Step Completed cioanalytics.track('Checkout Step Completed', { checkout_id: "cool_persons_checkout_123", step: 2, shipping_method: "ground", payment_method: "Visa" }) checkout_id string The checkout/transaction ID. payment_method string The payment method selected by the user. shipping_method string The shipping method selected by the user. step integer The step number of the checkout process. Payment Info Entered cioanalytics.track('Payment Info Entered', { checkout_id: "cool_persons_checkout_123", step: 2, shipping_method: "ground", payment_method: "Visa", order_id: "order123" }) Order Completed cioanalytics.track('Order Completed', { subtotal: 119.99, products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ], order_id: "cool_persons_cart_123", checkout_id: "checkout123", total: 0, affiliation: "Shopify", revenue: 139.99, shipping: 5, tax: 10, discount: 20, coupon: "NEWUSER20", currency: "USD" }) Order Updated cioanalytics.track('Order Updated', { order_id: "cool_persons_cart_123", affiliation: "Shopify", revenue: 139.99, shipping: 5, tax: 10, discount: 20, coupon: "NEWUSER20", currency: "USD", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ] }) Order Refunded cioanalytics.track('Order Refunded', { order_id: "order123", affiliation: "Shopify", revenue: 139.99, shipping: 5, tax: 10, discount: 20, coupon: "NEWUSER20", currency: "USD", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ], total: 119.99, subtotal: 119.99, checkout_id: "checkout123" }) Order Cancelled cioanalytics.track('Order Cancelled', { order_id: "order123", affiliation: "Shopify", revenue: 139.99, shipping: 5, tax: 10, discount: 20, coupon: "NEWUSER20", currency: "USD", products: [ { product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" } ], total: 119.99 }) Coupons overview Send coupon events when your customers enter, apply, or remove coupons from their shopping carts or orders. Action Description Coupon Entered User entered a coupon on a shopping cart or order Coupon Applied Coupon was applied on a user’s shopping cart or order Coupon Denied Coupon was denied from a user’s shopping cart or order Coupon Removed User removed a coupon from a cart or order Coupon EnteredCoupon AppliedCoupon DeniedCoupon Removed Coupon Entered cioanalytics.track('Coupon Entered', { order_id: "order123", cart_id: "cool_persons_cart_123", coupon_id: "NEWUSER20" }) cart_id string The ID of the cart that the coupon applies to (if applicable). coupon_id string the coupon ID the person entered. order_id string The order/transaction the coupon applies to (if applicable). Coupon Applied cioanalytics.track(‘Coupon Applied’, { order_id: “order123”, cart_id: “cool_persons_cart_123”, coupon_id: “NEWUSER20”, coupon_name: “$20 off for new users”, discount: 20 }) cart_id string The ID of the cart that the coupon applies to (if applicable). coupon_id string the coupon ID the person entered. coupon_name string The name of the coupon, if applicable. discount number The discount applied through the coupon. This is the ammount subtracted from the total in other transaction events. order_id string The order/transaction the coupon applies to (if applicable). Coupon Denied cioanalytics.track(‘Coupon Applied’, { order_id: “order123”, cart_id: “cool_persons_cart_123”, coupon_id: “NEWUSER20”, coupon_name: “$20 off for new users”, reason: “Not customer’s first order” }) cart_id string The ID of the cart that the coupon applies to (if applicable). coupon_id string the coupon ID the person entered. coupon_name string The name of the coupon, if applicable. order_id string The order/transaction the coupon applies to (if applicable). reason string The reason the coupon was denied. Coupon Removed cioanalytics.track(‘Coupon Removed’, { order_id: “order123”, cart_id: “cool_persons_cart_123”, coupon_id: “NEWUSER20”, coupon_name: “$20 off for new users”, discount: 20 }) cart_id string The ID of the cart that the coupon applies to (if applicable). coupon_id string the coupon ID the person entered. coupon_name string The name of the coupon, if applicable. discount number The discount applied through the coupon. This is the ammount subtracted from the total in other transaction events. order_id string The order/transaction the coupon applies to (if applicable). Wishlisting overview Send these events if your ecommerce app supports wishlist features. Action Description Product Added to Wishlist User added a product to the wish list Product Removed from Wishlist User removed a product from the wish list Wishlist Product Added to Cart User added a wishlist product to the cart Product Added to WishlistProduct Removed from WishlistWishlist Product Added to Cart Product Added to Wishlist cioanalytics.track('Product Added to Wishlist', { wishlist_id: "wishlist123", wishlist_name: "Favorite Shoes", product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" }) Product Removed from Wishlist cioanalytics.track('Product Removed from Wishlist', { wishlist_id: "wishlist123", wishlist_name: "Favorite Shoes", product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" }) Wishlist Product Added to Cart cioanalytics.track('Wishlist Product Added to Cart', { wishlist_id: "wishlist123", wishlist_name: "Favorite Shoes", product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, quantity: 1, coupon: "NEWUSER20", position: 3, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" }) Sharing overview If your store integrates with social apps or or supports sharing, you can send events when your customers share product information. Action Description Product Shared Shared a product with one or more friends Cart Shared Shared the cart with one or more friends Product SharedCart Shared Product Shared cioanalytics.track('Product Shared', { share_via: "email", share_message: "Check out these cool shoes!", recipient: "friendOfcool.person@example.com", product_id: "coolshoes-123", sku: "abc-123-xyz", category: "shoes", name: "Cool Shoes", brand: "A Shoe Brand", variant: "red", price: 139.99, url: "https://www.example.com/product/123", image_url: "https://www.example.com/product/123.jpg" }) brand string The brand associated with the product. category string The product category a person viewed. image_url string The URL of the product image. name string The name of the product a person viewed. price number The price of the product. recipient string The person the product was shared with. share_message string The message the customer sent with the share. share_via string The channel the product was shared through. sku string The stock keeping unit (SKU) of the product a person viewed. url string The URL of the product page. variant string The variant of the product a person viewed, if applicable. Cart Shared cioanalytics.track('Cart Shared', { share_via: "email", share_message: "Check out my cart!", recipient: "friendOfcool.person@example.com", cart_id: "cool_persons_cart_123", products: [ { product_id: "coolshoes-123" } ] }) cart_id string The shopping cart ID. products array of [ objects ] An array of product IDs contained in the shared cart. recipient string The person the product was shared with. share_message string The message the customer sent with the share. share_via string The channel the product was shared through. Reviewing overview Send the Product Reviewed event when customers review products in your store. Action Description Product Reviewed User reviewed a product cioanalytics.track('Product Reviewed', { product_id: "coolshoes-123", review_id: "review_123", review_body: "These shoes are great!", rating: 5 }) rating integer The rating the customer gave the product. review_body string The body of the review. review_id string The ID of the review. --- ## Email events URL: https://docs.customer.io/integrations/data-in/semantic-events/email/ If you send email events into Customer.io from somewhere other than your own workspace, you should shape your events to use the following common fields. How it works Email service providers and other platforms produce events that help you track the lifecycles of your emails—whether emails are delivered, opened, etc. If you send email events into Customer.io, we recommend that you follow the specifications on this page. See Email Events if you want to download the specification or try out email events. We use this specification to support default actions—any we have now and ones we might add in the future. If you send email lifecycle events that conform to this specification, you’ll be ready to trigger downstream actions in integrations that rely on these events. Events in an email lifecycle Emails (outside of Journeys) produce the following events. Not every message will produce the same events, so it may be helpful to understand the possible lifecycle of an email. Email Bounced Email Delivered Email Link Clicked Email Marked as Spam Email Opened Unsubscribed flowchart LR a{Was email delivered?}-.->|no|b(Email Bounced) a-->|yes|c(Email Delivered) c-->x{DidDoes the user open the email?}-->|yes|d(Email Opened) d-->e{Did the user respond?}-->|yes|z subgraph z[Responses: not mutually exclusive] direction LR f(Email Link Clicked) g(Email Marked as Spam) h(Unsubscribed) end Email Bounced This event indicates that your email server/service provider couldn’t deliver a message to the recipient. { "user_id": "020ba8yf4r", "action": "track", "event": "Email Bounced", "context": { "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } context object Contextual information about the event. These fields are in addition to any captured automatically by your source library. traits object Traits you want to capture as context for the event. email string The recipient’s email address. Email Delivered This event indicates that your email server/service provider delivered a message to the recipient. { "user_id": "020ba8yf4r", "action": "track", "event": "Email Delivered", "context": { "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } context object Contextual information about the event. These fields are in addition to any captured automatically by your source library. traits object Traits you want to capture as context for the event. email string The recipient’s email address. Email Opened This event indicates that the user opened the email. Email opens may rely on tracking pixels or other techniques that aren’t 100% reliable. Even if you send this event yourself, you may not be able to capture every open. You’re likely to get concrete metrics from the following three events. { "user_id": "020ba8yf4r", "action": "track", "event": "Email Opened", "context": { "ip": "67.207.109.102", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } Email Link Clicked Send this event when someone clicks a link in your message. In most cases, you’ll need to add some kind of query parameter or other link-tracking mechanism to track clicks this way. { "user_id": "020ba8yf4r", "action": "track", "event": "Email Link Clicked", "context": { "ip": "67.207.109.102", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } properties object link_id string The ID of the link that the user clicked. link_url string The URL of the link that the user clicked. Email Marked as Spam Send this event when a user marks your message as spam. { "user_id": "020ba8yf4r", "action": "track", "event": "Email Marked as Spam", "context": { "ip": "67.207.109.102", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } Unsubscribed Send this event when a user unsubscribes from your emails. You can also send a list values to indicate that a user unsubscribed from a specific mailing list. { "user_id": "020ba8yf4r", "action": "track", "event": "Unsubscribed", "context": { "ip": "67.207.109.102", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", "traits": { "email": "cool.person@example.com" } }, "properties": { "email_id": "18vzF7u3z", "email_subject": "20% off: A token of our appreciation!", "campaign_id": "abc123", "campaign_name": "New Customer Discount" } } properties object list_id string The ID of the mailing list the user unsubscribed from. list_name string The name of the mailing list the user unsubscribed from. --- ## Live chat events URL: https://docs.customer.io/integrations/data-in/semantic-events/live-chat/ If you send live chat events into Customer.io, you should shape incoming events using the formats on this page. We base our default actions on these formats, making it easier to use your events in downstream integrations. How it works If you send live chat events into Data Customer.io, you should shape incoming events using the formats on this page. We use this specification to support default actions—any we have now and ones we might add in the future. If you send live chat events that conform to this specification, you’ll be ready to trigger downstream actions in integrations that rely on an live chat events. See Live Chat Events if you want to download the specification or try out live chat events. Events in the live chat lifecycle Live chats typically produce the following events. To make the most of your live chat events, make sure that you use the event names and structures set below. In general, you’ll open a conversation, send and receive messages, and then close the conversation. Live Chat Conversation Started LIve Chat Conversation Ended Live Chat Message Sent Live Chat Message Received sequenceDiagram participant a as Data Source participant b as User b->>a: Live Chat Conversation Started opt Some message activity occurs b-->>a: Message Received a-->>b: Message Sent end note over a,b: Chat ends b->>a: Live Chat Conversation Ended Live Chat Events Live chat consists of four events. You can see payloads below, but the schema is the same for each; only the event name changes. These events are from your perspective, so you’ll send Live Chat Message Sent events when you or your agent sends a message and Live Chat Message Received events when you or your agent receives a message. While all properties in events are optional, you should send the conversation_id and message_id properties in your events at a minimum, so you can trace individual messages to a specific chat instance. Live Chat Conversation StartedLive Chat Conversation EndedLive Chat Message SentLive Chat Message Received Live Chat Conversation Started { "userId": "020ba8yf4r", "action": "track", "event": "Live Chat Conversation Started", "properties": { "agent_id": "adf21fcad99100", "agent_name": "Wile E Coyote", "agent_username": "wileecoyte", "conversation_duration": 0, "conversation_id": "abd627dbecffc", "message_body": "Meep Meep", "message_id": "fgdaab013614cda" } } Live Chat Conversation Ended { "userId": "020ba8yf4r", "action": "track", "event": "Live Chat Conversation Ended", "properties": { "agent_id": "adf21fcad99100", "agent_name": "Wile E Coyote", "agent_username": "wileecoyte", "conversation_duration": 120, "conversation_id": "abd627dbecffc", "message_body": "That's all, folks!", "message_id": "fgdaab013614cda" } } Live Chat Message Sent { "userId": "020ba8yf4r", "action": "track", "event": "Live Chat Message Sent", "properties": { "agent_id": "adf21fcad99100", "agent_name": "Wile E Coyote", "agent_username": "wileecoyte", "conversation_duration": 80, "conversation_id": "abd627dbecffc", "message_body": "*Help!*", "message_id": "fgdaab013614cda" } } Live Chat Message Received { "userId": "020ba8yf4r", "action": "track", "event": "Live Chat Message Received", "properties": { "agent_id": "adf21fcad99100", "agent_name": "Wile E Coyote", "agent_username": "wileecoyte", "conversation_duration": 40, "conversation_id": "abd627dbecffc", "message_body": "Meep Meep", "message_id": "fgdaab013614cda" } } agent_id string The ID of the agent taking the conversation. agent_name string The real name of the agent. agent_username string The username of the agent. conversation_duration integer The duration of the conversation in seconds. 0 when the conversation starts, and the number of seconds indicates the time in the conversation when each message conversation_id string The ID of the conversation. message_id string The ID of the message that starts the conversation. --- ## Mobile App Lifecycle Events URL: https://docs.customer.io/integrations/data-in/semantic-events/mobile-app/ Our Mobile App Lifecycle Event specification helps you send events in a uniform format that automatically maps to the integrations we support. Following this spec makes it easier to use data from your mobile apps without having to re-map your incoming data to each integration. How it works Our Mobile App Lifecycle Event specification helps you send events in a uniform format that automatically maps data to the integrations we support. Following this spec makes it easier to use data from your mobile apps without having to re-map your incoming data each time you set up a new integration. These events typically contain information about your app version and build. If you use our SDKs, we capture some of the following events automatically. Some, like Application Crashed, are events you’ll send manually. Our mobile SDKs send lifecycle events automatically If you use one of our mobile SDKs, we automatically capture the following events by default. Application Installed Application Opened Application Backgrounded Application Foregrounded Application Updated These events can help you understand your customer’s lifecycle—like the time they spend in the app (time from opened to backgrounded), how often they update the app, and so on. You’ll notice that our mobile SDKs don’t capture events like Application Crashed automatically. That’s because the app must be open for us to send events from your app to Customer.io. These are the minimum required versions of our mobile SDKs to capture application lifecycle events. If you’re running an earlier version, update the SDK to take advantage of these events and other features! SDK Minimum version for lifecycle events Android 3.0 iOS 4.0 React Native 4.0 Flutter 2.0 Expo 2.0 Application Installed Our CDP-enabled SDKs send this event the first time a user opens your application. If the user never opens your app, we won’t be able to collect this event. This event doesn’t wait for the SDK to capture attribution or campaign information, and is collected automatically. Platforms like Facebook and Google require discrete install events to correctly attribute installs to ads served through their platforms. { "userId": "019mr8mf4r", "type": "track", "event": "Application Installed", "properties": { "version": "1.2.3", "build": "1234" } } event string A person installed your app.Accepted values:Application Installed properties object The properties typically sent with the event. build string The specific build of the app installed. version string The version of the app installed. event string A person installed your app.Accepted values:Application Installed properties object The properties typically sent with the event. build string The specific build of the app installed. version string The version of the app installed. Application Opened Send this event when a user launches your mobile application—after the first open. The first open results in an Application Installed event, so we only send this event on subsequent opens. Like the Application Installed event, this event also does not wait for the SDK to capture attribution information but it can include information about referring applications or deep link URLs if available when the app opens. { "userId": "019mr8mf4r", "type": "track", "event": "Application Opened", "properties": { "from_background": false, "referring_application": "GMail", "url": "url://location" } } event string A person opened your app. Our SDKs automatically send this event when a person opens your app fresh or when they return to the app after sending it to the the background.Accepted values:Application Opened properties object The properties typically sent with the event. build string The specific build of the app opened. from_background boolean If true, the user opened the app from the background. url string The value of UIApplicationLaunchOptionsURLKey from launchOptions. Collected on iOS only. version string The version of the app opened. event string A person opened your app. Our SDKs automatically send this event when a person opens your app fresh or when they return to the app after sending it to the the background.Accepted values:Application Opened properties object The properties typically sent with the event. build string The specific build of the app opened. from_background boolean If true, the user opened the app from the background. url string The value of UIApplicationLaunchOptionsURLKey from launchOptions. Collected on iOS only. version string The version of the app opened. Application Backgrounded Send this event when a user backgrounds the application upon applicationDidEnterBackground. { "userId": "019mr8mf4r", "type": "track", "event": "Application Backgrounded", "properties": {} } event string A person sent your app to the background. Our SDKs automatically send this event when a person backgrounds your app.Accepted values:Application Backgrounded properties object There are no default properties for the Application Backgrounded event. You can add custom properties to the event payload. event string A person sent your app to the background. Our SDKs automatically send this event when a person backgrounds your app.Accepted values:Application Backgrounded properties object There are no default properties for the Application Backgrounded event. You can add custom properties to the event payload. Application Foregrounded Send this event when your audience returns to your app after previously backgrounding it. This event is distinct from Application Opened in that the app is considered opened already, even if it’s in the background. { "userId": "019mr8mf4r", "type": "track", "event": "Application Foregrounded", "properties": {} } event string A person brought your app to the foreground after having backgrounded it, indicating that they’re using it again. Our SDKs automatically send this event when a person brings your app back to the foreground.Accepted values:Application Foregrounded properties object There are no default properties for the Application Foregrounded event. You can add custom properties to the event payload. event string A person brought your app to the foreground after having backgrounded it, indicating that they’re using it again. Our SDKs automatically send this event when a person brings your app back to the foreground.Accepted values:Application Foregrounded properties object There are no default properties for the Application Foregrounded event. You can add custom properties to the event payload. Application Updated This event fires when the user opens your app after updating your app. Our CDP-enabled SDKs automatically collect this event rather than the “Application Opened” event after someone updates your app. { "userId": "019mr8mf4r", "type": "track", "event": "Application Updated", "properties": { "previous_version": "1.1.2", "previous_build": "1234", "version": "1.2.0", "build": "1456" } } event string A person updated your app.Accepted values:Application Updated properties object The properties typically sent with the event. build string The specific build of the app the person upgraded to. previous_build string The previous build of the app installed—the build a person is upgrading from. previous_version string The previous version of the app installed—the version a person is upgrading from. version string The version of the app a person upgraded to. event string A person updated your app.Accepted values:Application Updated properties object The properties typically sent with the event. build string The specific build of the app the person upgraded to. previous_build string The previous build of the app installed—the build a person is upgrading from. previous_version string The previous version of the app installed—the version a person is upgrading from. version string The version of the app a person upgraded to. Application Uninstalled You can send this event when a user uninstalls your app. Like similar events that happen outside of your app itself, our CDP-enabled SDKs cannot capture this event automatically. However, some direct-mode destinations detect this for you using silent push notifications through their own SDKs. You might also be able to send these events to Customer.io using callbacks. { "userId": "019mr8mf4r", "type": "track", "event": "Application Uninstalled", "properties": {} } event string A person uninstalled your app.Accepted values:Application Uninstalled properties object There are no default properties for the Application uninstalled event. You can add custom properties to the event payload. event string A person uninstalled your app.Accepted values:Application Uninstalled properties object The properties typically sent with the event. Application Crashed You can send this event when you receive a crash notification from your app, but it is not meant to supplant traditional crash reporting tools. By tracking crashes this way, you can analyze which types of users are impacted by crashes and how those crashes affect their engagement. You may also want to send messages to users who experience crashes through other messaging channels. Like similar events that happen outside of your app itself, our CDP-enabled SDKs cannot capture this event automatically. But you might be able to capture this kind of event with a callback or a webhook. { "userId": "019mr8mf4r", "type": "track", "event": "Application Crashed", "properties": {} } event string A person experienced a crash in your app. Our SDKs automatically send this event when a person experiences a crash in your app.Accepted values:Application Crashed properties object There are no default properties for the Application Crashed event. You can add custom properties to the event payload. event string A person experienced a crash in your app. Our SDKs automatically send this event when a person experiences a crash in your app.Accepted values:Application Crashed properties object There are no default properties for the Application Crashed event. You can add custom properties to the event payload. --- ## Video playback events URL: https://docs.customer.io/integrations/data-in/semantic-events/video/ Video events help you observe and report how customers engage with your videos and ad content. Using the specification outlined on this page will help you take advantage of out-of-the-box actions for integrations that rely on video playback events. How it works Video events help you observe and report how customers engage with your videos and ad content. Using the specification outlined on this page will help you take advantage of out-of-the-box actions for integrations that rely on video playback events. See Video Playback Events if you want to download the specification or try out video playback events. Playback events occur in three areas: Video: this represents the complete movie, program, or video that a person watches. Content: often referred to as a “pod,” this is a group of content within the video—like a video segment with a mid-roll ad would be considered 2 pods (one before the ad and one after). Ads: these are the ads that play within the video. You’ll send events across all three areas to track how people engage with your video content. Playback lifecycle Remember that a “video” itself represents all of the content (pods) and ads in a session. You’ll want to send playback events not only when a user starts a video, but also when each content pod or ad starts. Beyond that, you’ll also want to send “heartbeat” events on a regular interval that a content pod or ad plays, helping you determine how long people watch your content. We recommend sending a heartbeat event every 10 seconds. flowchart LR A(Video Playback Started)-->b(Video Content Started)-->|send every 10 seconds|c c(Video Content Playing) c-.->|User pauses video|f(Video Playback Paused) f-.->|User resumes video|m(Video Playback Resumed)-.->c c-->|content ends|n(Video Content Completed) n-->p{Is this the end of the video?} p-->|yes|r(Video Playback Completed) p-.->|no|s{What plays next?} s-.->|Content|b s-.->|Ads|t(Video Ad Started) Video Playback events Playback events represent the playback events related to the video player itself. Some of these events are things your audience explicitly triggers—play, pause, resume, and so on, but these also cover buffering and other events that affect the player. When a user presses play, you’ll start with a Video Playback Started event containing a session_id that you’ll use for subsequent events in the session. The session ID helps you track a user’s session with a specific video player and group of content/ads. So if a page on your website contains two video players, each player should have a separate session and session ID. But, if the page only contained one player and it contained two “video contents” in a row, you’d have one session ID with two “Video Content” events. When you send a Video Playback Started event, you’ll send arrays of content_asset_ids, content_pod_ids, ad_asset_id, and ad_pod_id—indicating all the potential assets in the playback session. For all other events, you’ll send the individual IDs as strings—indicating the specific pod or asset a person interacts with. Event Description Video Playback Started Sent when a user starts a video. Video Playback Paused Sent when a user pauses a video. Video Playback Resumed Sent when a user resumes a video. Video Playback Completed Sent when a user completes a video. Video Playback Buffer Started Sent when a video starts buffering. Video Playback Buffer Completed Sent when a video stops buffering. Video Playback Seek Started When a user manually seeks to a position in the content or ad in the playback session. Use seek_position to denote where the user is seeking to position to denote where the user’s original position. Video Playback Seek Completed Sent when a user stops seeking through a video. Video Playback Interrupted Sent when playback stops unintentionally (from network loss, browser close/redirect, or app crash). With this event you can pass a property to denote the cause of the interruption. Video Playback Exited Sent when a user navigates away from a playback/stream. Video Playback Started When a user starts a video, you’ll send a Video Playback Started event. This event should contain the following properties. When you start video playback, you’ll define arrays of content_asset_ids and content_pod_ids potentially in the session; these help you understand all the content that could possibly be contained within the session. All other playback events list an individual content_asset_id and/or content_pod_id. { "action": "track", "event": "Video Playback Started", "userId": "userId", "properties": { "session_id": "12345", "content_asset_ids": ["0129370"], "content_pod_ids": ["episode1", "episode2"], "ad_asset_id": ["ad123", "ad097"], "ad_pod_id": ["adSegment1", "adSegment2"], "ad_type": ["pre-roll", "mid-roll"], "position": 0, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Other Playback Events All playback events aside from Video Playback Started include a content_asset_id and content_pod_id to help you track the relevant content and ad when playback events occur. The Video Playback Interrupted event includes a method parameter to help you define the method by which playback was interrupted. Video Playback PausedVideo Playback ResumedVideo Playback CompletedVideo Playback Buffer StartedVideo Playback Buffer CompletedVideo Playback Seek StartedVideo Playback Seek CompletedVideo Playback Seek InterruptedVideo Playback Seek Exited Video Playback Paused { "action": "track", "event": "Video Playback Paused", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 0, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Resumed { "action": "track", "event": "Video Playback Resumed", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 20, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Completed { "action": "track", "event": "Video Playback Completed", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 3600, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Buffer Started { "action": "track", "event": "Video Playback Buffer Started", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 1800, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Buffer Completed { "action": "track", "event": "Video Playback Buffer Completed", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 1800, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Seek Started { "action": "track", "event": "Video Playback Seek Started", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "seek_position": 1900, "position": 1800, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Seek Completed { "action": "track", "event": "Video Playback Seek Completed", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 1900, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false } } Video Playback Seek Interrupted { "action": "track", "event": "Video Playback Seek Interrupted", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 1800, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false, "method": "network loss" } } Video Playback Seek Exited { "action": "track", "event": "Video Playback Exited", "userId": "userId", "properties": { "session_id": "12345", "content_asset_id": "0129370", "content_pod_id": "episode1", "position": 3600, "total_length": 3600, "bitrate": 100, "framerate": 29.00, "video_player": "youtube", "sound": 42, "full_screen": true, "ad_enabled": true, "quality": "hd1080", "livestream": false, } } Content events Within a playback session, videos contain “pods” of content. A pod is a group or segment of content or an advertisement. Imagine you have a playback session that with a single piece of video content and one mid-roll advertisement. This means that your video contains two content pods—one before the mid-roll ad and one after. In this instance, you’d start and complete the first pod of content; you’d start and complete the ad; you’d start and complete the second pod of content. All of this would happen within one playback start. You should also Video Content Playing on a regular interval as a heartbeat event. This helps you track how long people watch your content. Event Description Video Content Started Sent when a user starts a new piece of video content. Video Content Playing Sent when a user is playing a piece of video content. Video Content Completed Sent when a user completes a piece of video content. Below are some sample events with definitions for each property you might send in these events. Video Content StartedVideo Content PlayingVideo Content Completed Video Content Started { "action": "track", "event": "Video Content Started", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "program": "The Last Dance", "title": "Episode 1", "description": " Flashbacks chronicle Michael Jordan's college and early NBA days. The Bulls make a preseason trip to Paris amid tensions with GM Jerry Krause.", "season": "1", "position": 0, "total_length": 3600, "genre": "Documentary", "publisher": "ESPN", "full_episode": true, "keywords": ["NBA", "Basketball", "Jordan", "Chicago Bulls"] } } Video Content Playing { "action": "track", "event": "Video Content Playing", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "program": "The Last Dance", "title": "Episode 1", "description": " Flashbacks chronicle Michael Jordan's college and early NBA days. The Bulls make a preseason trip to Paris amid tensions with GM Jerry Krause.", "season": "1", "position": 800, "total_length": 3600, "genre": "Documentary", "publisher": "ESPN", "full_episode": true, "keywords": ["NBA", "Basketball", "Jordan", "Chicago Bulls"] } } Video Content Completed { "action": "track", "event": "Video Content Completed", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "program": "The Last Dance", "title": "Episode 1", "description": " Flashbacks chronicle Michael Jordan's college and early NBA days. The Bulls make a preseason trip to Paris amid tensions with GM Jerry Krause.", "season": "1", "position": 3600, "total_length": 3600, "genre": "Documentary", "publisher": "ESPN", "full_episode": true, "keywords": ["NBA", "Basketball", "Jordan", "Chicago Bulls"] } } airdate string  (date-time) The ISO-8601 date-time when the video content/pod originally aired or was published. asset_id string The ID of the video a user interacted with. bitrate integer The current kbps. channel string The channel the video content/pod belongs to or is aired on, like hgtv or my_youtube_channel. description string The description of the video content/pod. framerate integer The average frames per second (fps). full_episode boolean Set to true if the content is a full episode of a show. genre string The genre of the video content/pod. keywords array of [ strings ] Keywords associated with the video content/pod. livestream boolean Set to true if the content is a livestream. pod_id string The ID of the content “pod” in a playback session. Imagine a video that contains content and an ad; that means the video contains two pods—one for the content and one for the ad. This is the specific piece of content that the user interacted with. position integer The current index position in seconds of the playhead, including the duration of any ads seen (if available). If the playback is a livestream, check the documentation for relevant destinations for details on how to correctly pass the playhead position. program string The name of the program or show the content belongs to (if applicable). publisher string The publisher of the video content/pod. season string The season the video content/pod belongs to, if applicable. session_id string The unique ID of the overall session used to tie all events generated from a specific playback. This value should be the same across all playback, content, and ad events if they are from the same playback session. title string The title of the video content/pod. total_length integer The total duration of the content/asset in seconds. Note that this is not the duration of the video itself or the playback, but a piece of a complete “pod” of content. If the content is an ad in the video, this is the length of the ad. Ad events Ad events are similar to content events, but they represent the ads that play within a video. Ads can also be “pods”—like a group of ads For example, imagine that your video has two pre-roll ads, one mid-roll ad, and one post-roll ad. This results in three ad “pods”: ad pod 1: plays the two pre-roll ads ad pod 2: plays the one mid-roll ad ad pod 3: plays the one post-roll ad Event Description Video Ad Started Sent when a user starts a new ad. Video Ad Playing Sent when a user is playing an ad. Video Ad Completed Sent when a user finishes an ad. Below are some sample events with definitions for each property you might send in these events. Video Ad StartedVideo Ad PlayingVideo Ad Completed Video Ad Started { "action": "track", "event": "Video Ad Started", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "type": "pre-roll", "title": "The New New Thing!", "position": 0, "total_length": 30, "publisher": "Apple", "load_type": "dynamic" } } Video Ad Playing { "action": "track", "event": "Video Ad Playing", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "type": "pre-roll", "title": "The New New Thing!", "position": 10, "total_length": 30, "publisher": "Apple", "load_type": "dynamic" } } Video Ad Completed { "action": "track", "event": "Video Ad Completed", "userId": "userId", "properties": { "session_id": "abcde", "asset_id": "8675309", "pod_id": "segment1", "type": "pre-roll", "title": "The New New Thing!", "position": 30, "total_length": 30, "publisher": "Apple", "load_type": "dynamic" } } asset_id string The ID of the video a user interacted with. content object For video destinations that require you to send content metadata with ad events, you can send all the content metadata in this object—like content.title, etc. load_type string Set to dynamic if you insert ads dynamically and linear if ads are the same for all viewers.Accepted values:dynamic,linear pod_id string The ID of the content “pod” in a playback session. Imagine a video that contains content and an ad; that means the video contains two pods—one for the content and one for the ad. This is the specific piece of content that the user interacted with. pod_length integer The number of ad assets in the ad-pod pod_position integer The position of the pod/asset relative to other assets in the same pod. If your ad pod plays 3 ads, you would indicate if this is add number 1, 2, or 3. position integer The current index position in seconds of the playhead, with respect to the length of the ad. publisher string The ad’s publisher. quartile integer Specifies the quartile of the ad that a viewer reached. If you use our client-side libraries, we’ll automatically track this for you. session_id string The unique ID of the overall session used to tie all events generated from a specific playback. This value should be the same across all playback, content, and ad events if they are from the same playback session. title string The title of the ad. total_length integer The total duration of the ad in seconds. type string The ad type. Values can include pre-roll, mid-roll, and post-roll.Accepted values:pre-roll,mid-roll,post-roll Heartbeat events You should send a “heartbeat” event called Video Content Playing or Video Ad Playing every 10 seconds to indicate that a video (or ad) is still playing. This event contains the following properties: Video Content Playing Video Content Playing Video Ad Playing Video Ad Playing Resuming playback When you send a Video Playback Resumed event, you should also send a heartbeat event (Video Content Playing or Video Ad Playing). Make sure you resume your 10 second heartbeats when users resume video playback (likely after you sent a Video Playback Paused event). Video quality event As with pausing and resuming video, you should also send events when your audience changes video quality settings. We don’t have actions that hinge specifically on playback quality changes, but tracking quality can help you figure out the performance of your video content—whether your users can stream it in its original resolution or if they need to change it. A quality change event should contain the following properties. bitrate integer The current kbps. droppedFrames integer The number of frames dropped during playback. framerate integer The average frames per second (fps). startupTime integer The time it takes for the video to start playing. --- ## Backfill historical data URL: https://docs.customer.io/integrations/data-in/importing-old-data/ As you get started with Customer.io, you might want to backfill historical data for people and events. This page helps you get started. Make sure you understand [how backfilled data can trigger campaigns](#backfill-data-campaigns), if you're already up and running. Backfill people and attributes You can backfill people and their attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in your Customer.io account through our integrations or manually in your workspace. You might do this to store all your data within Customer.io so you can create segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. and campaign logic around them. Learn more about when backfilled data would trigger campaigns. Advanced: write a script If you’re a developer, you can write a script to loop through all of the people in your database and add them to Customer.io. Make sure you follow our API guidelines as it can cause significant API traffic.  We may not process your API calls in the order you send them If you’re sending multiple identify calls per user, and the chronology of the attribute updates is important, you must send a _timestamp value as discussed in our documentation on people’s attributes. Easier: upload a CSV Customer.io shines brightest when you send real-time data about your users. However, sometimes you need to backfill historical data or add data not integrated with your Customer.io account, like a list of leads from a conference. In these cases, you can import a CSV file to add or update your people. Backfill events You can send historical eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. data to Customer.io by adding a timestamp to your API call, as shown in the examples below. You might do this to store all your data within Customer.io, add people to segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static., or trigger campaigns. See our Pipelines API (recommended) or classic Track API for more information about track calls. If you base segment membership on events, backfilling events will add people to segments. However, if you use event-based segment membership as a campaign trigger, backfilled events will not reliably trigger campaigns. Learn more about when backfilled data would trigger campaigns.  Events older than 30 days won’t appear in Activity Logs If you upload older events, you can still take advantage of them in segments and other places, but they won’t appear in your activity logs. Pipelines API (Recommended)Track API cURL Pipelines API (Recommended) curl -X POST https://cdp.customer.io/v1/api/track \ -u api_key: <your_api_key_here> \ -H 'Content-Type: application/json' \ -d '{ "userId": "5", "event": "purchased", "properties": { "price": 23.45 }, "timestamp": 1359389415 }' Track API cURL curl -i https://track.customer.io/api/v1/customers/5/events \ -X POST \ -u YOUR-SITE-ID-HERE:YOUR-SECRET-API-KEY-HERE \ -H 'Content-Type:application/json' \ -d '{"name":"purchased","timestamp":1359389415, "data":{"price":23.45}}' Backfilled data can trigger campaigns Backfilled data can trigger campaigns made with a segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. or eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. trigger. However, we only trigger a campaign when the backfilled data happened after the campaign started. Segment-triggered campaigns After you activate a segment-triggered campaign, you might realize you need to backfill data that would add people to the segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. that triggers the campaign. If the _timestamp of the people/attribute change is within 24 hours of when Customer.io processed the data, people can enter the campaign. We say people can enter because it also depends on the campaign’s filter and frequency settings. For example, let’s say you activated the segment-triggered campaign 10 days ago. Then you backfilled attribute data and looked at your activity log: The timestamp is today at 11:38:04 am (CST). And it finished processing today at 11:39:04 am (CST). This could trigger your campaign because: the timestamp is AFTER the campaign started the difference between the timestamp and processed at timestamp is less than 24 hours However, if your campaign begins with a delay longer than 24 hours, we’ll use the delay length as the maximum difference. For example, if you begin a campaign with a 4 day wait, then the max difference between the timestamp and processed at timestamp is now 96 hours, not 24. Event-triggered campaigns After you activate an event-triggered campaign, you might realize you need to backfill event data that would trigger this campaign. If the timestamp of the event is within 72 hours of when Customer.io processed the data, people can enter your campaign. We say people can enter because it also depends on the campaign’s filter and frequency settings. For example, let’s say your campaign is triggered when someone completes a survey, and you activated the campaign 10 days ago. Then you backfill event data like this: The timestamp is today at 11:20:27 am (CST). And it finished processing today at 11:22:40 am (CST). This could trigger your campaign because: the timestamp is AFTER the campaign started the difference between the timestamp and processed at timestamp is less than 72 hours However, if your campaign begins with a delay longer than 72 hours, we’ll use the delay length as the maximum difference. For example, if you begin a campaign with a 4 day wait, then the max difference between the timestamp and processed at timestamp is now 96 hours, not 72. --- ## Proxying requests to Customer.io URL: https://docs.customer.io/integrations/data-in/proxying-requests/ You can route client-side requests through your own server to improve your security posture and keep all requests to Customer.io 'first party.' Proxying requests through your backend server lets you keep your Customer.io credentials secure while maintaining control over data collection. Instead of sending data directly from client applications to Customer.io, your SDKs send data to a server you control, which then forwards the requests to Customer.io. Why proxy requests? There are several reasons you might want to proxy requests through your backend: Security: Keep your Customer.io credentials secure on your backend instead of embedding them in client applications. Privacy and compliance: Route requests through your own domain to provide better privacy controls to meet app store requirements or avoid 3rd-party blockers. Enrich data: If necessary, you can transform, enrich, or filter requests before you send them to Customer.io. General setup process You’ll need to do a few things to proxy requests from your client to Customer.io: Configure your client (mobile app, website, etc.) to send requests to your backend instead of Customer.io. Set up your backend to receive requests, extract authentication tokens, and validate them. Forward events to Customer.io by parsing events and sending them with your actual Customer.io credentials. Handle incoming requests by tying together authentication, validation, and forwarding in your request handler. 1. Configure your client Configure your SDK to send requests to your backend instead of Customer.io. The exact configuration settings you need to change depend on whether you use our JavaScript integration for your website or one of our mobile SDKs. For our JavaScript client see our JavaScript proxy instructions. The way you invoke a proxy depends on whether you use our JavaScript snippet or import the cdp-analytics-browser library. For our Mobile SDKs you’ll set apiHost and cdnHost settings when you initialize the SDK. See: Android proxy iOS proxy Expo plugin proxy React Native proxy Flutter proxy In any case, you’ll set an authorization key. Normally this would be something you get from Customer.io. But, when you use a proxy, you can use any key that you can decode into basic authorization credentials when requests hit your proxy server. We’ll discuss that in more detail later on this page. 2. Set up your backend Your backend needs to receive requests from your client SDKs and authenticate them using whatever custom authorization value you provide. (You can simply send your Customer.io credentials, but this would defeat the added security of the proxy!) The SDK sends a base64-encoded Authorization header as Basic <base64-encoded-value>. Your backend needs to: Initialize a server and Customer.io SDK client with your actual credentials (stored securely) Extract the authentication token from incoming requests Validate the token against your authentication system Prepare a valid credential for the requests you forward to Customer.io The samples below provide a reference implementation to help you proxy requests to Customer.io. It is not a complete guide for security or hardening your services. While proxying requests helps secure your credentials, you can take additional security measures depending on your organization’s security requirements and risk tolerance. Node.js Node.js const express = require('express'); const { Analytics } = require('@customerio/cdp-analytics-node'); const app = express(); // Your actual Customer.io credentials (stored securely on backend) const CIO_WRITE_KEY = process.env.CIO_WRITE_KEY; // Create Customer.io client for forwarding events const cioAnalytics = new Analytics({ writeKey: CIO_WRITE_KEY }); // Extract authentication token from request function extractClientToken(req) { const authHeader = req.headers.authorization || ''; if (!authHeader.startsWith('Basic ')) { return null; } // Decode the base64-encoded credentials const base64Credentials = authHeader.split(' ')[1]; const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); // Extract the token (the username part of user:password) return credentials.split(':')[0]; } // Validate the client's authentication token function isValidClientToken(token) { // Implement your authentication logic here // This could check against a database, validate a JWT, etc. return token && token.startsWith('client-api-key-'); } Python Python from flask import Flask, request, jsonify import os import base64 import json from customerio_cdp_analytics import Analytics app = Flask(__name__) # Your actual Customer.io credentials (stored securely on backend) CIO_WRITE_KEY = os.environ.get('CIO_WRITE_KEY') # Create Customer.io client for forwarding events cio_analytics = Analytics(write_key=CIO_WRITE_KEY) # Extract authentication token from request def extract_client_token(request): auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Basic '): return None # Decode the base64-encoded credentials base64_credentials = auth_header.split(' ')[1] credentials = base64.b64decode(base64_credentials).decode('utf-8') # Extract the token (the username part of user:password) return credentials.split(':')[0] # Validate the client's authentication token def is_valid_client_token(token): # Implement your authentication logic here # This could check against a database, validate a JWT, etc. return token and token.startswith('client-api-key-') Go Go package main import ( "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" "github.com/customerio/cdp-analytics-go" ) var cioAnalytics *analytics.Client func init() { // Your actual Customer.io credentials (stored securely on backend) cioWriteKey := os.Getenv("CIO_WRITE_KEY") cioAnalytics = analytics.New(cioWriteKey) } // Extract authentication token from request func extractClientToken(r *http.Request) string { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Basic ") { return "" } // Decode the base64-encoded credentials base64Credentials := strings.TrimPrefix(authHeader, "Basic ") credentials, err := base64.StdEncoding.DecodeString(base64Credentials) if err != nil { return "" } // Extract the token (the username part of user:password) parts := strings.SplitN(string(credentials), ":", 2) return parts[0] } // Validate the client's authentication token func isValidClientToken(token string) bool { // Implement your authentication logic here // This could check against a database, validate a JWT, etc. return strings.HasPrefix(token, "client-api-key-") } 3. Create a function to forward events to Customer.io You’ll need to capture requests so that you can forward them to Customer.io. Node.js Node.js async function forwardEventToCio(event) { // Verify the event type is a valid method if (typeof cioAnalytics[event.type] !== 'function') { throw new Error(`Unknown event type: ${event.type}`); } // Forward the event to Customer.io await cioAnalytics[event.type](event); } Python Python def forward_event_to_cio(event): event_type = event.get('type') # Map event types to their handler methods event_handlers = { 'identify': lambda: cio_analytics.identify( user_id=event.get('userId'), traits=event.get('traits', {}), timestamp=event.get('timestamp'), context=event.get('context') ), 'track': lambda: cio_analytics.track( user_id=event.get('userId'), event=event.get('event'), properties=event.get('properties', {}), timestamp=event.get('timestamp'), context=event.get('context') ), 'page': lambda: cio_analytics.page( user_id=event.get('userId'), name=event.get('name'), properties=event.get('properties', {}), timestamp=event.get('timestamp'), context=event.get('context') ), 'screen': lambda: cio_analytics.screen( user_id=event.get('userId'), name=event.get('name'), properties=event.get('properties', {}), timestamp=event.get('timestamp'), context=event.get('context') ), 'group': lambda: cio_analytics.group( user_id=event.get('userId'), group_id=event.get('groupId'), traits=event.get('traits', {}), timestamp=event.get('timestamp'), context=event.get('context') ) } handler = event_handlers.get(event_type) if not handler: raise ValueError(f"Unknown event type: {event_type}") handler() Go Go func forwardEventToCio(event map[string]interface{}) error { eventType, ok := event["type"].(string) if !ok { return fmt.Errorf("invalid event type") } // Map event types to their handler functions eventHandlers := map[string]func() error{ "identify": func() error { return cioAnalytics.Identify(&analytics.Identify{ UserId: event["userId"].(string), Traits: event["traits"].(map[string]interface{}), Timestamp: event["timestamp"], Context: event["context"], }) }, "track": func() error { return cioAnalytics.Track(&analytics.Track{ UserId: event["userId"].(string), Event: event["event"].(string), Properties: event["properties"].(map[string]interface{}), Timestamp: event["timestamp"], Context: event["context"], }) }, "page": func() error { return cioAnalytics.Page(&analytics.Page{ UserId: event["userId"].(string), Name: event["name"].(string), Properties: event["properties"].(map[string]interface{}), Timestamp: event["timestamp"], Context: event["context"], }) }, "screen": func() error { return cioAnalytics.Screen(&analytics.Screen{ UserId: event["userId"].(string), Name: event["name"].(string), Properties: event["properties"].(map[string]interface{}), Timestamp: event["timestamp"], Context: event["context"], }) }, "group": func() error { return cioAnalytics.Group(&analytics.Group{ UserId: event["userId"].(string), GroupId: event["groupId"].(string), Traits: event["traits"].(map[string]interface{}), Timestamp: event["timestamp"], Context: event["context"], }) }, } handler, exists := eventHandlers[eventType] if !exists { return fmt.Errorf("unknown event type: %s", eventType) } return handler() } 4. Handle incoming requests Set up request handlers that tie everything together—extract credentials, validate requests, and forward requests to Customer.io. Node.js Node.js app.use((req, res) => { let body = ''; req.on('data', chunk => body += chunk.toString()); req.on('end', async () => { try { // Extract and validate authentication token const clientToken = extractClientToken(req); if (!isValidClientToken(clientToken)) { return res.status(401).json({ error: 'Unauthorized' }); } // Parse the request body const payload = JSON.parse(body); // Forward to Customer.io if (req.url === '/v1/batch') { // Forward batch events for (const event of payload.batch) { await forwardEventToCio(event); } } else { // Handle other endpoints as needed await forwardEventToCio(payload); } res.status(200).json({ success: true }); } catch (error) { console.error('Error processing request:', error); res.status(500).json({ error: 'Internal server error' }); } }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Proxy server listening on port ${PORT}`); }); Python Python @app.route('/<path:path>', methods=['POST']) def proxy_request(path): try: # Extract and validate authentication token client_token = extract_client_token(request) if not is_valid_client_token(client_token): return jsonify({'error': 'Unauthorized'}), 401 # Parse the request body payload = request.get_json() # Forward to Customer.io if path == 'v1/batch': # Forward batch events for event in payload.get('batch', []): forward_event_to_cio(event) else: # Handle other endpoints as needed forward_event_to_cio(payload) return jsonify({'success': True}), 200 except Exception as e: print(f'Error processing request: {e}') return jsonify({'error': 'Internal server error'}), 500 if __name__ == '__main__': port = int(os.environ.get('PORT', 3000)) app.run(host='0.0.0.0', port=port) Go Go func proxyHandler(w http.ResponseWriter, r *http.Request) { // Extract and validate authentication token clientToken := extractClientToken(r) if !isValidClientToken(clientToken) { http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized) return } // Read the request body body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, `{"error": "Invalid request"}`, http.StatusBadRequest) return } // Parse the request body var payload map[string]interface{} if err := json.Unmarshal(body, &payload); err != nil { http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest) return } // Forward to Customer.io if strings.HasSuffix(r.URL.Path, "/v1/batch") { // Forward batch events batch := payload["batch"].([]interface{}) for _, e := range batch { event := e.(map[string]interface{}) if err := forwardEventToCio(event); err != nil { log.Printf("Error forwarding event: %v", err) } } } else { // Handle other endpoints as needed if err := forwardEventToCio(payload); err != nil { log.Printf("Error forwarding event: %v", err) } } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"success": true}`)) } func main() { http.HandleFunc("/", proxyHandler) port := os.Getenv("PORT") if port == "" { port = "3000" } log.Printf("Proxy server listening on port %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } Testing your proxy To test your proxy implementation, configure a client SDK to send events to your proxy server instead of directly to Customer.io. Make sure you check the following things: Backend logs: Confirm the authentication token was extracted and validated correctly Event forwarding: Verify the request was successfully forwarded to Customer.io Customer.io workspace: Check that the event appears in your Customer.io data Error handling: Test with an invalid auth token to ensure unauthorized requests are rejected. See the Quick Start Guide for your SDK (JavaScript, Android, iOS, React Native, Flutter, Expo) for details on where to pass your auth token. Troubleshooting Events don’t appear in Customer.io Verify your backend receives requests from your client (check logs or the network tab in your browser). Confirm that the authentication token is being extracted and validated correctly by your backend. (Make sure you don’t get a 401 Unauthorized error.) Look for error responses from Customer.io’s API. (Make sure you don’t get 400 Bad Request errors.) Authentication failures Verify the key you use in your client is the what your backend expects. Check that the Authorization header is parsed correctly. Confirm that your authentication logic works as expected. --- ## Customer.io API URL: https://docs.customer.io/integrations/data-in/connections/http-api/ With the Pipelines API (shown in the UI as the Customer.io API source), you can send data from anywhere directly to Customer.io and other downstream integrations without using an SDK. See our API documentation for more information about the Pipelines APIs.  Examples on this page use our US region If you’re in our EU region, you’ll use endpoints beginning with https://cdp-eu.customer.io/v1/ instead. Connect a Customer.io Pipelines API source Go to Integrations and click Add Integration. Find the Customer.io API integration. Give the integration a name. The name helps you find and differentiate between different API credentials; you might name them for users, environments, or the services you use them for. Use the key to send a successful test call. You can’t save your credentials until you’ve sent a successful test: curl --request POST \ --url https://cdp.customer.io/v1/identify \ --header 'Authorization: Basic <your key here>'\ --header 'content-type: application/json' \ -d ' { "userId": "97980cfea0067", "traits": { "name": "Cool Person", "email": "cool.person@example.com" } }' Click Complete Setup. After you’ve added your source, you can start making your own calls and add a destination to work with your new source. Enable automatic geolocation support You can automatically geolocate people when you identify them and pass their IP addresses in the context.ip field in your identify requests. This helps you gather information about your audience’s location and time zone so you can schedule messages at the right times or send messages relevant to their communities. If you’ve already set up your integration to capture IP addresses, and you’ve enabled the workspace-level Automatic Geolocation Data Collection setting, you can enable geolocation for your integration. After you enable the Customer.io API source, go to your integration’s Settings tab and turn on the Enable Geolocation setting.  Make sure you capture your users’ IP addresses If you don’t set the context.ip in your requests, we won’t be able to capture geolocation data for your users. Identify The identify method tells us who the current website visitor is, and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. You should call identify when a user creates an account, logs in, etc. You can also call it again whenever a person’s traits change. We’ve shown a typical call with a traits object, but we’ve listed all the fields available in an identify call below. You can send an identify call with an anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data sent in previous anonymous page, track, and identify calls with the person you identify by userId. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Track The track method tells us about actions people take—the events people perform—on your site. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. You can send events with an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. Track calls require an event name describing what a person did. And they generally include a series of properties, providing additional information about the event. Beyond that, we’ve provided a complete schema for writable event fields below, and you can find more information in our API documentation. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Page The Page method records page views on your website, along with optional extra information about the page a person visited. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Group The Group method associates an identified person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Group calls are useful for integrations where you maintain relationships between people and larger organizations, like in Customer.io! In Customer.io Journeys, you can store groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and trigger campaigns based on a person’s relationship to an object—like an account, online class, and so on. Find more details about group, including the group payload, in our API spec.  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## Journeys Message Metrics URL: https://docs.customer.io/integrations/data-in/connections/cio-journeys/ How it works Customer.io automatically captures events relating to message activity—the messages you send, whether or not people open them, when people click them, and so on. So when you look at your integrations, you’ll see Customer.io (Workspaces) as a data source that you can connect to other services. Treating incoming metrics as a source lets you send data to destinations that help you aggregate and analyze audience data, so you follow and chart how your audience reacts to different message channels in your marketing stack. And understanding our payload format helps you accurately map metrics from Customer.io to other platforms you might use to analyze your audience data. sequenceDiagram actor c as User participant b as Customer.io participant a as Integration b->>b: Create draft b->>a: Email Drafted metric b->>c: Send email b->>b: Record Email Sent metric b-->>a: Email Sent metric c->>b: User opens email b->>b: Record Email Opened metric b-->>a: Email Opened metric What metrics do we send out of Customer.io? See our semantic events specification for a complete list of the events, and properties, we send out of Customer.io. Customer.io captures and sends the following semantic events downstream to your connected destinations: Channel Events Email Email Drafted, Email Attempted, Email Sent, Email Delivered, Email Opened, Email Link Clicked, Email Converted, Email Unsubscribed, Email Bounced, Email Suppressed, Email Marked as Spam, Email Failed Push Push Drafted, Push Attempted, Push Sent, Push Delivered, Push Opened, Push Link Clicked, Push Converted, Push Bounced, Push Suppressed, Push Failed SMS SMS Drafted, SMS Attempted, SMS Sent, SMS Delivered, SMS Link Clicked, SMS Converted, SMS Bounced, SMS Failed In-App In-App Drafted, In-App Attempted, In-App Sent, In-App Opened, In-App Clicked, In-App Converted, In-App Failed Webhook Webhook Drafted, Webhook Attempted, Webhook Sent, Webhook Link Clicked, Webhook Converted, Webhook Failed Slack Slack Drafted, Slack Attempted, Slack Sent, Slack Link Clicked, Slack Failed Subscription Subscribed, Unsubscribed, Subscription Preferences Changed Metric Events Customer.io metrics are sent as semantic track events—they look like any event you’d send to Customer.io using the track method, but they have a specific event name that combines the channel and metric—like Email Opened or Push Sent. The properties object contains information about the customer, the message (delivery_id), the campaign, broadcast, or transactional ID the message originated from, and so on.  If you’re familiar with our reporting webhooks, you’ll notice this format is different. We send events to external platforms in a standardized track event format rather than the webhook payload format you might subscribe to directly from Customer.io. Event Format All metric events follow this structure: Field Type Required Description type string Yes Always track for metric events event string Yes The event name (channel + metric, e.g., “Email Opened”, “Push Sent”) userId string No The person’s identifier based on your workspace identifier type anonymousId string No For multi-identifier workspaces, contains the person’s email messageId string Yes Unique identifier for this event (from webhook event_id) timestamp string (ISO 8601) Yes When the event occurred sentAt string (ISO 8601) Yes When the event was sent to the destination receivedAt string (ISO 8601) No When the destination received the event originalTimestamp string (ISO 8601) No Original event timestamp context object No Contextual information (may include person traits like email) properties object Yes Event-specific properties (varies by channel and metric) Available Events We send track events for all message lifecycle metrics across different channels: Email: Email Drafted, Email Attempted, Email Sent, Email Delivered, Email Opened, Email Link Clicked, Email Converted, Email Unsubscribed, Email Bounced, Email Suppressed, Email Marked as Spam, Email Failed Push: Push Drafted, Push Attempted, Push Sent, Push Delivered, Push Opened, Push Link Clicked, Push Converted, Push Bounced, Push Suppressed, Push Failed SMS: SMS Drafted, SMS Attempted, SMS Sent, SMS Delivered, SMS Link Clicked, SMS Converted, SMS Bounced, SMS Failed In-App: In-App Drafted, In-App Attempted, In-App Sent, In-App Opened, In-App Clicked, In-App Converted, In-App Failed Webhook: Webhook Drafted, Webhook Attempted, Webhook Sent, Webhook Link Clicked, Webhook Converted, Webhook Failed Slack: Slack Drafted, Slack Attempted, Slack Sent, Slack Link Clicked, Slack Failed Subscription: Subscribed, Unsubscribed, Subscription Preferences Changed Common Properties Most metric events include these common properties in the properties object: Property Type Description customer_id string The person’s customer ID delivery_id string Unique identifier for the message delivery action_id integer The action ID that sent the message (for campaigns and workflows) journey_id integer The workflow ID that sent the message parent_action_id integer The parent action ID (for split actions) campaign_id integer The campaign ID that sent the message broadcast_id integer The broadcast ID that sent the message newsletter_id integer The newsletter ID that sent the message transactional_message_id integer The transactional message ID that sent the message content_id integer The content ID of the message trigger_id integer The trigger ID for API-triggered broadcasts trigger_event_id string The event ID that triggered the campaign userId string A copy of the user identifier in the properties object Example: Email Opened Event { "type": "track", "event": "Email Opened", "userId": "12345", "messageId": "01H8F21G9KK14JKX233RDFJCNM", "timestamp": "2023-08-22T16:42:55.003Z", "sentAt": "2023-08-22T16:42:57.739Z", "context": { "traits": { "email": "cool.person@example.com" } }, "properties": { "customer_id": "12345", "delivery_id": "RKK4AwUAAYoeIC5PHMYd6-vTAYhWkQ==", "recipient": "cool.person@example.com", "subject": "Password reset request", "transactional_message_id": 8, "userId": "12345" }, "receivedAt": "2023-08-22T16:42:57.742Z", "originalTimestamp": "2023-08-22T16:42:55.000Z" } Example: Push Sent Event Push events include a recipients array that contains information about each device the push was sent to: { "type": "track", "event": "Push Sent", "userId": "12345", "messageId": "01H8F21G9KK14JKX233RDFJCNM", "timestamp": "2023-08-22T16:42:55.003Z", "sentAt": "2023-08-22T16:42:57.739Z", "context": { "traits": {} }, "properties": { "customer_id": "12345", "delivery_id": "dgS_ugcCBYUGhAYBlNIe87ANFfGw2QrqUUY1", "content_id": 1, "newsletter_id": 1, "recipients": [ { "device_id": "123", "device_platform": "android" } ], "userId": "12345" }, "receivedAt": "2023-08-22T16:42:57.742Z", "originalTimestamp": "2023-08-22T16:42:55.000Z" } --- ## Mobile App Sources URL: https://docs.customer.io/integrations/data-in/connections/mobile/ Unlike most other sources, our mobile sources aren't just a way to pass data from one platform to another. They support push notifications and in-app messages from Customer.io Journeys! How mobile SDKs work When you integrate our mobile SDK into your app, you’ll gain support for push notifications, in-app messages, and the ability to send your app data to both Customer.io and downstream integrations like your analytics platform. We currently have SDKs for iOS (Swift), Android, React Native, Expo, and Flutter. flowchart LR a(Your app)-->|User activity (identify, track, etc)|b((Customer.io)) b-.->d(Your analytics platform) b-.->e(Your CRM) Minimum versions Our SDKs became “integration sources” in fairly recent releases. If you’re on a release older than the ones listed below, you’ll need to upgrade to take advantage of all our integration features—like support for anonymous activity. Platform Minimum “source” version iOS (Swift) 3 Android 4 React Native 4 Flutter 2 Expo 2 Add a mobile integration To support our mobile SDKs, you’ll first need to create your integration in Customer.io. This generates a set of API credentials you’ll use to initialize the SDK and communicate with Customer.io. Go to Integrations and click Add Integration. Select your mobile platform. Enter a Name for your integration, like “My Android App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Now the Integrations page shows that your integration is connected to your workspace. You can also connect your integration to other services if you want to send your mobile data to other places outside of Customer.io—like your analytics provider, data warehouse, or CRM. Now you’re ready to integrate the SDK into your app! See our SDK developer documentation for help with the integration process. --- ## Get started URL: https://docs.customer.io/integrations/data-in/connections/forms/connected-forms/ Our form integrations automatically add people and trigger campaigns when they submit your forms. This makes it easy to capture and respond to leads, people who provide feedback, and more. How it works Our form integrations identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. the form submitter and assign them attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. based on the values they provide in your form. You can map form fields to attributes when you set up your form. Form submissions also act as eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. that can trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria., making it easy to respond to your audience automatically when they submit your forms! flowchart LR a(Person submits form) a-->b{Is person new?} b-->|yes|d(Identify person) b-.->|no|c(Update person) a--->|Can trigger follow-up campaigns|e(form_submit event) Supported form providers We directly integrate with Jotform and Typeform. But you can use our custom forms JavaScript snippet to support virtually any form provider. We’ve tested our custom forms JavaScript snippet and our form scanning service with the following services below, but it may be compatible with other services. As long as your form fields include a name attribute, we should be able to scan your form and capture form submissions using our custom forms JavaScript snippet. Jotform Typeform Formstack Instapage landing page forms Netlify forms Squarespace Form Blocks and Newsletter Blocks Unbounce landing pages (not pop-ups or sticky bars) Webflow Wordpress forms Your own HTML forms We can also support virtually any form provider when you integrate directly with the /forms API or with custom form attributes. In general, we suggest that you use our /forms API for backend integrations—anytime you want to capture submissions outside of our client-side JavaScript snippet. General setup process The process changes slightly depending on the form provider. See the individual provider for detailed instructions. But in general, you’ll: Jotform / Typeform Other providers Copy an endpoint from Customer.io to your form provider. Send a test submission of your form. Update the mapping of form fields to attributes. Give Customer.io the URL of a page containing your form. Update the mapping of form fields to attributes. Add the custom forms JavaScript snippet to your site. Test a form submission and make sure that it identifies a person. Scanning forms When you set up an integration with a form provider, we’ll scan your forms to identify form fields and attempt to map them to attributes. But the fields we capture from your form may not match the attributes you already use in your workspace. You may need to update mappings to ensure that form submissions use the right attribute names! For example, the Jotform integration in the screenshot below shows q4_name captured as a q4_name attribute. But you may want to map that to a more common name instead. To update attribute mappings, you’ll simply click Edit and select the attribute you want to map the form field to. Using the same form on multiple pages If you have a form with the same fields on multiple pages, you only need to scan one URL; we capture submissions from all pages under the same form in Customer.io. When you scan a form, we identify the form based on the fields contained in the form. Each field on the form is identified by its name, or, for textarea elements without names, we assign them a name in the format textarea_1. When you integrate directly with our API, a unique form is determined entirely by the form_id. If you submit a form with the same fields on different pages, you can submit them with the same form_id to treat them as the same form, or set different form_id values to differentiate between your forms. Troubleshooting: form scanning errors When scanning forms, you may run into some errors. In most cases, an error means that you’re trying to scan a form we don’t support, or you’re trying to scan a form that you’ve already scanned on another page. No forms found under this address In general, this means that the URL either does not contain a form, or contains a form we don’t support. This error appears when: The URL contains forms we don’t support yet: like HubSpot forms, etc. You’ll find a list of the form providers we currently support here. The URL contains JavaScript-rendered forms. We do not yet support pure JavaScript forms. The page contains <div>-based forms. Forms must use the <form> tag. Form with same form fields already exists You’ve scanned this form on another page, and you do not need to scan it again. We identify forms based on their visible fields; if a form with identical, visible fields exists on multiple pages, we’ll recognize it on any page you submit it from. If you need to differentiate between the same form on different pages, you can add a hidden field to the form that represents the page it’s submitted on. We’ll pass this data in Customer.io, and you can use it to filter people in or out of campaigns based on your form. Form submission events in the Activity Log Each form submission captured by the custom forms JavaScript snippet or the API is recorded as a form_submit event. You can see these events in the Activity Log. Click an event to see the form_id and values that a person submitted. --- ## Formstack URL: https://docs.customer.io/integrations/data-in/connections/forms/formstack/ You can connect forms to your Customer.io workspace to automatically add people and trigger campaigns when they submit your forms. This makes it easy to capture and respond to leads. How it works Formstack is a form builder that lets you create forms and add them to your website. This integration lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously., update, and trigger events for people who submit your forms. When someone submits a form, we’ll automatically identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them and update their attributes. We also collect the submission as an event that you can use to trigger campaigns and respond to people who submit your form. To connect your form to Customer.io: Scan your form: Link us to a page containing your form, so we can read the form fields and map them to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. You’ll need to scan each form you want to connect to Customer.io. Install the custom forms JavaScript snippet: This is a different snippet than our other JavaScript integrations; you only need to install the snippet once. Test your implementation: Submit your form with a test profile—a profile that doesn’t represent a real person—to make sure that your fields map correctly before you send real submissions. 1. Scan your Formstack form You’ll give us the URL of a page containing your form. We’ll scan the page and map the form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. While we scan your form to map fields to attributes, this process doesn’t capture form submissions. You’ll need to install the custom forms JavaScript snippet to complete the setup process. Formstack doesn’t assign friendly name attributes to fields. So, after you scan your form, you’ll need to update the names of your fields in Customer.io to map to human-readable attributes. When you’re done scanning your form, see Map Formstack fields to attributes for more information.  You only need to scan a form once If you use the same form on multiple pages, we’ll treat them as the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Directory and select the Forms integration. Select Custom and click Connect form. Enter the URL of your form (or a page containing your form) and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. Re-map fields to attributes. Formstack fields appear as in the format field113891598, typically from top-to-bottom, left-to-right as you read your form. You’ll need to inspect your form to determine which name attribute corresponds to the field label, so that you can accurately update fields in Customer.io. See Map Formstack fields to attributes for more information. If you haven’t already, Install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. Map Formstack fields to attributes We use the name attribute from form fields when we scan your form, but Formstack does not assign a friendly name attribute to fields. Instead, Formstack assigns a numeric identifier value to the name attribute in each field in the format field113891598, which you’ll probably want to update to a human-readable attribute name. To update form attributes in Customer.io, you need to know which name attribute corresponds to the field label, so that you can update field/attribute names in Customer.io. When you scan a form, fields generally appear in Customer.io from top-to-bottom, left-to-right. To be sure that you map fields correctly, you can find the names of your fields by examining the HTML on your form page, or in Formstack by going to Forms, selecting your form, and then clicking the field you want to find the identifier for. The URL updates with each field you select; the final segment in the URL path represents the field identifier (in the format field<identifer>).  Test a form submission to check your Formstack mappings Because Formstack fields don’t have friendly names, we strongly recommend that you test a form submission to make sure that your fields map to the correct attributes. After you map your form and install the custom forms JavaScript snippet, submit a form and find the person in the People tab in Customer.io. If that person doesn’t have the correct attributes, you can update the field/attribute names in Customer.io to match the Formstack field names. 2. Add the custom forms JavaScript snippet to your site In Formstack, you add JavaScript to a theme. You cannot add a script to the default theme. You either need to create a new theme or copy an existing one so that you can edit it. The custom forms JavaScript snippet is not the same as our basic JavaScript snippet. You must install the Connected forms JavaScript snippet to send data from scanned forms back to Customer.io. Because this snippet is different from our other JavaScript integrations, you might want to create a new set of credentials specifically for your forms integration. To find and install this snippet: Go to the Settings tab of your Forms integration. If you’re not already on the Forms integration, go to Integrations > Connections and select your Forms integration. Click JS Snippet, select the credentials you want to use for your forms integration, and click Copy. In Formstack, go to the form containing your theme. Click Style and then click Advanced Code Editor. Paste the custom forms JavaScript snippet into the Header HTML or Footer HTML; it doesn’t matter which. Click Save Changes. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information. --- ## Jotform URL: https://docs.customer.io/integrations/data-in/connections/forms/jotform/ Connect Jotforms to your workspace to automatically add people and trigger campaigns when they submit your form. Our Jotform integration relies on a webhook from Jotform. How it works When you set up a Jotform integration, we’ll give you a personalized Webhook URL. You’ll provide this webhook URL to Jotform. Whenever someone submits your form, Jotform calls this webhook to send the form submission data to Customer.io. The form submission creates a person in your workspace if they do not already exist, or updates them if they do. In both cases, we map form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each person who fills out your form. We capture the first e-mail field on your form as the email attribute in Customer.io. For other fields, we use the internal input ID as the attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. name. Jotform typically prefixes field IDs with q<x>_ where x is the order that the field was added to the form. So, for example, if the first field on your form is an e-mail field, it will appear in Customer.io as a q1_email attribute by default—though you can map form fields to different attributes after your first form submission. sequenceDiagram participant A as Person participant B as Jotform participant C as Customer.io A->>B: Submits form B->>C: form_submit event rect rgb(229, 254, 249) C-->>C: First form submission: Map form fields to attributes end C->>C: Create or update person C-->>A: Optional response campaign ("Thanks for your response!") You can also trigger campaigns when people submit your form. This provides a handy way to automate personalized responses. You can personalize responses with 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}}. using either customer attributes that you assign from form submissions ({{customer.<attribute>}}) or form fields from the form_submit event (in the format {{event.<form_field_id>}})! Connect Jotform to your workspace As a part of this process, you’ll add a webhook to your Jotform. Then, you’ll can preview and fill out your form to test your webhook-enabled form. When your test data appears in Customer.io, you’ll map form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. Go to Integrations and select Jotform. Click Copy to get your webhook URL. In Jotform, go to your form’s Settings, click Integrations and find the Webhooks integration. Paste the URL you copied in previous steps and click Complete Integration. Return to your Jotform’s Build page and click Preview Form. You can either fill out your form or click Fill Form to send test data to Customer.io. Return to Customer.io and see how your form mapped to your workspace. From here, you can re-map fields to attributes before you deploy your form to a wider audience. Map form fields to attributes The first time you submit a form, we map Jotform fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io using each form field’s internal ID. You’ll almost certainly want to re-map form fields to more apt attribute names. On your first submission, we use the internal input ID as the attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. name. Jotform typically prefixes field IDs with q<x>_ where x is the order that the field was added to the form. So, for example, if the first field on your form is an e-mail field, it will appear in Customer.io as a q1_email attribute by default—though you can map form fields to different attributes after your first form submission. If you haven’t already made a test submission for your form, go to your Jotform’s Build page and click Preview Form. You can either fill out your form or click Fill Form to send test data to Customer.io. When you go to map fields in Customer.io, you’ll see a Preview attribute. You can ignore this; it only appears when you fill out your form in the Preview mode. In Customer.io, go to Integrations > Forms and click your form. If your form doesn’t appear on this page, try refreshing the page or clearing your browser cache. Click Edit under Mapped Fields* to set the attributes for each form field. In each field, you can select an existing attribute or type in a new one. Click Save when you’re done.  Tests can create or update people in Customer.io Whenever you fill out your Jotform, even in Preview mode, it will create or update a person with test data in your workspace using the ID or email in the test request. You can use this person as a reference as you set up your form, but you may want to delete this person when you’re done. Removing attributes from your form When you remove a field from your Jotform, we’ll stop capturing it in Customer.io automatically. You can’t remove a field from your form, or otherwise disable it, in Customer.io. Complex field types and unsupported fields Some Jotform fields return JSON objects. For example, the Name field produces an object with two values: first and last. When we encounter a field like this, we convert it to a string. So, if you used the Name field on your form and someone named Cool Person filled in your form, the result in Customer.io is an attribute called q<x>_name with a value of Cool Person. If you want to split complex values, like names, into different attributes—like, first_name and last_name—you can enter your form respondents into a campaign and use Create or update person actions to split your Jotform name into first_name and last_name. We do not support the following Jotform field types: File Upload, Product List, Signature or Captcha. Find recent Jotform submissions Go to Integrations > Forms. Click and select Manage Form for your Jotform. The Submissions card shows recent submissions. Click a submission to see details for that form. --- ## Squarespace URL: https://docs.customer.io/integrations/data-in/connections/forms/squarespace/ You can connect forms to your Customer.io workspace to automatically add people and trigger campaigns when they submit forms on your Squarespace site. This makes it easy to capture and respond to leads. How it works Squarespace is a website builder that lets you create websites and add forms to your site. This integration lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously., update, and trigger events for people who submit your forms on your Squarespace site. When someone submits a form, we’ll automatically identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them and update their attributes. We also collect the submission as an event that you can use to trigger campaigns and respond to people who submit your form. To connect your form to Customer.io: Scan your form: Link us to a page containing your form, so we can read the form fields and map them to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. You’ll need to scan each form you want to connect to Customer.io. Install the custom forms JavaScript snippet: This is a different snippet than our other JavaScript integrations; you only need to install the snippet once. Test your implementation: Submit your form with a test profile—a profile that doesn’t represent a real person—to make sure that your fields map correctly before you send real submissions. 1. Scan your form You’ll give us the URL of a page containing your form. We’ll scan the page and map the form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. While we scan your form to map fields to attributes, this process doesn’t capture form submissions. You’ll need to install the custom forms JavaScript snippet to complete the setup process.  You only need to scan a form once If you have the same form on multiple pages, we’ll treat submissions as if they’re all from the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Directory and select the Forms integration. Select Custom and click Connect form. Enter the URL of your form (or a page containing your form) and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. (Optional) Re-map fields to attributes. We automatically map the name attribute of fields to attributes in Customer.io. But you might change mappings to better match the format of attributes in your workspace. For example, if a field name comes through as first_name but you camel-case your attributes, you might change it to firstName. If you haven’t already, Install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. 2. Add the custom forms JavaScript snippet to your site You’ll add the custom forms JavaScript snippet to Squarespace pages using the code injection feature. You can inject code at the page or site levels. If you only have forms on a few pages, you probably want to inject the snippet at the page level. If you have forms on most or all of your pages, you might want to inject the snippet at the site level. The custom forms JavaScript snippet is not the same as our basic JavaScript snippet. Even if you’ve installed one of our other JavaScript integrations, you must install the Custom forms JavaScript snippet to take advantage of this integration. To find and install this snippet: Go to the Settings tab of your Forms integration. If you’re not already on the Forms integration, go to Integrations > Connections and select your Forms integration. Click JS Snippet, select the credentials you want to use for your forms integration, and click Copy. Because this snippet is different from our other JavaScript integrations, you might want to create a new set of credentials specifically for your forms! In Squarespace, go to the page or site that you want to add the snippet to. Click Settings > Advanced. Paste the custom forms JavaScript snippet in the header or body sections of the page. Click Save. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information. Squarespace-specific behavior When we scan or capture submissions from Squarespace forms, we handle a few things automatically: Textarea elements: Squarespace does not let you assign a name to <textarea> elements, so we assign them names in the format textarea_1, with ascending numbers for each text area element in your form. Honeypot fields: Squarespace adds anti-spam honeypot fields to forms with names matching message-yui_*-field. We automatically exclude these fields from form scanning and submissions, similar to how we exclude hidden fields. You don’t need to do anything to filter them out. --- ## Typeform URL: https://docs.customer.io/integrations/data-in/connections/forms/typeform/ Connect Typeforms to your workspace to automatically add people and trigger campaigns when they submit your form. Our typeform integration relies on a webhook from Typeform. How it works When you set up a Typeform integration, we’ll give you a personalized Webhook URL. You’ll provide this webhook URL to Typeform. Whenever someone submits your form, Typeform sends the form submission data to Customer.io. The form submission creates a person in your workspace if they do not already exist, or updates them if they do. In both cases, we map form fields to attributes for each person. We capture the first field on your form with an type="email" as the email attribute in Customer.io. For other fields, we convert the label to an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.—limited to 40 characters and snake. For example, a field labeled “Form Field” is converted to a form_field attribute. After your first form/webhook submission, you can re-map form fields to attributes in Customer.io. You can also trigger campaigns when people submit your form. This provides a handy way to automate personalized responses. You can personalize responses with 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}}. using either customer attributes that you assign from form submissions ({{customer.<attribute>}}) or form fields from the form_submit event (in the format {{event.<form_field_id>}})! sequenceDiagram participant A as Person participant B as Typeform participant C as Customer.io A->>B: Submits form B->>C: form_submit event rect rgb(229, 254, 249) C-->>C: First form submission: Map form fields to attributes end C->>C: Create or update person C-->>A: Optional response campaign ("Thanks for your response!") Connect Typeform to your workspace As a part of this process, you’ll add a webhook to your Typeform. After someone submits your webhook-enabled form, it’ll appear in your workspace on the Integrations > Forms page. Then you’ll be able to map form fields to attributes. Go to Integrations > Forms. Click Connect form and select Typeform. Click Copy to get your webhook URL. In Typeform, go to your form > Connect > Webhooks, and click Add a webhook. Paste the URL you copied in previous steps and click Save webhook. By default, Typeform webhooks are OFF. Make sure you turn your Webhook ON. Now, Customer.io is set up to capture submissions from your Typeform. However, we suggest that you perform a test submission so that you can see how your form’s fields are mapped to attributes in Customer.io. This gives you the opportunity to re-map fields to attributes before you deploy your form to a wide audience. Map form fields to attributes When you first submit a form, we map Typeform field labels to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. Friendly field labels may not map perfectly to your attributes. For example, we convert field labeled “What is your name” to a what_is_your_name attribute by default. If your fields don’t map appropriately to your attributes, you can re-map them after someone submits your form for the first time. If you haven’t already, perform a test submission. In your Typeform, go to Connect > Webhooks, click View deliveries, and then click Send test request. In Customer.io, go to Integrations > Forms and refresh the page. If your form doesn’t appear, try clearing your browser cache. Click and select Manage Form. Click Edit under Mapped Fields* to set the attributes for each form field. In each field, you can select an existing attribute or type in a new one. Click Save when you’re done. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information.  Tests can create or update people in Customer.io Whenever you send a test, it will create or update a person with test data in your workspace using the ID or email in the test request. You can use this person as a reference as you set up your form, but you should delete this person when you’re done. Find recent Typeform submissions Go to Integrations > Forms. Click and select Manage Form for your Typeform. The Submissions card shows recent submissions. Click a submission to see details for that form. --- ## Unbounce URL: https://docs.customer.io/integrations/data-in/connections/forms/unbounce/ You can connect forms to your Customer.io workspace to automatically add people and trigger campaigns when they submit forms on your Unbounce site. This makes it easy to capture and respond to leads. How it works Unbounce is a website builder that lets you create websites and add forms to your site. This integration lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously., update, and trigger events for people who submit your forms on your Unbounce site. This integration does not work with forms in Unbounce popups or sticky bars. When someone submits a form, we’ll automatically identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them and update their attributes. We also collect the submission as an event that you can use to trigger campaigns and respond to people who submit your form. To connect your form to Customer.io: Scan your form: Link us to a page containing your form, so we can read the form fields and map them to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. You’ll need to scan each form you want to connect to Customer.io. Install the custom forms JavaScript snippet: This is a different snippet than our other JavaScript integrations; you only need to install the snippet once. Test your implementation: Submit your form with a test profile—a profile that doesn’t represent a real person—to make sure that your fields map correctly before you send real submissions. 1. Scan your form You’ll give us the URL of a page containing your form. We’ll scan the page and map the form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. While we scan your form to map fields to attributes, this process doesn’t capture form submissions. You’ll need to install the custom forms JavaScript snippet to complete the setup process.  You only need to scan a form once If you use the same form on multiple pages, we’ll treat them as the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Directory and select the Forms integration. Select Custom and click Connect form. Enter the URL of your form (or a page containing your form) and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. (Optional) Re-map fields to attributes. We automatically map the name attribute of fields to attributes in Customer.io. But you might change mappings to better match the format of attributes in your workspace. For example, if a field name comes through as first_name but you camel-case your attributes, you might change it to firstName. Now, if you haven’t already, install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. 2. Add the custom forms JavaScript snippet to your site You can add the JavaScript snippet at the domain level or page level, but you should not add it in both places, or you’ll receive duplicate submissions for each form. We also don’t support forms in Unbounce popups or sticky bars. The custom forms JavaScript snippet is not the same as our other JavaScript snippets. Even if you’ve used one of our other JavaScript integrations, you must install the Custom forms JavaScript snippet to take advantage of this integration. To find and install this snippet: Go to the Settings tab of your Forms integration. If you’re not already on the Forms integration, go to Integrations > Connections and select your Forms integration. Click JS Snippet, select the credentials you want to use for your forms integration, and click Copy. Because this snippet is different from our other JavaScript integrations, you might want to create a new set of credentials specifically for your forms! Go to Unbounce and add the snippet to your site. You can add the snippet at the domain level or page level, but you should only add it in one place or you’ll receive duplicate submissions for each form. Domain level: paste the script under Settings > Script Manager. Page level: on the page containing your form, click Javascripts at the bottom of the page and insert the custom forms JavaScript snippet. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information. --- ## Webflow URL: https://docs.customer.io/integrations/data-in/connections/forms/webflow/ You can connect forms to your Customer.io workspace to automatically add people and trigger campaigns when they submit forms on your Webflow site. This makes it easy to capture and respond to leads. How it works Webflow is a website builder that lets you create websites and add forms to your site. This integration lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously., update, and trigger events for people who submit your forms on your Webflow site. You must have a paid Webflow plan to take advantage of this integration. When someone submits a form, we’ll automatically identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them and update their attributes. We also collect the submission as an event that you can use to trigger campaigns and respond to people who submit your form. To connect your form to Customer.io: Scan your form: Link us to a page containing your form, so we can read the form fields and map them to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. You’ll need to scan each form you want to connect to Customer.io. Install the custom forms JavaScript snippet: This is a different snippet than our other JavaScript integrations; you only need to install the snippet once. Test your implementation: Submit your form with a test profile—a profile that doesn’t represent a real person—to make sure that your fields map correctly before you send real submissions. 1. Scan your form You’ll give us the URL of a page containing your form. We’ll scan the page and map the form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. While we scan your form to map fields to attributes, this process doesn’t capture form submissions. You’ll need to install the custom forms JavaScript snippet to complete the setup process.  You only need to scan a form once If you use the same form on multiple pages, we’ll treat them as the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Directory and select the Forms integration. Select Custom and click Connect form. Enter the URL of your form (or a page containing your form) and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. (Optional) Re-map fields to attributes. We automatically map the name attribute of fields to attributes in Customer.io. But you might change mappings to better match the format of attributes in your workspace. For example, if a field name comes through as first_name but you camel-case your attributes, you might change it to firstName. If you haven’t already, Install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. 2. Add the custom forms JavaScript snippet to your site The custom forms JavaScript snippet is not the same as our other JavaScript snippets. Even if you’ve used one of our other JavaScript integrations, you must install the Custom forms JavaScript snippet to take advantage of this integration. To find and install this snippet: Go to the Settings tab of your Forms integration. If you’re not already on the Forms integration, go to Integrations > Connections and select your Forms integration. Click JS Snippet, select the credentials you want to use for your forms integration, and click Copy. Because this snippet is different from our other JavaScript integrations, you might want to create a new set of credentials specifically for your forms! In Webflow, go to Pages. Click for the page you want to install the custom forms JavaScript snippet on. Scroll down to Custom Code and paste the custom forms JavaScript snippet in either the Inside tag or Before tag. It doesn’t matter which. Click Save. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information. --- ## Wordpress with WPForms URL: https://docs.customer.io/integrations/data-in/connections/forms/wordpress-with-wpforms/ You can connect forms to your Customer.io workspace to automatically add people and trigger campaigns when they submit forms on your Wordpress site. This makes it easy to capture and respond to leads. How it works This integration lets you identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously., update, and trigger events for people who submit your forms on your Wordpress site using the WPForms plugin. You may be able to capture form submissions from other Wordpress plugins, but we can only guarantee support for WPForms. Contact us if you need support for a different Wordpress plugin. When someone submits a form, we’ll automatically identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. them and update their attributes. We also collect the submission as an event that you can use to trigger campaigns and respond to people who submit your form. To connect your form to Customer.io: Scan your form: Link us to a page containing your form, so we can read the form fields and map them to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. You’ll need to scan each form you want to connect to Customer.io. Install the custom forms JavaScript snippet: This is a different snippet than our other JavaScript integrations; you only need to install the snippet once. Test your implementation: Submit your form with a test profile—a profile that doesn’t represent a real person—to make sure that your fields map correctly before you send real submissions. 1. Scan your form You’ll give us the URL of a page containing your form. We’ll scan the page and map the form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io. While we scan your form to map fields to attributes, this process doesn’t capture form submissions. You’ll need to install the custom forms JavaScript snippet to complete the setup process.  You only need to scan a form once If you use the same form on multiple pages, we’ll treat them as the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Directory and select the Forms integration. Select Custom and click Connect form. Enter the URL of your form (or a page containing your form) and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. (Optional) Re-map fields to attributes. We automatically map the name attribute of fields to attributes in Customer.io. But you might change mappings to better match the format of attributes in your workspace. For example, if a field name comes through as first_name but you camel-case your attributes, you might change it to firstName. If you haven’t already, Install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. 2. Add the custom forms JavaScript snippet to your site You’ll need to add the JavaScript snippet to pages containing your form. If your form is on a few pages, you can add a custom HTML block below the form and paste the JavaScript snippet into the block. You can add the snippet to your site using Google Tag Manager, the Wordpress Insert Headers and Footers plugin, and so on, but the instructions below are just for the standard custom HTML block. The custom forms JavaScript snippet is not the same as our other JavaScript snippets. Even if you’ve used one of our other JavaScript integrations, you must install the Custom forms JavaScript snippet to take advantage of this integration. To find and install this snippet: Go to the Settings tab of your Forms integration. If you’re not already on the Forms integration, go to Integrations > Connections and select your Forms integration. Click JS Snippet, select the credentials you want to use for your forms integration, and click Copy. Because this snippet is different from our other JavaScript integrations, you might want to create a new set of credentials specifically for your forms! In Wordpress, go to the page containing your form. Add a custom HTML block below the form and paste the JavaScript snippet into the block. Publish the page containing the form. When you publish the page, it’ll begin submitting results to your workspace. Now you can take advantage of form submissions to trigger campaigns, personalize messages, and more. See Use form data in Customer.io for more information. --- ## Custom JS integrations URL: https://docs.customer.io/integrations/data-in/connections/forms/javascript-form-integrations/ Our JavaScript forms snippet automatically adds people and triggers campaigns when people submit your forms. This makes it easy to capture and respond to leads, people who provide feedback, and more—even for form providers that don't have a direct integrations with Customer.io. How it works When someone submits your form, we’ll identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. the form submitter and assign them attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. based on the values they provide in your form. You can map form fields to attributes when you set up your form (normally after the first submission). Form submissions also act as eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. that you can use to trigger campaigns, making it easy to respond to your audience automatically when they submit your forms! For some providers, like Jotform and Typeform, we can directly integrate with their services. For other providers, like Formstack, you can use our custom forms JavaScript snippet—which is different from our other JavaScript integrations—to capture form submissions. Install the custom forms JavaScript snippet To capture form submissions using the custom forms JavaScript snippet, you need to: Scan your form to Customer.io on the Integrations > Forms page. For us to scan your form, it must: Use a <form> tag. We cannot scan <div>-based forms. Not be inside an <iframe>. Have fields with name attributes; name is how we identify non-textarea fields and map them to attributes. Have a field that maps directly to one, and only one identifier—id or email as determined by your workspace settings. Install the custom forms JavaScript snippet on the page(s) containing your form. You can do these things in any order, but you cannot capture form information until you’ve done both. 1. Scan a form To add a form, provide the URL of the page containing your form. Customer.io can scan most forms that use the <form> tag, as long as your form is not inside an <iframe> and your form fields have name attributes. If your form contains <textarea> fields that do not have a name (as with Squarespace), we’ll number them (e.g. textarea_1).  If your form is on multiple pages, you only need to add your form once. If you use the same form on multiple pages, we’ll treat them as the same form (so long as the custom forms JavaScript snippet is installed on all pages containing the form). Go to Integrations > Forms. Select Custom and click Connect form. Enter the URL of your form and click Scan for Forms. Select the form(s) you want to connect to your workspace and click Choose Form. If your page contains multiple forms, you can select each form and set a Name. The Name is how you’ll select forms in the UI. Map Form Fields to Attributes and click Set Up Form. By default, we map the names of each field directly to an attribute. Click to prevent Customer.io from capturing a field. Take care when mapping fields; attribute names and values are case sensitive. If you segment or trigger campaigns based on attribute names and values, you want to make sure that you assign attributes and values correctly. If you haven’t already, Install the custom forms JavaScript snippet. Go to the Settings tab to find the snippet. 2. Add the custom forms JavaScript snippet to your site The custom forms JavaScript snippet is not the same as our basic JavaScript snippet. You must install the Connected forms JavaScript snippet to send data from scanned forms back to Customer.io. Before you can find the custom forms JavaScript snippet, you must have scanned at least one form. To find and install this snippet: Go to Integrations > Forms and click Settings. The Settings tab only appears after you scan your first form. If you haven’t already done so, scan a form. Click JS Snippet. Copy and paste the snippet. Where you paste the snippet may vary based on your form provider. Custom HTML or Netlify Forms: paste the script on the page containing your form. The script can go anywhere in the <head> or <body> tags. Other form providers: See the supported providers below.  You can install both Customer.io JavaScript snippets We have two JavaScript snippets: the basic JavaScript snippet and the Custom forms snippet. The basic snippet identifies and updates people who browse your website, and the custom forms snippet identifies and updates people who submit your form. Form fields and URL parameters captured by the JavaScript snippet The custom forms JavaScript snippet captures both form fields and URL parameters on the page containing your form, but the two work differently: form fields are converted to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. on a person who submits a form; URL parameters are not. Form Fields: When you set up a form in Customer.io, you scan a URL for forms. Customer.io identifies fields in your form by name attributes. After you scan a form, you determine which fields we capture, and map each field to an attribute for people who submit the form. Customer.io also reserves three fields: form_id, form_name, and form_type; if fields on your form (or in your API payload) include these fields, we’ll ignore them. form_id: Assigned by customer.io when you scan the form or through the API in the path. form_name: Assigned when you map your form in Customer.io. form_type: Represents the form’s origin. Using the JavaScript snippet or API, this will always read custom. Other values represent direct integrations with Facebook Lead Ads, Jotform, Typeform, etc. form_url: The URL of the page that a user submitted your form from. form_url_params: An array of URL parameters present when a user submitted your form. URL Parameters: When someone submits a form, we capture URL parameters on the form page, so that you can segment people and filter campaigns based on these values. But, because URL parameters are not mapped to attributes when you connect your form to Customer.io, we cannot apply URL parameters to people as attributes. URL parameters captured in this way appear in the form_url_parameters array, to help you understand which fields come from your form and which come from the page URL; items in this array are not applied to to a person as attributes. If a form field and URL parameter have the same name, the form field will win and we’ll ignore the URL parameter.  Use an attribute change to apply URL parameters to people By default, we only capture URL parameters for segmentation purposes. However, you can use an attribute change event in a form-triggered campaign to convert URL parameters to attributes for people who submit your forms. Mapping identifiers and initial form submission errors If an identifier in your form does not map directly to an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.—i.e. a field capturing email addresses is called something like email_address rather than email in your initial request—you’ll receive a 400, but we’ll still add your form on the Integrations > Forms page. You can then re-map your email_address field to email, and your form will begin working normally. Tested form providers While we have complete setup instructions for a number of other form providers, we’ve we’d like to call out a few specifics for the following providers: Instapage You can install the JavaScript snippet in the head, body, or footer of your Instapage form page. If you use a custom domain (as opposed to the default, pagedemo.co URL), it may take a while before you can begin transmitting data to Customer.io due to DNS propagation issues. Netlify Netlify forms are basically custom HTML forms that include a data-netlify="true" parameter in the <form> element. Include the JavaScript Form Snippet on the page containing your form. It can go anywhere in the <head> or <body> tags. Form submissions will appear both in Netlify and as people in your Customer.io workspace. --- ## Facebook Lead Ads URL: https://docs.customer.io/integrations/data-in/connections/forms/facebook-lead-ads/ When you run Facebook ads, you can include forms to capture information about people who are interested in your products and services. This integration lets you add people who fill out your Facebook lead ad form to your Customer.io workspace, then trigger campaigns so you can convert your Facebook leads to customers or users. Overview To set up a Facebook lead ads integration, you connect a page in your Facebook business account to Customer.io, activate the forms you want to capture leads from, and map fields from your forms to attributes. When a person fills out your Facebook lead ad, Customer.io receives a form_submit event containing the Facebook lead form ID, the form name, and values representing the form submission. This form_submit event adds a person to, or updates a person in, your workspace. You can also use it to trigger campaigns, add people to segments, etc.  Form submissions must include email addresses Your form needs an email field and form submissions must include an email address so we can tie form submissions to people. If people skip the email field in your form, Customer.io cannot use their submissions. Prerequisites To take advantage of this integration: Your Facebook lead ad forms must capture email addresses in a field called email. People cannot skip this field, or we won’t be able to use their submissions. Your workspace must use email address as a unique key to identify people. Customer.io maps email addresses from your form to people—each email address represents a unique person in your workspace. You can map additional fields in your form to attributes in Customer.io. You can connect one Facebook business page per workspace. If you have multiple pages, you’ll need to create additional workspaces to capture leads from each page.  Your workspace must identify people by email to use this integration Most workspaces are already set up to use email as an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. But, if your workspace does not identify people by email address, you can add email as an identifier or create a new workspace to take advantage of this integration. Facebook Permissions and Authentication When you add a form to Customer.io, you’ll authenticate with Facebook and grant Customer.io access to your Facebook business account. This user must have the following permissions: Manage Page Leads Access Business Management (shown as “Manage your business” in the Meta UI) We’ll confirm the pages you want to grant Customer.io access to and the permissions we’re requesting. While you grant Customer.io access to your Facebook business and page, we don’t store your credentials. Instead, we request an access token from Facebook, which we use to authenticate with pages and lead ads. This token will not expire unless you revoke access to Customer.io from your Facebook business account. Set up a Facebook lead ads integration Your workspace must let you identify people by email address, and your form must require people to fill out an email field to use the Facebook lead ads integration. Go to Integrations > Facebook Lead Ads. Click Sign in with Facebook to connect. Enter your Facebook credentials and click Log In. Customer.io now lists your Facebook lead ad forms, but you still need to Activate the forms that you want to use to add people to your workspace and trigger campaigns. Activate a lead ad form When you activate a form, Customer.io begins capturing form_submit events whenever someone fills out the form. This event adds people to, or updates people in, your workspace, and can trigger campaigns. When you activate a form, you map form fields to attributes you want to set on people in Customer.io. Go to Integrations > Facebook Lead Ads. Click Activate for the form you want to activate. Map form fields to attributes in Customer.io. Your form must have a field called email that captures email addresses. (Optional) Click Next to assign or update Added Attributes. These are static attribute values that do not come from form fields and apply to everyone who fills out your form. These attributes are not unique to individual respondents. Click Activate. Manage form attributes You can change the way fields in your active forms map to attributes in Customer.io. Go to Integrations > Facebook Lead Ads. Click the options icon, and select Manage form. Change the Mapped Form Fields or Added Attributes. If you disable an attribute, your form will still capture these values, and they’ll appear in form_submit events, but they won’t be set as attributes on people in Customer.io. Click Save changes. Test form events You can send test form_submit events representing a person who has already filled out your form to test campaigns or broadcasts triggered from Facebook lead ads.  Test events do not affect attributes Test events only test campaigns, messages, etc using the values from your form, but they cannot add or update people in your workspace. If you want to perform an end-to-end test, you can send a test event from the Facebook Lead Ads Testing Tool. To send a test event: Go to Integrations > Facebook Lead Ads. Click the options icon, and select Test form event. Find the person you want your event to represent. This person must already exist in your workspace.  Use a test person If you send a test event for a real member of your audience, you may inadvertently enter that person into a campaign or send them messages. We strongly recommend testing events using your own internal email address or a test address. Fill out test values for the form. Click Send form_submit event. Troubleshooting Lead data retention Facebook retains lead data for 90 days. If your integration is disconnected or your access token is revoked, you won’t be able to add or update leads that were created more than 90 days ago. Test your form If you’re having trouble with your Facebook Lead Ads integration, verify that your app is subscribed and that it captures email addresses in a field called email. Go to https://developers.facebook.com/tools/lead-ads-testing. Select the page and form you want to test from the dropdowns. Click Preview form to verify your app is subscribed or click Create lead to test form submission. Make sure email is a required field If you create your form via Facebook Ads Manager, make sure that the email field is required. If people skip the email field, you won’t add or update them in Customer.io. Customer.io doesn’t know if a field is optional or required, so you’ll need to check this in your Facebook business account. CRM access error (103) If you get an error response 103 - CRM access has been revoked from Lead Access Manager when testing or using your Facebook Lead Ads integration, you need to have a Facebook Admin enable CRM access for the account you use to set up your Facebook Lead Ads integration. Note that only a Facebook Admin can enable CRM access. You can’t do this from a regular user account, even if you have full control over ads and leads. Deactivate a form Deactivating a form prevents it from sending events into Customer.io and capturing new respondents. Deactivating a form does not affect leads that already entered your workspace by filling out the form, nor does it archive a form in your Facebook business account. Go to Integrations > Facebook Lead Ads. Click the options icon for the form you want to modify, and select Deactivate. Type the form name and click Deactivate. Trigger a campaign from a form When you have Active Facebook lead ad forms, you can trigger campaigns whenever someone fills out a form.  Using liquid in form-triggered campaigns In your campaign, you can use liquid representing attributes you set from your form or properties from the form_submit event itself. You might want to use event properties if you want to reference a form field value that you don’t set as an attribute on people who fill out your form. To set up a form-triggered campaign: Go to Campaigns and click Create Campaign or edit an existing campaign. Click They fill out a form and select the Trigger form that causes people to enter the campaign. Finish setting up your campaign as normal. When you review and start your campaign, people who fill out the form on Facebook automatically enter your campaign, helping you automate a conversation with, and hopefully convert, your leads to customers or users! You can see the campaign(s) triggered from your form on the Facebook lead ads integration page. --- ## Use form data in Customer.io URL: https://docs.customer.io/integrations/data-in/connections/forms/forms-in-campaigns/ You can use form submissions to trigger campaigns and the data in each form submission to personalize messages. This helps you talk to users who fill out your forms. How it works You can use form submissions to trigger campaigns and you can personalize messages using form submission data. This helps you talk to users who fill out your forms. Each form submission also comes into Customer.io as an event called form_submit. You can use this event to create segments or complex campaign conditions in campaigns that aren’t necessarily triggered by the original form submission. Trigger a campaign from a form Form submission events can trigger campaigns, so you can start a conversation with people and foster conversions immediately when they fill out your form. You can start a form-triggered campaign by going to Integrations > Forms and clicking Create campaign for the form that you want to trigger your campaign. This automatically populates a campaign trigger with your form name. Or you can create a form-triggered campaign from the Campaigns menu: Go to Campaigns and click Create Campaign. Click Form submission. Choose the form that should trigger your campaign. Continue creating your campaign. After you activate the campaign, each form submission will trigger a campaign.  Use hidden fields to filter campaigns based on form submissions from different pages If your form appears on different pages, and you want to differentiate campaigns based on the page a person is on when they submit your form, you can click Add event data filter and filter for a hidden field that differentiates between your forms on different pages. Use form data in a message When someone submits a form, Customer.io captures both the form fields and any URL parameters from the page. You can access these variables in messages using 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}}.. Form Fields come in as event properties and are mapped to customer attributes. You can reference form fields either way in campaigns that are triggered by form submissions, but the two names can be different! The event property ({{event.form_field_name}}) reflects the original form field name. The customer attribute ({{customer.attribute_name}}) reflects where that field was mapped during setup. For example, if you mapped a form field called msisdn to phone, then you could access the event property as {{event.msisdn}} or the customer attribute at {{customer.phone}}—both represent the same value! You might be able to use either event data or customer data, but there are situations when you should use one over the other: Use customer attributes when: You need to reference form data in campaigns that are not triggered by form submissions. Attributes may also have friendlier names than event properties. Use event data when: You need to reference URL parameters or fields that aren’t mapped to customer attributes. URL parameters are only available as event properties in campaigns triggered by form submissions. For example, if you want to reference a URL parameter from your form page called referred-by, you would use {{event.referred-by}}. --- ## Forms API (backend integrations) URL: https://docs.customer.io/integrations/data-in/connections/forms/forms-api/ You can use the Customer.io Forms API to capture form submissions from any form provider. This makes it easy to capture and respond to leads based on information you receive in your backend. How it works When someone submits your form, we’ll identifyThe Customer.io operation that adds or updates a person. When you identify a person, Customer.io either adds a person if they don’t exist in your workspace, or updates them if they do. Before you identify someone (by their email address or an ID), you can track them anonymously. the form submitter and assign them attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. based on the values they provide in your form. You can map form fields to attributes when you set up your form (normally after the first submission). Form submissions also act as eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. that you can use to trigger campaigns, making it easy to respond to your audience automatically when they submit your forms! Use the Forms API If you don’t want to use our JavaScript snippet, you can write your own backend integration with the Customer.io /forms API. Even if you use our JavaScript snippet, you may want to maintain a backend API integration in case your audience blocks or disables JavaScript. To connect your form to Customer.io, just send a send a POST to https://track.customer.io/api/v1/forms/{form_id}/submit when someone submits your form. The form_id is an arbitrary string value representing your form. If we haven’t seen the form ID before, we create a new form connection (found on the Integrations > Forms page). Requests with the same form ID are treated as submissions from the same form. The payload of a request is a data object that must contain field that maps to an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.—the email or ID of the person filling out the form. If these attributes are titled email or id, we’ll map them automatically. If not, your form will show an error on the Go to Integrations > Forms page after your first submission, and you can re-map map fields to the appropriate identifiers. If the person represented by the identifier in your request does not already exist, we create them. Additional keys in the data object represent form fields and values from the form that a person submitted. By default, we map form fields in your request directly to attributes, e.g. if you have a form field called first_name, we map that field to the first_name attribute. { "data": { // each key represents a form field // form fields map directly to attribute names unless you re-map them "first_name": "Cool Person", "email": "cool.person@example.com", } } After you submit your first /forms request, you can re-map form fields (properties in the data object) to attributes on the Integrations > Forms page.  You cannot disable fields sent to the /forms API While you can map form fields to attributes in our UI, you cannot disable fields sent to the /forms API. If you send data to the /forms API, we’ll apply it to the person represented in the request. Mapping identifiers and initial form submission errors If an identifier in your form does not map directly to an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.—i.e. a field capturing email addresses is called something like email_address rather than email in your initial request—you’ll receive a 400, but we’ll still add your form on the Integrations > Forms page. You can then re-map your email_address field to email, and your form will begin working normally. Submit forms using the action attribute You can assign a POST method and action to your HTML form tag to submit forms directly to Customer.io without using our JavaScript snippet. When you set up a form this way, we generate a URL that you’ll copy directly to your form’s action attribute. Your form must have a success page—a page that you redirect people to when they click Submit. <form method="POST" action="https://customerioforms.com/forms/submit_action?site_id=1ea07cb331d4ed0c39ba&form_id=2d75e032c78a41a&success_url=https://success.example.com/thanks" > <!-- by default, we map input name attributes to Customer.io attributes --> <label for="email_input">E-mail address</label> <input id="email_input" name="email" type="email" /> <label for="job_title">Job Title</label> <input id="job_title" name="job title" type="text" /> <button>Submit</button> </form> By default, we map form fields to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. using the name property. For example, if you have a field with a name property job_title, we’ll set that attribute on people who respond to your form. You can remap fields after your first submission. Your form must contain one field that maps to an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.—the email or ID of the person filling out the form. If your identifier field has a name attribute of id or email, we’ll map it to the appropriate identifier automatically. If your identifier field isn’t named id or email, your form will show an error and you’ll have the opportunity to map your fields after your first submission.  You cannot disable fields sent to the /forms API While you can map form fields to attributes in our UI, you cannot disable fields sent to the /forms API. If you send data to the /forms API, we’ll apply it to the person represented in the request. Set up a form with an action attribute Go to Integrations and select Forms. Select Custom Form, click View other options, and click Action attribute. If your workspace has multiple sets of Track API credentials, select the credentials you want to use for your form. Enter your Success page address URL. This is the page your form sends people to when they click Submit. Copy the HTML snippet and adjust the form to add your form’s fields. Or, Copy the URL to your form’s action attribute and make sure that your form’s method attribute is set to POST. Submit your form as a test. This makes it appear in your Customer.io Forms dashboard. Return to the Integrations > Forms page, and select the form you just added. Make sure that the Mapped Fields reflect the attributes that you want to assign to people who submit your form. If they don’t, click Edit and re-map attributes appropriately. --- ## Edit or disconnect forms URL: https://docs.customer.io/integrations/data-in/connections/forms/edit-disconnect/ You can edit forms to add, remove, or re-map form fields. If you use forms temporarily, you can also disconnect them from your workspace when you're done. How it works You can edit a form at any time to add, remove, or re-map form fields. But changing form fields does not affect form submissions you’ve already received. If you want to keep attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. values consistent, you might need to run a campaign to update attributes. Edit a form You can edit your form to add fields, re-map fields to attributes, or turn off fields you no longer want to capture. Your changes only reflect new respondents. Changing form fields doesn’t change attributes on people who already filled out your form. If you move a form to a new page, you do not need to edit or re-scan your form. You simply need to make sure that the page you move the form to has the custom forms JavaScript snippet. Go to Integrations and select Forms. Select the form you want to edit and click Edit. Add, remove, or re-map form fields. Enable/disable fields: Check and un-check fields to enable and disable them. You cannot disable fields when you use the /forms API or send forms using the action attribute. Add fields: If your form uses the custom forms JavaScript snippet, click Re-scan to update the fields on your form. If your form integrates directly with our API, click Add Field. Edit field names: Change the names of attributes you want to map form fields to. If integrated with the /forms API, you can also change the name of the form field that you want to map. Click Save.  You cannot disable fields when you use the /forms api or an action attribute While you can map form fields to attributes in our UI, you cannot disable fields for forms sent to our API or using the action attribute. If you send data to the /forms API, we’ll apply it to the person represented in the request. Disconnect a form Disconnecting a form prevents Customer.io from collecting form submissions and identifying people who fill out your form. You can reconnect the form later, if you want to stop collecting submissions temporarily. Go to Integrations > Forms. Click and then click Disconnect. Confirm that you want to disconnect the form. Disconnected forms still appear in the forms list. You can find all your disconnected forms by selecting the Disconnected status. --- ## Getting Started URL: https://docs.customer.io/integrations/data-in/connections/javascript/js-source/  Are you already integrated with our classic JavaScript snippet? If you’re already integrated with Customer.io, and your calls are prefixed with _cio, you’re using our classic JavaScript snippet. We recommend that you update to the JavaScript client integration described on this page, so that you can take advantage of our latest features. But, if you don’t want to update, you’ll find documentation for our classic JavaScript snippet here. How it works Our JavaScript client library helps you send data from your website to Customer.io and any downstream integrations where you want to use your data. This is our most popular integration. When people visit your website, you’ll want to identify them. The identify method is fundamental to Customer.io, providing a unique identity for a person that we can use across Customer.io and all the services you integrate with. After you identify a person, subsequent calls reference the identified person for the remainder of the client’s session on your site. You can still generate events before you identify someone. These events are anonymous. When you identify a person, we’ll automatically associate the identified person and their anonymous activity, giving you a complete view of your audience’s activity on your site. Want to learn more? Check out the Pipelines API reference. flowchart LR a(person visits site)-->|generate anonymousId|z subgraph z [anonymous activity] direction TB b(visit a page) b-->|page|c(add item to cart) c-->|track|d(person logs in) end subgraph y [identified activity] direction TB e(person initiates checkout)-->|track|f(person finishes transaction) f-->|track|g(person logs out) end d-->|identify|e g-->|reset|h(person is anonymous again) What can I do with the JavaScript snippet? When you add the JavaScript snippet, it’ll begin tracking anonymous pageviews. This is similar to the way Google Analytics works. However, you’ll need to identify your audience to take advantage of most analytics and marketing tools. You can do that with the identify method described below. This is enough to use most basic CRM functionality—like in Salesforce or Intercom. But, lots of analytics can do more than record identities. You can get the most out of platforms by recording the actions that people perform on your website. that’s what you can do with the track call. Set up your JavaScript Client As a part of this installation process, you’ll either use the JavaScript snippet or import the JavaScript client as a package. Go to Integrations. In the Connections tab, pick the JavaScript integration. Give the integration a Name and click Submit. The name is simply a friendly name to help you find and recognize your integration in Customer.io. Install the JavaScript client. If you use a framework like React, Vue, or Next.js, you might want to import the JavaScript client as a package. See the JavaScript Frameworks guide for more information. Otherwise, copy the Sample Code into the head section of your website. If you’re in our EU region, make sure that t.src contains our EU url (https://cdp-eu.customer.io/v1/analytics-js/snippet/). Now you’re ready to use the snippet to identify people and send events to Customer.io!  Your write key is exposed When you use our JavaScript client, your write key is exposed. This key is only used to send data into Customer.io. But, if you’re concerned about security, you might want to use our Node.js package instead. Install the JavaScript client via a tag manager You can use our JavaScript client with a tag manager like Google Tag Manager. This might be a good option if you don’t have direct access to your website’s codebase. Follow the process above to install the JavaScript client. But, instead of adding the code to your website, you’ll add it as a tag in your tag manager. In general, the tag manager approach is perfectly fine. But, you should understand that a tag manager can lead to additional latency between your client and Customer.io. It can also make it tougher to trace, troubleshoot, and diagnose problems. Framework-specific installation For framework-specific instructions—including React, Next.js, and Vue.js—see our JavaScript Frameworks guide. Identifying Users The identify method tells us who the current website visitor is, and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. You should call identify when a user creates an account, logs in, etc. You can also call it again whenever a person’s traits change. We’ve shown a typical call with a traits object, but we’ve listed all the fields available in an identify call below. You can send an identify call with an anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data sent in previous anonymous page, track, and identify calls with the person you identify by userId. Before you identify someone, their activity is anonymous. We’ll automatically assign unidentified people an anonymousId so you can still send page and track events. But when you identify a person, we’ll automatically associate the identified person with their anonymous activity, so you have a more complete view of your audience’s activity. cioanalytics.identify('f4ca124298', { email: 'cool.person@example.com', first_name: 'cool', last_name: 'person' }); That identifies Cool Person by their unique User ID (f4ca124298—the value you know this person by in your database) and sets their first_name, last_name and email traits. Traits are information that you want to record about your audience in Customer.io and other integrations. In general, we recommend that you inject an identify call into the footer of every page of your site where a user is logged in. That way, no matter what page the user first lands on, they are always identified. Stop identifying people (logout) When someone comes to your website, we’ll assign them an anonymousId. When they log in or provide information you can use to identify them, you’ll use the identify method. This stores their identity to a cookie and local storage, so you’ll recognize them when they come back to your website. When they log out of your website, you’ll call the reset method to reset their identity. This anonymizes their data again, respecting your audience’s right to privacy. Tracking events The track method tells us about actions people take—the events people perform—on your site. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. You can send events with an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. Track calls require an event name describing what a person did. And they generally include a series of properties, providing additional information about the event. Beyond that, we’ve provided a complete schema for writable event fields below, and you can find more information in our API documentation. For example, imagine that you want to send an event when someone begins an online course. You’d send a call that looks something like this: cioanalytics.track('class_started', { title: 'How to use Customer.io', progress: '2%', category: 'getting started' }); In this case, the event is called class_started. That event contains the title, progress and category properties that tell you more information about the class the person started and why. If you’re just getting started, some of the events you should track are events that indicate the success of your site, like Signed Up, Item Purchased or Article Bookmarked. You can get started with just a few events and add more later! Querystring Methods You can trigger an identify and a track method based on query strings in your URL. This can help you execute calls when tracking visitors from email click-throughs, social media clicks, and digital advertising. Parameter Description Triggers ajs_uid The userId for an identify call. This triggers an identify call, letting you automatically identify people who click links. ajs_event The event name for a track call. This triggers a track call automatically when people visit a link. ajs_aid The anonymousId for the current user. If you don’t pass a value, this returns the current anonymousId. If you pass a value, it sets the value as the user’s anonymousId. ajs_prop_<property> A property you want to pass to a track call. This won’t implicitly trigger an event and is dependent on you also passing ajs_event. This property is included in the resulting track call. ajs_trait_<trait> A trait for the person in the identify call. This won’t implicitly trigger a call. It’s dependent on you also passing ajs_uid. This trait is included in the resulting identify call. For example, this URL: https://example.com/?ajs_uid=123456789abcd&ajs_event=Clicked%20Email&ajs_aid=abc123&ajs_prop_emailCampaign=First+Touch&ajs_trait_name=Karl+Jr. would create the following events on the page. cioanalytics.identify('123456789abcd', { name: 'Karl Jr.' }); cioanalytics.track('Clicked Email', { 'emailCampaign': 'First Touch' }); cioanalytics.user().anonymousId('abc123'); Automatically identify people who click links in your messages In links you add to messages, you can add a ajs_uid UTM parameter that automatically identifies people who click your link. This helps you attribute message metrics and conversions to your messages! To automatically identify people who click links in your messages, you must use their cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. This is a canonical identifier that we assign to every person in your workspace. You may not know a person’s cio_id, but you can pass it to tracked links using 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}}.. On links in your messages, you’ll add the UTM parameter ajs_uid=cio_{{customer.cio_id}} and we’ll automatically identify the person when they click the link and land on a page containing our JavaScript client. https://link.example.com?ajs_uid=cio_{{customer.cio_id}} Frequently Asked Questions Will the snippet slow things down for users? No. Our JavaScript snippet loads code asynchronously, so it won’t affect your page load speed. When the snippet is running on your site, you can enable integrations in your workspace and we’ll start sending data to them automatically. What should I do with third party code? When you use our JavaScript client, you should remove third party code for any integrations you’re going to send data to using Customer.io. This ensures that you don’t record duplicate information. For example, if you want to send data from your JavaScript client to Google Analytics, you’ll want to remove Google Analytics tracking code from your site otherwise you may double-count visitors and events! Do I need to update the snippet to support new integrations? No! You don’t have to do anything to support new integrations. When you add a new data-out integration in Customer.io, we’ll automatically route your data to it. When you create (or change!) an integration, we generate a new API Key for that integration. You use the API Key in your code to tell our servers where the data comes from, so we can route it to outbound integrations and other tools. --- ## JavaScript Frameworks URL: https://docs.customer.io/integrations/data-in/connections/javascript/frameworks/ If you want to install our client-side library as a package—rather than using [our JavaScript snippet](/integrations/data-in/connections/javascript/)—you'll use our `@customerio/cdp-analytics-browser` package. This guide covers installation for popular JavaScript frameworks. Client-side vs. server-side The instructions on this page are for client-side implementations. In general, we prefer the client-side library to support things like in-app messaging and automatic page tracking. But, if you need to use a server-side implementation, see our Node.js library. Feature Browser Library Node.js Library Package @customerio/cdp-analytics-browser @customerio/cdp-analytics-node Page context Automatic: captured from DOM Manual: you must provide URL, path, etc. Identity persistence Automatic: stored in cookies/localStorage Manual: you must pass userId or anonymousId on every call In-app message support Use for Client-side tracking, single page applications (SPAs), in-app messages Server-side tracking, API routes, background jobs React For React-based applications, you’ll want to create a singleton instance of the analytics client that you can import throughout your app. This prevents you from initializing the library multiple times and ensures consistent tracking. Go to Integrations. In the Connections tab, pick the JavaScript integration to get your Write Key. Install the package: npm install @customerio/cdp-analytics-browser Create an analytics module (for example, src/analytics.js): import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser'; export const cioanalytics = AnalyticsBrowser.load({ // cdnURL: 'https://cdp-eu.customer.io', // Set if you're in our EU data center writeKey: '<YOUR_WRITE_KEY>' }); Import and use the analytics instance in your components: import { cioanalytics } from './analytics'; function SignupButton() { const handleClick = () => { cioanalytics.track('Signup Started'); }; return <button onClick={handleClick}>Sign Up</button>; } Track page views in React If you use client-side routing (like React Router), you’ll need to track page views manually when routes change. For example, here’s how you might track page views in React Router: import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { cioanalytics } from './analytics'; function usePageTracking() { const location = useLocation(); useEffect(() => { cioanalytics.page(); }, [location]); } // Use in your App component function App() { usePageTracking(); return ( // your app content ); } Next.js Next.js supports both client-side and server-side rendering. For client-side tracking, you’ll use the browser library with the 'use client' directive. If you want to use the server-side library, see our Node.js library instructions. Go to Integrations. In the Connections tab, pick the JavaScript integration to get your Write Key. Install the package: npm install @customerio/cdp-analytics-browser Create an analytics component in ./components/analytics.js (or .tsx for TypeScript): 'use client'; import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser'; export const cioanalytics = AnalyticsBrowser.load({ // cdnURL: 'https://cdp-eu.customer.io', // Set if you're in our EU data center writeKey: '<YOUR_WRITE_KEY>' }); export default function Analytics() { return null; } Load this component in your ./app/layout.tsx file so it initializes on every page: import Analytics from '../components/analytics' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <Analytics /> <body>{children}</body> </html> ) } Use the analytics instance in your pages: import { cioanalytics } from '../components/analytics' export default function Home() { return ( <main> <button onClick={() => cioanalytics.track('Button Clicked')}> Track Event </button> </main> ) } Track page views in Next.js Next.js uses client-side routing. To track page views on route changes, you can use usePathname and useSearchParams. You might create a component like this and add it to your layout alongside the Analytics component. 'use client'; import { usePathname, useSearchParams } from 'next/navigation'; import { useEffect } from 'react'; import { cioanalytics } from './analytics'; export function PageTracker() { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { cioanalytics.page(); }, [pathname, searchParams]); return null; } Vue.js In Vue.js, you can expose the analytics instance as a global property so it’s available in all components. Go to Integrations. In the Connections tab, pick the JavaScript integration to get your Write Key. Install the package: npm install @customerio/cdp-analytics-browser Add the following code to your main.js file (or your application’s entry point): import { createApp } from 'vue' import App from './App.vue' import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser' const app = createApp(App) app.config.globalProperties.$cioanalytics = AnalyticsBrowser.load({ // cdnURL: 'https://cdp-eu.customer.io', // Set if you're in our EU data center writeKey: '<YOUR_WRITE_KEY>' }); app.mount('#app') Use $cioanalytics in your components: <template> <button @click="$cioanalytics.track('Button Clicked')">Track Event</button> </template> Track page views in Vue.js If you use Vue Router for client-side navigation, track page views using navigation guards: import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [/* your routes */] }) router.afterEach((to) => { // Access the global property from the app instance app.config.globalProperties.$cioanalytics.page({ name: to.name, path: to.path }); }) Listening for in-app message events If you want to send in-app messages and respond to user interactions, add the integrations object when initializing the library. You can find the Site ID in > Workspace Settings > API and Webhooks Credentials > Tracking API Keys. import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser' const cioanalytics = AnalyticsBrowser.load( { writeKey: '<YOUR_WRITE_KEY>', }, { integrations: { 'Customer.io In-App Plugin': { siteId: '<YOUR_SITE_ID>', events: (e) => { switch (e.type) { case "in-app:message-opened": // do something when a message is opened break; case "in-app:message-dismissed": // do something when a message is dismissed break; case "in-app:message-action": // do something when a message is interacted with break; case "in-app:message-error": // do something when a message errors break; case "in-app:message-changed": // do something when a user moves to the next step break; } } } } } ); Tracking page views When you use the JavaScript snippet, we automatically send a page() call on every page load. If you use client-side routing, as many of the frameworks on this page do, you’ll need to send your own page() calls when routes change. The browser library automatically enriches page() calls with: path: The URL path referrer: The previous page URL search: Query string parameters title: The page title url: The full page URL You can override any of these values by passing them explicitly. cioanalytics.page('Category', 'Page Name', { title: 'Custom Title', url: 'https://example.com/custom-path' }); Set the cdnURL if you’re in our EU data center If you’re in our EU data center, set the cdnURL parameter to use our EU regional endpoint. import { AnalyticsBrowser } from '@customerio/cdp-analytics-browser' const cioanalytics = AnalyticsBrowser.load({ cdnURL: 'https://cdp-eu.customer.io', writeKey: '<YOUR_WRITE_KEY>' }); --- ## Method Reference URL: https://docs.customer.io/integrations/data-in/connections/javascript/method-reference/ This page describes the anatomy of the major methods available to the JavaScript snippet. The basic tracking methods below are the building blocks of your integrations. They include [Identify](#identify), [Track](#track), [Page](#page), [Group](#group), and [Alias](#alias) calls. Identify The identify method is how you link your users, and their actions, to a recognizable userId and traits. The Identify method follows the format below and includes the following options. cioanalytics.identify([userId], [traits], [options], [callback]); // A typical identify call without options or callback cioanalytics.identify('f4ca124298', { email: 'cool.person@example.com', first_name: 'cool', last_name: 'person' }); Argument Optional/Required Type Description userId optional String The database ID for the user. If you don't know who the user is yet, you can omit the userId and just record traits. You can read more about identities in the identify reference. traits optional Object A dictionary of traits you know about the user, like their email or name. You can read more about traits in the identify reference. options optional Object A dictionary of options. In most cases, options are just the integrations you want to enable or disable for the call. If you do not pass a traits object, pass an empty object (as an {}) before options callback optional Function A function executed after a short timeout, giving the browser time to make outbound requests first. Automatically identify people who click links in your messages You can automatically identify people when they click links in messages you send from Customer.io using the ajs_uid UTM parameter with their cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). in the format ?ajs_uid=cio_{{customer.cio_id}}. See our UTM parameter documentation for more information. Identify calls and anonymous people By default, we cache traits in the browser’s localStorage and attach them to subsequent identify calls. This means that you can make an identify call before someone has a userId, to store traits that you think will become meaningful later if or when you identify a person by their ID. For example, you might call identify when someone signs up for alerts about a subject or a newsletter, but hasn’t yet created an account yet. cioanalytics.identify({ favoriteBaseballTeam: 'san francisco giants', likes: ['baseball','hockey'], email: 'cool.person@example.com` }); Then, when the user completes the sign up process, you could send the following event. cioanalytics.identify('12091906-01011992', { name: 'cool person', }); The traits object for the second call will automatically pick up the email, likes, and favoriteBaseballTeam traits that you set in the first call! Customer.io Journeys doesn’t support anonymous identify calls yet We’re working on support for anonymous profiles, but, for now, Journeys ignores anonymous identify calls. This doesn’t mean you shouldn’t send anonymous identify calls. Many of our other integrations support anonymous identify calls. But even if Journeys is the only place you send data, we store traits in the browser’s localStorage, so you can still use anonymous identify calls to store information that you’ll want to attach to a user when you do identify them. Identify Callbacks You can omit both traits and options, and pass a callback as the second argument. cioanalytics.identify('12091906-01011992', function(){ // Do something after the identify request has been sent // Note: site-critical functionality should not depend on your analytics provider }); Track The Track method helps you record actions your users perform. You’ll find details about track calls in the track method payload. The only required argument for the track method is an event name. In general, events contain some number of properties. Beyond that, you can also set options for the call (typically the integrations listed in the call), and a callback that you want to perform after the track call. cioanalytics.track('article_completed', { title: 'How to send a track event', course: 'Intro to Analytics', }, { integrations: { "mixpanel": true, "salesforce": false } }); cioanalytics.track(event, [properties], [options], [callback]); Argument Optional/Required Type Description event String The name of the event. properties optional Object A dictionary of [properties](/integrations/api/cdp/#operation/track) for the event. If the event name was 'Added to Cart', it might have properties like price and productType. options optional Object A dictionary of options. In most cases, options are just the integrations you want to enable or disable for the call. If you do not send traits, pass an empty object (as an {}) before options callback optional Function A function executed after a short timeout, giving the browser time to make outbound requests first. Track Link The trackLink helper method attaches the track call as a handler to a link. It inserts a short, 300 ms timeout to give the track call more time to execute. You might do this to handle events in the event of a redirect that occurs before the track method could complete all requests. var link = document.getElementById('free-trial-link'); cioanalytics.trackLink(link, 'Clicked Free-Trial Link', { plan: 'Enterprise' }); Argument Optional/Required Type Description element(s) Element or Array DOM element that you want to bind to the track method. You can pass an array of elements or jQuery objects. But it must be an element. You cannot simply pass a CSS selector._ event String or Function The name of the event passed to the track method. You can also pass a function that returns a string that you want to use as the name of the track event. properties optional Object or Function A dictionary of properties that you want to pass with the track method or a function that returns an object that you want to pass to the properties of the event. Track Form trackForm is a helper method that binds a track call to a form submission. It inserts a short, 300 ms timeout to give the track call more time to execute. You might do this to handle events in the event of a redirect that occurs before the track method could complete all requests. var form = document.getElementById('signup-form'); cioanalytics.trackForm(form, 'Signed Up', { plan: 'Premium', revenue: 99.00 }); Argument Optional/Required Type Description form(s) Element or Array The form element you want to track or an array of form elements/jQuery objects. You must provide elements; you cannot simply enter a CSS selector._ event String or Function The name of the event passed to the track method. You can also pass a function that returns a string that you want to use as the name of the track event. properties optional Object or Function A dictionary of properties that you want to pass with the track method or a function that returns an object that you want to pass to the properties of the event. Page The Page method records page views on your website, along with optional extra information about the page a person visited. Our JavaScript client automatically sends a page call on load. The JavaScript client may also instantiate libraries from other downstream integrations and typically requires this call to be fired once per page to properly initialize code that bypasses Customer.io and goes directly to an outside service. A Page call is included by default as the final line in the JavaScript snippet. You can modify this page call. cioanalytics.page([category], [name], [properties], [options], [callback]); Argument Optional/Required Type Description category optional String The category of the page. This is useful for ecommerce-style cases where many pages might live under a category. If you pass only one string to page we assume that it's the page name. You must include a `name` to send a category. name optional String The name of the page. properties optional Object A dictionary of properties of the page. We automatically collect url, title, referrer, path, and search properties. The URL defaults to a canonical url if available, and falls back to document.location.href. options optional Object A dictionary of options. In most cases, options are just the integrations you want to enable or disable for the call. If you do not pass a traits object, pass an empty object (as an {}) before options callback optional Function A function executed after a short timeout, giving the browser time to make outbound requests first. Default Page Properties When you make a page call, we automatically add the title, url, path, referrer, and search properties. You can override these properties if you want—like if you use the JavaScript client in your single page app. Otherwise, if you send this call: cioanalytics.page('Docs'); We automatically add the following information: cioanalytics.page('Docs', { title: 'Customer.io Docs', url: 'https://customer.io', path: '', referrer: 'https://customer.io', search: '?utm_source=customerio&utm_medium=docs' }); Group The Group method associates an identified person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Group calls are useful for integrations where you maintain relationships between people and larger organizations, like in Customer.io! In Customer.io Journeys, you can store groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and trigger campaigns based on a person’s relationship to an object—like an account, online class, and so on. Find more details about group, including the group payload, in our API spec. The Group method follows the format below and contains the following fields. cioanalytics.group(groupId, [traits], [options], [callback]); Argument Optional/Required Type Description groupId String The Group ID you want to associate with the identified person. traits optional Object A dictionary of traits for the group. In Customer.io Journeys, we refer to these as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. options optional Object A dictionary of options. In most cases, options are just the integrations you want to enable or disable for the call. If you do not pass a traits object, pass an empty object (as an {}) before options callback optional Function A function executed after a short timeout, giving the browser time to make outbound requests first. Example group call: cioanalytics.group('UNIVAC Working Group', { principles: ['Eckert', 'Mauchly'], site: 'Eckert–Mauchly Computer Corporation', statedGoals: 'Develop the first commercial computer', industry: 'Technology' }); By default, group traits are cached in the browser’s local storage and attached to each subsequent group call, similar to how the identify method works. Find more details about group including the group payload in the Group Spec. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. This is an advanced method. For now, you’ll only need to send alias calls to manage user identities successfully in Mixpanel. But we may add integrations in the future that’ll make use of this call. cioanalytics.alias(userId, [previousId], [options], [callback]); Argument Optional/Required Type Description userId String The new user ID you want to set for a person. previousId optional String The user's previous userId. This defaults to the currently identified user's ID. options optional Object A dictionary of options. In most cases, options are just the integrations you want to enable or disable for the call. callback optional Function A function executed after a short timeout, giving the browser time to make outbound requests first. The options object: setting destinations The options object comes after traits or event properties in identify, track, page, group, and alias calls. It takes a single key called integrations and lets you determine the specific services that you want to send data to. Each item in the integrations object is a boolean, where true sends data to the service and false prevents data from going to the service. You can also set all to false, the call will only go to the integrations you explicitly set. If all is true or unset, we’ll send the call to all your integrations except services that you set to false. cioanalytics.identify('user_123', { email: 'cool.person@example.com', name: 'cool person' }, { integrations: { 'all': false, 'Intercom': true, 'Google Analytics': true } });  Use an empty object in place of empty traits or properties Arguments in our JavaScript calls are positional: they’re read in order. Even if you don’t set traits or event properties, you’ll need to send an empty object, so we know where to find your options object and the integrations therein! cioanalytics.identify('user_123', {}, { integrations: { 'all': false, 'Intercom': true, 'Google Analytics': true } }); Load Options You can modify the .load method in Analytics.js (the second line of the snippet) to take a second argument. If you pass an object with an integrations dictionary, then we’ll only load the integrations in that you set as true. You can only call .load on page load, or reload (refresh). If you modify the .load method between page loads, it does not have any effect until the page reloads. cioanalytics.load('writekey', { integrations: { All: false, 'Google Analytics': true, 'Segment.io': true } }) This way, you can conditionally load integrations based on what customers opt into on your site. The example below shows how you might load only the tools that the user agreed to use. Learn more about the load method. onConsentDialogClosed(function(consentedTools){ cioanalytics.load('writekey', { integrations: consentedTools }) }) --- ## Migrate from another service URL: https://docs.customer.io/integrations/data-in/connections/javascript/js-migration/ Our JavaScript snippet works similarly to other platforms, but you may need to do a bit of work to move from another service to Customer.io. Migrating from Segment Our calls are nearly identical to Segment’s. In most cases, you can simply replace your Segment script with our JavaScript client, and you’re all set. However, Segment supports a wider range of integration destinations than we do. Before you make the switch, check that we support the services you integrate with and the actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. that you expect before you move from Segment to Customer.io as your customer data platform (CDP). If you still use Segment, use the cioanalytics variable Normally you can call our JavaScript client with the analytics or cioanalytics variables. But Segment already uses the analytics variable! If you want to use both Segment and Customer.io in the same app, you’ll need to use the cioanalytics variable when you call our JavaScript client. This prevents conflicts with Segment, so you can use both Segment and Customer.io in the same app. You might do this if you want to migrate from Segment to Customer.io and want to verify that you’re capturing all the same data in Customer.io before you remove Segment from your app. Migrating from Customer.io’s JavaScript snippet Customer.io has two JavaScript clients—an older JavaScript snippet based on our Track API and a newer one that uses our Pipelines API. We recommend that you use the newer JavaScript client. If you’re new to Customer.io, you won’t see the older JavaScript snippet when you look up JavaScript in our integration directory. The newer JavaScript client makes supports additional features for in-app messaging and has things like retry logic built in; it’s also where we’ll focus our development efforts going forward. To migrate from the JavaScript snippet to our newer client, you’ll need to: Add the new JavaScript client to your website. Update requests to use cioanalytics rather than _cio and update the structure of your identify calls. Most other requests are the same. Differences between the new JavaScript client and older JavaScript snippet With our older snippet, a person’s id was a part of their other attributes. When you use the newer client, the id is its own argument, separate from a person’s attributes. New Client New Client cioanalytics.identify('12091906-01011992', { name: 'cool person', }); Old Snippet Old Snippet cioanalytics.identify({ id: '12091906-01011992', name: 'cool person', }); Migrating from Rudderstack Our calls are nearly identical to Rudderstack’s. However, their calls are attached to rudderanalytics and ours use analytics. You won’t have to replace your calls, but you should change instances of rudderanalytics to analytics after you replace your Rudderstack JavaScript with the JavaScript client. Keep in mind that Rudderstack supports a wider range of integrations than we do. Before you make the switch, check that we support your integrations and the actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. that you expect before you move from Rudderstack to Customer.io as your customer data platform (CDP). --- ## Managing identities URL: https://docs.customer.io/integrations/data-in/connections/javascript/js-source-identities/ We write the user's IDs to their browser's local storage, and use that as the user ID on cookies whenever possible. Local Storage is meant for storing this type of first-party information and helps us ensure high fidelity, first-party data. How it works Our JavaScript client manages the identities of your audience, first as an anonymousId. Then, when you identify them, you know them by their userId. Then we remove those values when a person logs out and you call cioanalytics.reset();. We store user identity information in cookies and local storage. You can retrieve or override this information as necessary to support your integrations. This page provides information about the values we store, where we store them, and how to get or override them. flowchart LR a(person visits site)-->|generate anonymousId|z subgraph z [anonymous activity] direction TB b(visit a page) b-->|page|c(add item to cart) c-->|track|d(person logs in) end subgraph y [identified activity] direction TB e(person initiates checkout)-->|track|f(person finishes transaction) f-->|track|g(person logs out) end d-->|identify|e g-->|reset|h(person is anonymous again) ID Persistence We write the user’s IDs to their browser’s local storage, and use that as the user ID on cookies whenever possible. If a user returns to your site after the cookie expires, we’ll look for an old ID in the user’s localStorage. If we find an ID, we’ll set it as the user’s ID again in the new cookie. If a person clears their cookies and localstorage, they’ll remove all IDs, and they’ll get a completely new anonymousID the next time they visit your site. Anonymous IDs We generate a universally unique ID (UUID) for website visitors when our JavaScript initializes, and we set this value as the anonymousId for each new visitor to your site. This happens before we load direct-connection integrations, so they don’t generate their own user IDs. Example: ajs_anonymous_id=%2239ee7ea5-b6d8-4174-b612-04e1ef3fa952 You can override the auto-generated anonymousID in code using the methods described below: Set anonymousId (before the ready method returns) Use a call to override the anonymousID Set anonymousId in the options object of a call Retrieve the Anonymous ID You can get the user’s current anonymousId with the user method. cioanalytics.user().anonymousId(); If the user doesn’t have an anonymousId (it’s null) this call will automatically generate and sets a new anonymousId for the user. Refreshing the Anonymous ID A user’s anonymousId changes in any of the following situations: They clear their cookies and localstorage. Your site or app calls cioanalytics.reset() during a user’s browser session. Your site or app calls cioanalytics.identify() with a userId that is different from the current userId. Override the Anonymous ID You can also set the anonymousId immediately, even before the ready method returns. cioanalytics.load('writekey'); cioanalytics.page(); cioanalytics.setAnonymousId('ABC-123-XYZ'); You might use this method if you queue calls before ready returns and those methods require a custom anonymousId. Keep in mind that setting the anonymousId in Analytics.js does not overwrite the anonymous tracking IDs for any other integrations you use.  Direct-connection integrations might have their own anonymous IDs In some rare cases, integrations might not use Customer.io’s anonymousId. Read the documentation for your integration to find out if it sets its own ID. Override the default Anonymous ID If the default anonymous UUID doesn’t meet your needs, you can override anonymousId for the current user using either of the following methods. cioanalytics.user().anonymousId('ABC-123-XYZ'); cioanalytics.setAnonymousId('ABC-123-XYZ') Override Anonymous IDs using the options object You can override an anonymousId in the options object of identify, page, or track calls. The custom anonymousId persists when you use the methods below, even if you don’t specify the anonymousId in the calls. Identify Identify cioanalytics.identify('user_123', { name: 'Jane Kim' }, { anonymousId: 'ABC-123-XYZ' }); Page Page cioanalytics.page({}, { anonymousId: 'ABC-123-XYZ' }); Track Track cioanalytics.track('Email Clicked', { callToAction: 'Signup' }, { anonymousId: 'ABC-123-XYZ' }); Saving traits to the context object Traits are things that you know about a user or a group, and which can change over time—like attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io Journeys. The options object contains a child object called context that automatically captures data depending on the event and the SDK or library you use. See our the context object to learn more. The context object contains an optional traits dictionary. This dictionary contains traits about the current user. You can use this to retrieve information about a user that you set or stored as a result of previous identify calls. This might be useful if you also want to send traits as properties in track or page calls. The information you pass in traits does not appear in your downstream tools (like Salesforce, Mixpanel, or Google Analytics). But this data does appear in warehouses and storage integrations. Imagine that you sent this identify call. cioanalytics.identify('12091906-01011992', { plan_id: 'Paid, Tier 2', email: 'cool.person@example.com' }); You can pass the plan_id into context.traits, so you can use them in track and page events that the user triggers later, as shown below. cioanalytics.track('Clicked Email', { emailCampaign: 'First Touch' }, { traits: { plan_id: 'Paid, Tier 2' } } ); This appends the plan_id trait to the track event. This does not add the name or email, since those traits were not in the context object. You must do this for every event that you want these traits to appear on, because context does not persist between calls. Clearing Traits You can pass an empty object to the traits object to clear all cached traits for a User or Group. Traits are cached by default when you call the Identify and Group methods. You can clear the traits object for the user or group by passing traits an empty object: cioanalytics.user().traits({}); cioanalytics.group().traits({}); Using cioanalytics.user() and cioanalytics.group() You can use the user or group method as soon as the Analytics.js library loads, to return information about the currently identified user or group. This information is retrieved from the user’s cookie.  Use the ready function You can wrap any reference to user() or group() in a ready function block to make sure that cioanalytics.js fully loads and these methods are available. Get the current user Get the current user cioanalytics.ready(function() { var user = cioanalytics.user(); var id = user.id(); var traits = user.traits(); }); Get a group Get a group cioanalytics.ready(function() { var group = cioanalytics.group(); var id = group.id(); var traits = group.traits(); }); Anonymizing IP addresses We automatically collect the user’s IP address for device-based events. You can pass a value for options.context.ip to prevent us from recording IP addresses like this: cioanalytics.track("Order Completed", {}, { context: { ip: "0.0.0.0" }}); You must add this override to every track call to explicitly override IP collection. If you reset this trait in the context object, we’ll default to the normal IP collection behavior. --- ## Cookies and identity management URL: https://docs.customer.io/integrations/data-in/connections/javascript/js-source-cookies/ Our JavaScript client library stores cookies on the client. You can recall this information when you need to reuse it and change whether we persist data to protect your audience's personal information. How it works The JavaScript library stores some information in cookies and local storage on the client to manage user identity. By default, we store up to five cookies on the client. You can recall this information when you need to reuse it and change whether we persist data to protect your audience’s personal information. Cookie Contains ajs_anonymous_id A user’s anonymous ID, set automatically when someone visits your site. This value is used to track anonymous activity and associate anonymous activity with an identified person. ajs_user_id The ID of the user, set when you identify a person. ajs_group_id The ID of a group, set when you associate a person with a group. ajs_user_traits Contains user attributesInformation that you know about a person, captured from identify events in Data Pipelines. Traits are analogous to attributes in Customer.io Journeys.—values associated with a person—that you set when you identify a person. ajs_group_traits Contains group traits—values associated with an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.—that you set when you associate a person with a group.  Our JavaScript client only sets first-party cookies. However, direct integrations might choose to set their own cookies—like Meta Pixel or Google Analytics 4. See your integration’s Cookie & Privacy Policy for more information. User ID and local storage To ensure the fidelity of customer data, we write your audience’s IDs to both local storage and cookies whenever possible. Local storage is designed for this type of first-party customer information. If a user returns to your site after our ajs_user_id cookie expires, Analytics.js looks for an ID in the user’s localStorage. If we find an ID there, we set it as the user’s ID again in a new cookie. This ensures that people stay “identified” in most cases. But if a user clears their cookies and localStorage, they’ll remove all their identifying information and get a completely new anonymousID the next time they visit your website. Cookie settings Our JavaScript client sets properties when creating cookies for user or group identities. You can override the default cookie properties when you load Analytics.js by passing a cookie object to the load method. You can modify the following parameters. Parameter Description Default domain The domain for the cookie. This must match the domain of the JavaScript origin. If an Analytics.js cookie already exists at the top-level domain, we carry the same cookie value to any subdomains, despite domain configuration. Top-level domain maxage The maximum time (in days) before the cookie expires on its own. 365 days (1 year) path The path the cookie is valid for. "/" sameSite Prevents the browser from sending the cookie along with cross-site requests. Lax secure Boolean determining whether cookies must be transferred over secure protocols (https) or not. false cioanalytics.load('writeKey', { cookie: { domain: 'sub.site.example', maxage: 7, // 7 days path: '/', sameSite: 'Lax', secure: true } })  Chrome limits cookies to 400 days If you try to set the maxage value beyond 400 days, then Chrome sets the upper limit to 400 days instead of rejecting it. Cross-site cookie support To enable cross-site cookie support, initialize the SDK with the following cookie settings: analytics.load('YOUR_WRITE_KEY', { cookie: { sameSite: 'None', secure: true } }); The sameSite: 'None' setting allows the cookie to be sent in cross-site requests. The secure: true setting ensures the cookie is only transmitted over HTTPS connections, which is required when using sameSite: 'None'. Cross-site tracking may not work for all users. Some browsers, like Safari, block cross-site tracking by default through privacy settings. User settings When you identify a person, we automatically persist their ID and traits locally. You can override how and where you want to store the user ID and traits when you load Analytics.js by passing a user object to the load method. Option Description Default persist When true, Analytics.js stores information locally. true cookie.key The name of the cookie that stores the user ID. ajs_user_id cookie.oldKey The name of a cookie that previously stored the user ID. Analytics.js will read this cookie if cookie.key isn’t found. ajs_user localStorage.key The name of the key that stores user traits in localStorage. ajs_user_traits cioanalytics.load('writeKey', { user: { persist: true, cookie: { key: 'ajs_user_id' }, localStorage: { key: 'ajs_user_traits' } } }) Group settings We automatically persist your audience’s associated group ID and group properties locally. You can override how and where you want to store the group ID and properties by passing in a group object to the load method when you load Analytics.js. Field Description Default persist When true, Analytics.js stores group information locally. true cookie.key Name of the cookie that stores the group id. ajs_group_id localStorage.key Name of the key that stores user traits in localStorage. ajs_group_properties cioanalytics.load('writeKey', { group: { persist: true, cookie: { key: 'ajs_group_id' }, localStorage: { key: 'ajs_group_properties' } } }) Persistent retries By default, Analytics.js automatically retries on network and server errors. When the client is offline or your application can’t connect to Customer.io, Analytics.js stores events in localStorage and falls back to in-memory storage when localStorage is unavailable. Disable identity persistence By default, Analytics.js automatically persists user and group identities in cookies and local storage. You can change this behavior using the disableClientPersistence setting. disableClientPersistence is a boolean that defaults to false, meaning that we’ll persist identity in local storage, cookies, and memory. Setting to true disables all persistence. // Disable all persistence methods cioanalytics.load('writeKey', { disableClientPersistence: true }) Disabling client persistence means you won’t be able to track people across pages. You might do this in situations where you customers’ data is extremely sensitive. But you can still track people on each page. See the sections below for more details. Identify When disableClientPersistence is set to true, Analytics.js cannot automatically keep track of a user’s identity when they go to different pages. You can still manually track identity by calling cioanalytics.identify() with the known identity on each page load, or you can pass in identity information to each page using query strings. Event retries Under normal circumstances, Analytics.js tries to determine when your audience might close a page and saves pending events to localStorage. When your audience goes to another page in the same domain, Analytics.js attempts to send any events it finds in localStorage. When disableClientPersistence is set to true, Analytics.js won’t store pending events in localStorage. Client-side cookie methods (get, set, clear) When you invoke a standard method for Analytics.js (identify, track, group, etc), the client-side library automatically sets cookies and local storage for you. The following methods help you get or assign values to cookies outside the standard JavaScript methods. You might want to do this to use your audience’s data to personalize elements on pages or to sanitize your audience’s data. To get a cookie’s value, pass an empty () at the end of the method. //return the user ID cookie (ajs_user_id) value cioanalytics.user().id() To assign a cookie’s value, include the string value inside those parenthesis, for example, ('123-abc'). To clear or remove the value for a specific field, pass in an empty value of its type. For example, for string (''), or for object ({}). //clear a user trait cioanalytics.user().traits({'redacted-pii': ''}) Field Cookie Name Analytics.js Method Local Storage Method Set Example Clear Example userId ajs_user_id cioanalytics.user().id(); window.localStorage.ajs_user_id cioanalytics.user().id('123-abc'); cioanalytics.user().id(''); anonymousId ajs_anonymous_id cioanalytics.user().anonymousId(); window.localStorage.ajs_anonymous_id cioanalytics.user().anonymousId('333-abc-456-dfg'); cioanalytics.user().anonymousId(''); user traits ajs_user_traits cioanalytics.user().traits(); window.localStorage.ajs_user_traits cioanalytics.user().traits({firstName:'Jane'}); cioanalytics.user().traits({}); groupId ajs_group_id cioanalytics.group().id(); window.localStorage.ajs_group_id cioanalytics.group().id('777-qwe-098'); cioanalytics.group().id(''); group traits ajs_group_properties cioanalytics.group().traits() window.localStorage.ajs_group_properties cioanalytics.group().traits({name:'Customer.io'}) cioanalytics.group().traits({}) Storage Priority By default, Analytics.js uses localStorage as its preferred storage location with Cookies as a fallback when localStorage is not available or not populated. An in-memory storage is used as a last fallback if all the previous ones are unavailable for any reason. --- ## Utility Methods and Performance URL: https://docs.customer.io/integrations/data-in/connections/javascript/utility-methods/ When you load the JavaScript snippet, you have access to additional methods that control how and when the JavaScript client loads How it works Our JavaScript client exposes the following utility methods to help you change how script loads on your page. Load Ready Debug On (Emitter) Timeout Reset (Logout) Load You can load a buffered version of our JavaScript that requires you call load explicitly before the script initiates any network activity. This is useful if you want to wait for user consent before you load the full compliment of integrations or send buffered events to Customer.io. Within the load method, you can set cookie, user, and group settings determining the cookies and local data that analytics.js stores with your client. You should only call the load method once. export const cioanalytics = new AnalyticsBrowser() cioanalytics.identify("hello world") if (userConsentsToBeingTracked) { cioanalytics.load({ writeKey: '<YOUR_WRITE_KEY>' }) // integrations loaded, enqueued events are flushed } You can also use load if you fetch settings asynchronously. const cioanalytics = new AnalyticsBrowser() fetchWriteKey().then(writeKey => cioanalytics.load({ writeKey })) cioanalytics.identify("hello world") Ready The ready method lets you pass a method that is called when Customer.io’s JavaScript finishes initializing, and when all enabled device-mode integrations load. It’s like jQuery’s ready method—but for integrations! The ready method isn’t invoked if any integration throws an error (Like if your API key expired, your configuration settings were incorrect, or if your integration is blocked by the browser) during initialization. The code in the ready function only executes after ready is emitted. If you want to access end-tool library methods that do not match Customer.io methods, like adding an extra setting to Mixpanel, you can use a ready callback so that you’re guaranteed to have access to the Mixpanel object, like this: cioanalytics.ready(function() { window.mixpanel.set_config({ verbose: true }); }); The ready method uses the following format, supporting a callback function that’s executed after all enabled integrations have loaded. cioanalytics.ready(callback); Debug The debug method turns on debug mode, which logs messages to the developer console that can help you debug your implementation. The call takes a boolean, where true enables debug mode and false disables it. //enable debug mode cioanalytics.debug(true); Emitter The global analytics object emits events whenever you call alias, group, identify, track, or page. Use the on method to set listeners for these events and run your own custom code. You might want to do this if you need to send data to a service we don’t support. The call works like this where: method is the event you want to listen for—one of alias, group, identify, track, or page. callback is a function that takes place after the method, and takes three arguments event, properties, and options. cioanalytics.on('track', function(event, properties, options) { bigdataTool.push(['recordEvent', event]); }); This method emits events before they are processed by Customer.io, and may not include some of the normalization we do on the client before sending data to Customer.io.  Page event properties are stored in the options object. Extending timeouts The timeout method sets the length of callbacks and helper functions in milliseconds. This is useful if you have multiple scripts that need to fire in your callback or trackLink and trackForm helper functions. For example, if you wanted to set the timeout to 500ms, you’d set it like this: cioanalytics.timeout(500); If you trigger ad network conversion pixels, we recommend that you extend the timeout to 500 ms to account for slow load times. Reset or log out Calling reset resets the id, including anonymousId, and clears traits for the currently identified user and group. You probably want to do this when a user logs out of your service or revokes consent from tracking cookies. cioanalytics.reset(); This method only clears cookies and localStorage created by Customer.io. It doesn’t clear data from other integrated tools, as those native libraries might set their own cookies for user tracking, sessions, and to manage states. We don’t share localStorage across subdomains. If you use our JavaScript client on multiple subdomains, you need to call cioanalytics.reset() for each subdomain to completely clear out the user session. Retries Our JavaScript client automatically retries network and server errors. With persistent retries, we: Support offline tracking, queueing your events and delivering them when a user comes back online. Handle network issues in the event that you can’t connect to our API. We store the events in the browser to ensure that you don’t lose data. Analytics.js stores events in localStorage and falls back to in-memory storage when localStorage is unavailable. We’ll retry up to 16 times with an exponentially increasing delay between retries. The maximum retry window is about three hours. Cross-Subdomain tracking Analytics.js tracks across subdomains out of the box for all of our data-out integrations. Analytics.js Performance Our JavaScript library and all integration libraries are loaded asynchronously (with the HTML script async tag). This means that we fire Customer.io methods asynchronously, so you should adjust your code accordingly if you want to send events from the browser in a specific order. We only load libraries required for the integrations you’ve enabled. When you disable an integration, Analytics.js stops requesting that library. Bundle size Our JavaScript snippet only increases your page size by about 1.1KB. However, the snippet asynchronously requests and loads a customized JavaScript bundle (analytics.min.js), which contains the code and settings needed to load your direct-mode integrations. This file’s size changes depending on the integrations you enable. Without any integrations, our JavaScript should be about 62 KB. Local storage cookies used by Analytics.js Analytics.js uses a few localstorage cookies if you have retries enabled. These cookies keep track of retry timing. The ack cookie is a timer used to determine if another tab should claim the retry queue. The reclaimStart and reclaimEnd cookies determine if a tab takes over the queue from another tab. The inProgress and queue cookies track events in progress, and events that are queued for retries. --- ## In-app messages URL: https://docs.customer.io/integrations/data-in/connections/javascript/in-app/ When you use our JavaScript client, you can send in-app messages to your website visitors. This page helps you understand how some in-app features work so that you can better target and display messages. How it works In-app messages for your website work differently than push notifications would: they require JavaScript, and they don’t go through a push notification service (like APNs or FCM). In most cases, you simply need to identify your web visitors, and they’ll become eligible to receive your in-app messages. sequenceDiagram Participant a as App User Participant b as JavaScript Client Participant c as Customer.io c->>c: Trigger in-app message c-->>b: If app isn't open, hold until user opens app a->>b: User opens app b->>c: Identify User c->>b: Send in-app message b->>a: User sees in-app message Send an in-app message To send an in-app message, you’ll need to do the following things. Because most of these things happen outside the SDK, we’ve linked to relevant documentation. Enable in-app messaging Set up message templates Add in-app messages to your campaigns or broadcasts In most cases, you’ll need to identify visitors to your website before they can receive an in-app message. Enable in-app messaging To set up in-app messages with our JavaScript client, you only need to enable in-app messaging in your workspace. When you have in-app messaging enabled, we’ll automatically load the in-app messaging plugin on pages containing your JavaScript snippet. Go to Settings > Workspace Settings and click Get Started next to In-App. Click Enable in-app. (Optional) Click Send Test and enter the email address or ID of a test user to prove your implementation. If this is your first in-app message, it might take a minute for it to appear and you might need to refresh the page where you expect to see your test message. When you send your first message, we poll slowly for messages (about once a minute). When you receive your first message, polling speeds up, eliminating the delay. Add support for anonymous messaging If you want to send in-app messages to people who aren’t identified, you’ll need to add an anonymousInApp flag to your snippet. This lets you support anonymous messaging. See Anonymous in-app messages for more information. !function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute('data-global-customerio-analytics-key', i);t.src="https://cdp.customer.io/v1/analytics-js/snippet/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._writeKey=key;analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.15.3"; analytics.load("YOUR_CDP_API_KEY", { "integrations": { "Customer.io In-App Plugin": { anonymousInApp: true } } }); analytics.page(); }}(); Listen to in-app message events We expose in-app message events that you can listen for and respond to—performing additional functions in response to a person interacting with or dismissing your message. You’ll define functions handling each event type in an events key when you initialize the SDK. These events all have a detail object containing additional data about the event, like the deliveryId or messageId that you can act on. analytics.load( "CDP_WRITE_KEY", { "integrations": { "Customer.io In-App Plugin": { siteId: "YOUR_SITE_ID", events: function(event) { switch (event.type) { case "in-app:message-opened": // do something when a message is opened break; case "in-app:message-dismissed": // do something when a message is dismissed break; case "in-app:message-action": // do something when a message is interacted with break; case "in-app:message-error": // do something when a message errors break; case "in-app:message-changed": // do something when a user moves to the next step in a multi-step message break; } } } } } ); If you set up our JavaScript client by importing the @customerio/cdp-analytics-browser package, learn more about listening for in-app message events. Events and examples You can listen for the following in-app events: Message opened: this happens when your message is displayed to a person. Message dismissed: happens when someone dismisses your message. Remember, dismissing a message doesn’t necessarily mean that a person didn’t respond; dismissing a message often happens when someone responds to a message too. Message action: these are the things that happen when a person interacts with your message Message error: respond when something goes wrong with your message. Message changed: respond when someone engages the next step in a multi-step message. See Multi-step messages for more information. Each event has a detail object containing additional data about the event, like the deliveryId or messageId that you can act on. For the message-action event, we also expose the actionName and actionValue. { // one of: "in-app:message-opened", "in-app:message-dismissed", // "in-app:message-action", "in-app:message-error" "type": "in-app:message-action", "detail": { "deliveryId": "1234567890", "messageId": "1234567890", // only for "in-app:message-action and in-app:message-changed" "actionName": "action name", // for message-changed, this is the name of the next step "actionValue": "action value" } } Page rules and in-app messages Page rules help you determine the pages where people can encounter your messages, ensuring that they’re relevant to the pages people visit on your website. Page rules also help avoid conflicting messages by distributing messages to the pages where they’re most relevant to your audience. If you send two messages of the same priority without page rules, they’ll appear one after the other. When you set a page rule for the Web platform, we use the page URL unless you pass page calls with a different name parameter. For example, an include rule for https://example.com/*/billing allows a message to appear on https://example.com/ui/billing or documents about billing under https://example.com/billing. You can also match on a query parameter in the URL using the contains operator. See the page method reference for more information.  Use * to represent all pages When you select a channel, you have to enter a page rule. But, if you want to show a message on every page on your website or app, you can simply enter *. Page rules for single-page applications The JavaScript snippet automatically sends page calls when it loads. But if you import/bundle the JavaScript SDK, or have a single page application (SPA), you’ll need to call cioanalytics.page() manually for each page or route. This tells the SDK what “page” a person is on so you can target in-app messages to people on certain pages of your app. --- ## Notification inbox URL: https://docs.customer.io/integrations/data-in/connections/javascript/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io also doesn’t deliver rendered inbox messages; they’re delivered as JSON payloads. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message.  This feature requires cdp-analytics-browser version 0.3.11 or later If you import our JavaScript client as a package (like @customerio/cdp-analytics-browser), you need to update to version 0.3.11 or later to use the notification inbox. Inbox methods You’ll fetch messages using the inbox() method. Method Description inbox() Fetch messages from the inbox. Takes optional parameters to filter messages by topic. (e.g. analytics.inbox('orders', 'announcements')) inbox.messages() Fetch messages from the inbox. inbox.total() Get the total number of messages in the inbox. inbox.totalUnopened() Get the number of unopened messages in the inbox. inbox.markOpened() Mark a message as opened. inbox.markUnopened() Mark a message as unopened. inbox.markDeleted() Mark a message as deleted. inbox.trackClick(actionName?) Track a click on the message. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you call the inbox() method. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Again, inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Get Messages // Get all messages const messages = await inbox.messages(); // Get counts const total = await inbox.total(); const unopened = await inbox.totalUnopened(); // Subscribe to updates const unsubscribe = inbox.onUpdates((messages) => { console.log('Inbox updated!', messages); // Update your UI }); Notification inbox code example Here’s a simple example for an inbox UI. This example assumes you’ve already set up your backend to trigger inbox messages and you’ve already loaded the Customer.io JavaScript SDK. // Cache messages and inbox instance to avoid re-fetching in each handler. // Alternatively, you could call inbox.messages() in handlers for simpler code // at the cost of an extra async call on each button click. let currentMessages = []; let inboxInstance; // Helper to escape HTML and prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Render function function renderInbox(messages) { currentMessages = messages; // Store for handlers const container = document.getElementById('inbox'); container.innerHTML = messages.map(message => { const props = message.properties; return ` <div class="inbox-message ${message.opened ? '' : 'unread'}"> <div class="inbox-message-content"> <h4>${escapeHtml(props.title)}</h4> <p>${escapeHtml(props.body)}</p> <small>${new Date(message.sentAt).toLocaleDateString()}</small> </div> <button onclick="handleRead('${message.messageId}', ${message.opened})"> ${message.opened ? 'Mark Unread' : 'Mark Read'} </button> <button onclick="handleDelete('${message.messageId}')">Delete</button> </div> `; }).join(''); } // Update the unread badge async function updateBadge() { if (inboxInstance) { const count = await inboxInstance.totalUnopened(); document.getElementById('unread-badge').textContent = count; } } // Wait for analytics to be ready, then initialize inbox and load messages cioanalytics.ready(() => { inboxInstance = cioanalytics.inbox('orders', 'announcements'); inboxInstance.messages().then(renderInbox); inboxInstance.onUpdates(renderInbox); // Update unread badge updateBadge(); }); // Message actions async function handleRead(messageId, isOpened) { const message = currentMessages.find(m => m.messageId === messageId); if (message) { isOpened ? await message.markUnopened() : await message.markOpened(); await updateBadge(); } } async function handleDelete(messageId) { const message = currentMessages.find(m => m.messageId === messageId); if (message) { await message.markDeleted(); await updateBadge(); } } --- ## Content Security Policy (CSP) URL: https://docs.customer.io/integrations/data-in/connections/javascript/content-security-policy/ If you enforce a Content Security Policy (CSP) on your website, you need to add directives that let the JavaScript client load and communicate with Customer.io. This page shows the required directives. A Content Security Policy (CSP) is a standard that helps you control the resources that a web browser can load on your page. If you enforce a CSP on your website, you need to allow the domains that our JavaScript client communicates with.  If you’re using our legacy JavaScript snippet, where calls use the _cio prefix, see the legacy CSP page instead. Standard directives US data center US data center script-src cdp.customer.io; connect-src cdp.customer.io *.api.gist.build *.cloud.gist.build; frame-src renderer.gist.build code.gist.build; style-src 'unsafe-inline'; EU data center EU data center script-src cdp-eu.customer.io; connect-src cdp-eu.customer.io *.api.gist.build *.cloud.gist.build; frame-src renderer.gist.build code.gist.build; style-src 'unsafe-inline'; Use nonce-based authorization If your CSP uses nonce-based authorization, you can add a nonce to the JavaScript client snippet instead of allowing entire domains in script-src. This provides a stricter security model because it authorizes specific script elements rather than entire domains. Add the nonce attribute to the snippet’s <script> tag, matching the nonce value in your CSP header: <script nonce="your-random-nonce-value"> // Your JavaScript client snippet code here </script> Your CSP header should include the matching nonce: script-src 'nonce-your-random-nonce-value' cdp.customer.io;  You still need to allow cdp.customer.io in script-src because the snippet dynamically loads additional scripts from this domain. Package-based installations If you installed the JavaScript client as a package—for example, @customerio/cdp-analytics-browser—rather than using the snippet, you don’t need to allow cdp.customer.io in script-src. The library bundles into your app code. You still need the connect-src directives so the library can send data to Customer.io. connect-src cdp.customer.io *.api.gist.build *.cloud.gist.build; frame-src renderer.gist.build code.gist.build; style-src 'unsafe-inline'; Custom proxy setups If you’ve set up a custom proxy to route requests through your own domain, replace the Customer.io domains in your CSP with your proxy domain. For example, if your proxy domain is analytics.example.com: script-src analytics.example.com; connect-src analytics.example.com *.api.gist.build *.cloud.gist.build; Glossary of directives Some of these directives are required for in-app messaging, which includes features like inbox messages, web-based microsurveys, and so on. If you don’t use these features, you can exclude the relevant directives. Directive Host Description script-src cdp.customer.io The host for the JavaScript client. The snippet loads the library and integration bundles from this domain. connect-src cdp.customer.io Required for sending tracking data—identify, track, and page calls—and loading integration settings. connect-src *.api.gist.build, *.cloud.gist.build Required for in-app messaging. We use these domains for message queues and real-time updates. You can exclude these if you don’t send in-app messages. frame-src renderer.gist.build, code.gist.build Required for rendering in-app messages in iframes. You can exclude these if you don’t send in-app messages. style-src 'unsafe-inline' Required because the in-app messaging infrastructure injects inline <style> blocks to position and display messages. You can exclude this if you don’t send in-app messages. Frequently asked questions Do you need unsafe-inline or unsafe-eval in script-src? No! Unlike our legacy JavaScript snippet, the JavaScript client loads as an external script. You don’t need unsafe-inline or unsafe-eval in your script-src directive. This is one of the benefits of upgrading from the legacy snippet. Can you exclude in-app messaging directives? Yes. If you don’t send in-app messages (including inbox messages, microsurveys, and so on), you can remove all gist domains from your CSP: *.api.gist.build and *.cloud.gist.build from connect-src renderer.gist.build and code.gist.build from frame-src 'unsafe-inline' from style-src --- ## Proxying the JavaScript client URL: https://docs.customer.io/integrations/data-in/connections/javascript/js-source-proxy/ You can proxy the client-side JavaScript (Analytics.js) and all tracking event requests through your domain. You might want to do this to deal with ad blockers or to keep your analytics requests from being blocked by firewalls. How it works When you load Analytics.js on your site, the script makes requests to Customer.io to load the library and to send calls. You can proxy these requests through your own domain, making all Customer.io requests look like first-party requests to your own domain. This can help you avoid problems with ad blockers or firewalls that don’t play nicely with third-party domains or requests. This page shows how to set up a custom domain that proxies to cdp.customer.io in Amazon CloudFront, but you can apply these principles to most modern content delivery networks (CDN). You’ll also need to add a CNAME record to your DNS settings and update your code to point to your new domain. Prerequisites To set up a custom domain, you need: Access to your site DNS settings A content delivery network (CDN) you can serve assets from Access to your CDN’s settings A security certificate for the proxy domain If you have trouble setting up your proxy, you’ll need to contact your IT department for help. We don’t have access to your domain resources to help you configure a custom proxy. Set up a custom proxy with Amazon CloudFront As a part of this process, you’ll set up two CloudFront distributions: one for the JavaScript host and one for our Data Pipelines API. You’ll also need to add a CNAME record to your DNS settings for each. Proxy the JavaScript host Log in to the AWS console and go to CloudFront. Click Create Distribution and configure the distribution settings. In the Origin section set: Origin Domain Name: cdp.customer.io Protocol: HTTPS only In the Default cache behavior section, set: Viewer protocol policy: Redirect HTTP to HTTPS Allowed HTTP Methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE In the Settings section, set: Alternate domain name (CNAME): cioanalytics.<yourdomain>.com Custom SSL certificate: Select an existing or new certificate and validate your authorization to use the Alternate domain name (CNAME) value. For more information, see Amazon’s documentation Requirements for using alternate domain names. Click Create Distribution. Take note of the Domain Name for use in the next step. Go to your domain registrar and add a new “CNAME” record. Set values for these fields: Name: As a part of this process, you should use a name that makes it clear what you use the subdomain for, like analytics.mysite.com. Value: The Domain Name value from CloudFront. Save your record. This might take some time to take effect, depending on your TTL (Time To Live) settings. You should verify that you’ve set up the record correctly. You may want to make a curl request to your domain to verify that the proxy works. Add the proxy to your code After you set up a proxy, you’ll need to add your new proxy address to your code. These instructions depend on whether you use our Client-side JavaScript snippet or the Node.js library. Client-side JavaScript snippet If you use our JavaScript client, you need to modify the cioanalytics.js snippet inside your website’s <head>. You’ll need to make two changes inside the declaration of the analytics.load function. We’ve highlighted the changes in an example below that’s formatted for readability. Change the t.src parameter in the snippet to point to your proxy host. Add an analytics._cdn property to the snippet containing your proxy host. <script> ! function() { var i = "cioanalytics", analytics = (window[i] = window[i] || []); if (!analytics.initialize) if (analytics.invoked) window.console && console.error && console.error("Snippet included twice."); else { ... analytics.load = function(key, e) { var t = document.createElement("script"); t.type = "text/javascript"; t.async = !0; t.setAttribute('data-global-customerio-analytics-key', i); t.src="https://<YOUR-PROXY-HOST>/v1/analytics-js/snippet/" + key + "/analytics.min.js"; var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(t, n); analytics._writeKey = key; analytics._loadOptions = e analytics._cdn = "https://<YOUR-PROXY-HOST>"; }; ... } }(); </script> To proxy API calls that typically go to cdp.customer.io/v1, you’ll set integrations['Customer.io Data Pipelines'].apiHost to your proxy address. window.cioanalytics.load("<MY_WRITE_KEY>", { integrations: { "Customer.io Data Pipelines": { apiHost: "YOUR-PROXY-HOST/v1", }, }, }); npm instructions If you use our Node.js library and want to proxy requests through your domain, you need to: Update the cdnURL setting. Proxy tracking calls that typically go to cdp.customer.io/v1 to your API host by configuring integrations['Customer.io Data Pipelines'].apiHost. const analytics = AnalyticsBrowser.load( { writeKey, // GET https://YOUR-PROXY-HOST/v1/projects/<writekey>/settings --> proxies to // https://cdp.customer.io/v1/projects/<writekey>/settings // GET https://YOUR-PROXY-HOST/next-integrations/actions/...js --> proxies to // https://cdp.customer.io/next-integrations/actions/...js cdnURL: 'https://YOUR-PROXY-HOST' }, { integrations: { 'Customer.io Data Pipelines': { // POST https://MY-CUSTOM-API-PROXY.com/v1/t --> proxies to // https://cdp.customer.io/v1/t apiHost: 'YOUR-PROXY-HOST/v1', protocol: 'https' // optional } } } ) --- ## Get started URL: https://docs.customer.io/integrations/data-in/connections/javascript/legacy-js/getting-started/ It's easy to get started with our JavaScript snippet: just paste the snippet into your pages and you're ready to use Customer.io!  New here? Try out our newer JavaScript client! This page is for our JavaScript snippet, which is older and doesn’t support some newer features in our JavaScript client library. If you’re just getting started with Customer.io, you should use the newer JavaScript client instead! How it works Our web SDK is a JavaScript snippet that helps you identify, send events for, and send in-app messages to people who visit your website(s) or use your web app(s). Simply drop the snippet into your site and it exposes functions you can call to take advantage of Customer.io. Install the snippet Go to Integrations > Customer.io API to find and copy our JavaScript snippet with all the values specific to your region and workspace. Include this snippet on every page in your app, immediately before the closing </body> tag. That’s it! Now you’re ready to use the Journeys Web SDK. Below is an example snippet for the US region. <script type="text/javascript"> var _cio = _cio || []; (function() { var a,b,c;a=function(f){return function(){_cio.push([f]. concat(Array.prototype.slice.call(arguments,0)))}};b=["load","identify", "sidentify","track","page","on","off"];for(c=0;c<b.length;c++){_cio[b[c]]=a(b[c])}; var t = document.createElement('script'), s = document.getElementsByTagName('script')[0]; t.async = true; t.id = 'cio-tracker'; t.setAttribute('data-site-id', 'YOUR_SITE_ID'); t.setAttribute('data-use-array-params', 'true'); //Enables in-app messaging t.setAttribute('data-use-in-app', 'true'); t.src = 'https://assets.customer.io/assets/track.js'; //If your account is in the EU, use: //t.src = 'https://assets.customer.io/assets/track-eu.js' s.parentNode.insertBefore(t, s); })(); </script> If you want to use Google Tag Manager to install the snippet, follow the instructions below Configure the SDK For the most part, copying the JavaScript snippet from any page in your workspace includes all the necessary parameters to use our web SDK. But there are a few parameters you can add or set to change the SDK’s behaviors: Parameter Default Description data-use-array-params true When true, arrays in identify and track calls keep their shape as passed to Customer.io. Set to false to pass arrays as objects containing keys representing the index/order of items in the request. See handling arrays in track calls for more information. data-auto-track-page true When true, the SDK automatically sends page view events when it loads. data-use-in-app false If you want to send in-app messages to your website, you must set this to true. See In-App Messages to ensure in-app is enabled in your workspace. data-cross-site-support false If true, the SDK can set cookies from iframes within third-party domains. (Note: Safari blocks cross-site tracking by default.) data-enable-in-memory-storage false If true, the SDK will track and store information in memory as a fallback when cookies aren’t available. See In-memory storage fallback for more information. Cross Site Cookie Support The data-cross-site-support parameter is designed to help you use the snippet in situations where cookies might not be available. When set to true, the SDK can set cookies when you use certain third party platforms like Shopify. Shopify uses iframes to embed your website, and this setting enables the SDK to set first-party cookies from your embedded site. Cross-site tracking may not work for all users. Safari blocks cross-site tracking by default (via the Prevent cross-site tracking setting). In-memory storage fallback The data-enable-in-memory-storage can help you use the web SDK in some situations where cookies aren’t available. When set to true, the web SDK will store information in memory as a fallback. This setting allows for tracking when cookies aren’t available due to privacy settings, running on third party platforms, etc. If a user’s cookies are unavailable and they haven’t been identified, you won’t be able to track their anonymous activity; anonymous tracking is unavailable when reading from memory. But if a user’s cookies are available, anonymous tracking will work as it normally does. Function reference The web SDK exposes the following functions. Function automatic anonymous description _cio.identify Identifies a person and updates their attributes. Future calls reference this person. _cio.page Sends a page event, representing a “pageview”. _cio.track Sends a custom event, representing a person’s activity on your website. _cio.reset Stop identifying a person. You might send this call when someone logs out or refuses tracking consent. _cio.on Listen for an in-app message event. _cio.off Stop listening for an in-app message event. Use the JavaScript SDK via Google Tag Manager (GTM) You can inject our JavaScript snippet and use in-app messaging features through Google Tag Manager. Install the snippet Log into Google Tag Manager and go to the container for your website. Create a new tag and select Custom HTML as your tag type. Paste the the Customer.io JavaScript snippet in the HTML text area. Make sure to replace YOUR_SITE_ID with your workspace site id. Go down to the Triggering section and add the All Pages trigger. Or, create a custom trigger that covers the individual pages or sections of your website or app that you want to track events from, collect page views from, and show in-app messages on. Save the new tag with your preferred tag name. Identify people Create a new tag and select Custom HTML as your tag type. Add the javascript code that identifies the people you want to message. This example assumes that you have access to a User ID variable in your GTM workspace. Your actual implementation will differ based on how your GTM container is configured. <script type="text/javascript"> var profile_id = {{User ID}}; if (profile_id && profile_id != 'undefined'){ _cio.identify({ id: profile_id }); } </script> Below the HTML text area are Advanced Settings. Expand the section and look for Tag Sequencing. Select the first checkbox Fire a tag before… and select the tag that you created in the previous section. Go down to the Triggering section and add the All Pages trigger. Or, create a custom trigger covering just the pages or sections of your website or app that you want to track events, collect page views, and show in-app messages. Save the new tag with your preferred tag name. Preview and test Click GTM Preview and go to a page or screen where you would expect to see user activity. Log into Customer.io go to the Activity Log. Here you should see pageviews generated by the Javascript tracking snippet and identified to your test profile from step 1. Submit and publish When you’ve verified the integration, submit the changes in GTM and publish the new version. --- ## Identify people URL: https://docs.customer.io/integrations/data-in/connections/javascript/legacy-js/identify/ Identifying people sends their data to Customer.io, and makes future calls reference the identified person. How it works Identifying people adds them to, or updates them in, your workspace, and makes them eligible to receive in-app messages. After you identify someone, calls to the SDK will automatically reference the identified person until you identify someone else or send a reset call. Before you identify someone, we’ll attribute their activity on your website—page and track events—to an anonymous user. When you identify a person, we’ll automatically associate their anonymous activities with their profile in your workspace. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs out or stops using your app for a significant period of time. sequenceDiagram actor a as Website visitor participant b as Your Website participant c as Customer.io a->>b: Person visits page note over a,c: Visitor is anonymous and cannot receive messages b->>c: anonymous page event a->>b: person logs in b->>c: identify person note over a,c: Future calls represent identified person. Anonymous activity attributed to identified person c->>c: associate anonymous activity with person a->>b: person adds item to cart b->>c: track event/trigger campaign c-->>a: person enters campaign/receives messages a->>b: person logs out note over a,c: Visitor is anonymous again Identify a person Identifying a person creates or updates a person in Customer.io, and makes future calls reference this person. For example, after you send an identify call, any track calls you make will be associated with the identified person. You identify a person with the _cio.identify function. Your identify call must include an id. But in most cases, an id can either be a person’s canonical identifier or an email address. Learn more about using email address as an identifier below. When you first identify someone, we strongly recommend that you send created_at—a timestamp (in seconds since epoch) representing the moment you first identified and created a person. You shouldn’t pass this value with subsequent identify calls, or you’ll overwrite a person’s created_at attribute. You can also send in additional attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that are of value to you. In the above example we send first_name, last_name and plan_name attributes. You can use these attributes to segment your audience or to personalize messages for people. _cio.identify({ id: 'YOUR_USER_ID_HERE', // Required to identify a person. // Strongly recommended when you first identify someone created_at: 1339438758, // When a person first signed up in Unix epoch format. // Example attributes (you can name attributes anything you want) email: 'user@domain.com', // Email of the currently signed in user. first_name: 'John', // First name and last name are shown on people pages. last_name: 'Smith', plan_name: 'premium' }); We also have a Custom forms JavaScript snippet that automatically identifies people when they fill out a form on your website. See Custom form integrations for more information. Using email as an identifier In most cases, you can use an email address as a person’s unique identifier. While our JavaScript snippet requires an id value, you can pass an email address as an id and we’ll detect it as such. When you use an email address as an id and a person does not exist, we’ll create that person and set their email attribute. This leaves their id attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. open, so you can assign it later. This is helpful for people who you might identify as leads (by email address) before they become a customer and generate an ID in your backend systems—like someone who signs up for a webinar by email address but hasn’t yet purchased your product. If an id is a non-email address value, you can also set an email attribute. _cio.identify({ // Use email as ID id: 'cool.person@example.com', // Strongly recommended when you first identify someone created_at: 1339438758 }); flowchart LR a(_cio.identify)-->b{Does person exist?} b--->|yes|c(Update person) b-->|no|d{What is id?} d-->|id|e(Add person with id) d-->|email|f(Add person with email id is empty) Automatically identify people who click tracked links By default (for workspaces created after July 12, 2021), Customer.io automatically appends a _cio_id parameter containing a person’s cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). to tracked linksA link in a message that logs when a person clicks it. You can gather metrics for tracked links and use them to determine your audience’s level of engagement.. If your tracked link sends people to a webpage containing our JavaScript library, which we recommend, you’ll need to append links with ajs_uid=cio_{{customer.cio_id}} to automatically identify people. See our JavaScript documentation for more information. If your tracked links send people to a webpage containing our legacy JavaScript snippet, the snippet automatically identifies people. Even if you don’t use our JavaScript Snippet, you can still take advantage of the _cio_id parameter in tracked links to identify people. If you integrate directly with our API or one of our libraries, you can identify people using cio_<_cio_id-param-value> rather than a person’s ID or email address. To change or disable this setting: Go to Workspace Settings > URL Parameters. If you already have URL parameters enabled, click Settings; otherwise, click Get Started. Toggle Add _cio_id URL parameter. This setting affects messages you send after you enable or disable it. It does not affect messages that you’ve already sent. Support for arrays in identify calls To maintain the integrity of arrays in identify calls and support complex JSON attributes—like if you want to set relationships on people—you need to make sure that the JavaScript snippet includes the line: t.setAttribute('data-use-array-params', 'true');. If you comment this line out or set it to false, the web SDK reshapes arrays as objects with integer keys. For example, the following event with an array results in a payload formatted as an object. Identify callReshaped payload _cio.identify({ my_array: ['one', 'two'] }) { "my_array": { 0: "one" 1: "two" } } Session management with browser cookies Our web SDK sets cookies to determine whether someone is anonymous or has already been identified. _cioanonid: contains an anonymous ID that the tracking snippet sets automatically on a person who has not been identified yet. When you identify a person, we associate their anonymous activity (like page views) with the identified person. _cioid: contains the id used when you call _cio.identify with the tracking snippet. Once set, these cookies persist until they expire - after 365 days or after a user clears their cache, for instance. Some browsers are shifting away from supporting 3rd-party cookies, but because these two cookies are only set for your domain, browsers will not block them. Keep in mind, some browser extensions may still prevent our JS snippet from loading, which would prevent you from identifying people or our snippet from setting cookies. If you use a third party platform like Shopify, you may want to enable the data-cross-site-support or data-enable-in-memory-storage parameters to support your implementation. See Cross Site Cookie Support and In-memory storage fallback for more information. Update a person’s attributes To update an existing user’s attributes, just send the _cio.identify call again. You must include a valid identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. value—normally id or email—and any attribute you want to set or change for that person. If the attributes already exist in their profile, we overwrite them. If your request includes new attributes we add them to the profile. Relate people to objects Objects are grouping mechanisms in Customer.io, like accounts that people belong to, flights they’ve booked, or online courses they’ve enrolled in. You can relate people to objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. using the cio_relationships array in your identify call. This call contains a single action, letting you add_relationships or remove_relationships. The relationships array can contain any number of object relationships that you might want to set for the identified person. _cio.identify({ id: 'userid_34', email: 'customer@example.com', cio_relationships: { action: "add_relationships", relationships: [ { identifiers: { object_type_id: "1", object_id: "acme" } } ] } }); Stop identifying a person When a person logs out of your website or app, you can clear the session by calling _cio.reset(). _cio.reset(); If you want to identify a new person—like when someone switches profiles on your website, etc—you can simply call _cio.identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track and page events URL: https://docs.customer.io/integrations/data-in/connections/javascript/legacy-js/events/ You can send events for people who visit your site, representing their activity on your website. You can also send `page` events to track the pages your audience visits. How it works You can send events representing the things people do, and the pages people visit, on your website. The events that you capture with our web SDK can trigger campaigns and supplament user information. You can send events before you identify a person. We call these “anonymous” events. When you identify your web visitor, we’ll automatically associate anonymous events with the person you identify. flowchart LR a[anonymous person visits page]-.->b[anonymous page event] c-.->d[anonymous track event] a-->c[person adds item to cart] c-->e[person logs in]-->|_cio.identify|f[identified person] b-.->|associate page with profile|f d-.->|associate event with profile|f What’s the difference between track and page events? There are two different functions that send events to Customer.io: track and page. The page function is specifically for “pageview” events, and the track function is for all other kinds of events. Under the hood, page events have the same structure as events you send with track, except for a couple of minor differences: page calls are automatic: if you don’t disable data-auto-track-page, the web SDK sends page calls automatically on load. In segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you can use page to segment people based on the URLs they visit and track events to segment people based on the names of the events they perform. Send custom events with track You may want to track custom events like "watchedIntroVideo" or "addToCart". Like most requests, track takes two parameters: (Required) the event name (Optional) an object containing additional data about the event. _cio.track("event_name", {string_data: "value", number_val: 1}) _cio.track("added_to_cart", { price: "23.45", item_type: "shoes", sku: 12345 }) Handling arrays in events By default, we maintain the integrity of arrays in event calls. But, if you want to convert arrays to objects, you can comment out the t.setAttribute('data-use-array-params', 'true'); line in the JavaScript snippet or set it to false. If this setting is commented out or set to false, the javascript snippet reshapes arrays as objects with integer keys. For example, the following event with an array results in a payload formatted as an object. Track callReshaped payload _cio.track('test_event, { my_array: ['one', 'two'] }) { "my_array": { 0: "one" 1: "two" } } You can also override the data-use-array-params setting from the JavaScript snippet on any individual track call using the useArrayParams boolean. _cio.track('my_event', { my-array ["item1", "item2"]}, { useArrayParams: true }); Page view events By default the web SDK automatically captures pageview events whenever it loads. For most websites, this means that you’ll capture page view events every time a person goes to a new page. These automatically-captured events use the page URL pathname (new URL(window.location.href).pathname) as the event name, excluding query strings, and they gather other basic user information like the user agent and window dimensions. This appear in your activity log or on a specific person’s activity as a page event. You can use these events to trigger campaigns, add people to segments, etc. If you don’t want to automatically track the pages your audience visits, or you want to send your own page events, you can disable automatic page tracking. Send your own page events You can pass your own page events, independently of automatically-tracked events. You might need to do this if you install our snippet in a single page application. Page events take two optional parameters—the page name and an object containing data about the event. If you leave the page name empty, we use the current page pathname (new URL(window.location.href).pathname), excluding query strings. _cio.page("pageName", {"extraDataObject": "moreData"}); Disable automatic page tracking You can turn off automatic page vie tracking by setting t.setAttribute('data-auto-track-page', 'false'); in the JavaScript snippet. See configuration options for more information. Anonymous events You can send track and page events before you identify the people who visit your website. This lets you attribute activity to people before they log in, sign up, or otherwise identify themselves. If you haven’t disabled anonymous event merging, the JavaScript snippet automatically associates events (that occurred in the past 30 days) with people when you identify them. See anonymous events for more information.  Anonymous event merging is on by default If you created your workspace before July 2021, you must enable Anonymous event merging. Otherwise, anonymous events are not associated with people you identify. URL parameters in pageview events The JavaScript snippet captures URL parameters as event data, helping you capture additional information about page views—a user’s search terms, products that a person has filtered for, dates that a user is interested in, etc. So, for example, a url in the format https://example.com/products?property1=stuff&property2=things. We capture property1 and property2 as event data. The resulting page view event looks like this: { "type": "page", "name": "https://example.com/products/", "data": { "property1": "stuff", "property2": "things" } } Create segments based on page views Go to Segments. Click Create Segment and create a Data-driven segment. Set a Page condition with URL matching and then set the URL you want to segment on. You can create segments based on pages users have or have not viewed. You can also click Refine, to segment page views based on a time frame or the number of times a person viewed a page e.g., “has not viewed in 30 days”, or “has viewed at least once”. You can also segment based on URL parameters that you captured by your event. The JavaScript snippet automatically captures URL parameters; if you send events using the Events API you must specify URL parameters in the data object. --- ## In-app messages URL: https://docs.customer.io/integrations/data-in/connections/javascript/legacy-js/in-app/ You don't need to do anything besides install the web SDK and identify people to send in-app messages to your website visitors. However, you may want to understand how some in-app features work to better target and display messages. How it works In-app messages for your website work differently than push notifications would: they require JavaScript, and they don’t go through a push notification service (like APNs or FCM). This means that as long as you identify your web visitors, and those visitors don’t disable JavaScript, you can send in-app messages to people. sequenceDiagram Participant a as App User Participant b as SDK Participant c as Customer.io c->>c: Trigger in-app message c-->>b: If app isn't open, hold until user opens app a->>b: User opens app b->>c: Identify User c->>b: Send in-app message b->>a: User sees in-app message Send an in-app message In general, you don’t need to do anything special with the web SDK to send an in-app message. You simply need to identify a person before you can send a message; you can’t send in-app messages anonymously. That said, to send an in-app message, you’ll need to do the following things. Because most of these things happen outside the SDK, we’ve linked to relevant documentation. Enable in-app messaging Set up message templates Set up in-app messages in your campaigns or broadcasts Identify visitors to your website. You cannot send in-app messages anonymously. Page rules and in-app messages Page rules help you determine the pages where people can encounter your messages, ensuring that they’re relevant to the pages people visit on your website. Page rules also help avoid conflicting messages by distributing messages to the pages where they’re most relevant to your audience. If you send two messages of the same priority without page rules, they’ll appear one after the other. When you set a page rule for the Web platform, we use the page URL pathname (new URL(window.location.href).pathname) unless you pass page calls with a different name parameter. We don’t support query strings. For example, an include rule for https://example.com/*/billing allows a message to appear on https://example.com/ui/billing or documents about billing under https://example.com/billing. See the page method reference for more information.  Use * to represent all pages When you select a channel, you have to enter a page rule. But, if you want to show a message on every page on your website or app, you can simply enter *. Page rules for single-page applications If your website is a single-page app (SPA), you must send page calls to tell the SDK what “page” a person is on. Listen to in-app message events The JavaScript snippet exposes several in-app message events that you can listen to via the _cio.on and _cio.off API. All events have a payload object with a type property that indicates the type of event and detail property that contains data corresponding to the event type. Make sure you add "on" and "off" to the list of functions you call on _cio in the snippet. <script type="text/javascript"> var _cio = _cio || []; (function() { var a,b,c;a=function(f){return function(){_cio.push([f]. concat(Array.prototype.slice.call(arguments,0)))}};b=["load","identify", "sidentify","track","page","on","off"];for(c=0;c<b.length;c++){_cio[b[c]]=a(b[c])}; var t = document.createElement('script'), s = document.getElementsByTagName('script')[0]; t.async = true; t.id = 'cio-tracker'; t.setAttribute('data-site-id', 'YOUR_SITE_ID'); t.setAttribute('data-use-array-params', 'true'); t.setAttribute('data-use-in-app', 'true'); t.src = 'https://assets.customer.io/assets/track.js'; //If your account is in the EU, use: //t.src = 'https://assets.customer.io/assets/track-eu.js' s.parentNode.insertBefore(t, s); })(); </script> Message opened event This event is triggered when an in-app message is shown to the user. The detail property always contains the messageId property whereas the deliveryId is not present if it’s a test message. Handle a message opened event Handle a message opened event const onMessageOpened = function (event) { console.log('Type: ', event.type); console.log('Message Id: ', event.detail.messageId); console.log('Delivery Id: ', event.detail.deliveryId); // not present in test messages }; // run the listener everytime message is shown _cio.on('in-app:message-opened', onMessageOpened); // run the listener only once _cio.on('in-app:message-opened', onMessageOpened, { once: true }) // turn off the listener _cio.off('in-app:message-opened', onMessageOpened) Event object Event object type string Defines the event type.Accepted values:in-app:message-opened Message dismissed event This event is triggered when an in-app message is dismissed by the user. Handle a message dismissed event Handle a message dismissed event _cio.on('in-app:message-dismissed', function (event) { // handle dismissed message }); Event object Event object type string Defines the event type.Accepted values:in-app:message-dismissed Message action event This event is triggered when the user performs an action on an in-app message. Handle a message action event Handle a message action event _cio.on('in-app:message-action', function (event) { // handle action // optional call to dismiss the message after handling the action event.detail.message.dismiss(); }); Event object Event object detail object actionName string The name of the action specified when building the in-app message. actionValue string The type of action that triggered the event. deliveryId string Delivery Id for the corresponding in-app message (not present in test message). messageId string Identifier string of the in-app message. type string Defines the event type.Accepted values:in-app:message-action Message error event This event is triggered when an in-app message produces an error. Handle a message error event Handle a message error event _cio.on('in-app:message-error', function (event) { // handle error }); Event object Event object type string Defines the event type.Accepted values:in-app:message-error --- ## Content Security Policy (CSP) URL: https://docs.customer.io/integrations/data-in/connections/javascript/legacy-js/content-security-policy/ CSP is an optional security protocol (specified via a header or meta data) that requires a web app to identify all the content providers that the web app will make use of. This page outlines the minimum required directives to enable full Customer.io functionality. Required CSP Directives for Accounts on the US Data Center script-src assets.customer.io code.gist.build customerioforms.com 'unsafe-inline' 'unsafe-eval'; connect-src track.customer.io customerioforms.com *.api.gist.build *.cloud.gist.build; frame-src renderer.gist.build code.gist.build; style-src code.gist.build 'unsafe-inline'; img-src track.customer.io; Required CSP Directives for Accounts on the EU Data Center script-src assets.customer.io code.gist.build eu.customerioforms.com 'unsafe-inline' 'unsafe-eval'; connect-src track-eu.customer.io eu.customerioforms.com *.api.gist.build *.cloud.gist.build; frame-src renderer.gist.build code.gist.build; style-src code.gist.build 'unsafe-inline'; img-src track-eu.customer.io; Glossary of Directives Directive: script-src Host Description assets.customer.io Location of the Customer.io javascript file, referenced in the installation snippet. code.gist.build Location of the services that enable In-App Messaging. customerioforms.com Location of the services that enable Connected Forms. Directive: connect-src Host Description track.customer.io Required for event communication. customerioforms.com Required for Connected Forms events. *.api.gist.build, *.cloud.gist.build Required for In-App Messaging. Directive: frame-src Host Description renderer.gist.build, code.gist.build Required for loading in-app messages. Directive: style-src Host Description code.gist.build Required for adding the in-app message styles to the page. Directive: img-src Host Description track.customer.io Required for pageview tracking. Frequently Asked Questions Are unsafe-inline and unsafe-eval directives required? Yes, unsafe-inline and unsafe-eval directives are required for javascript and CSS to behave properly. Can I exclude the directives relating to Connected Forms or In-App Messaging if I don’t plan on using those Customer.io features? Yes, you can exclude the directives relating to either or both features if you don’t plan on using the features. For Custom Forms, the directives to exclude are: customerioforms.com eu.customerioforms.com To remove In-App Messaging directives, exclude: *.api.gist.build *.cloud.gist.build renderer.gist.build code.gist.build --- ## Add a Classic Track API integration URL: https://docs.customer.io/integrations/data-in/connections/classic-api/journeys-sources/  Just getting started? Use our newer source integrations instead! This page is about our Track API and integration sources that rely on the Track API. If you’re new to Customer.io, you should integrate with our newer API or native integrations instead. Our newer API and integrations support batching, retry logic, and are where we’re focusing our development efforts. To send data from the Journeys Track API, or integrations based on the Track API, to a destination outside of Customer.io: Go to > Workspace Settings > API and webhook credentials. Click Create Track API Key. Give your credentials a name and select the workspace you want to use them in. The name helps you find and differentiate between different API credentials; you might name them for users, environments, or the services you use them for. Click Create Track API Key one last time, and you’ll have your credentials. You’ll use your Site ID and API Key as a username and password for basic authorization when you call our API. You can use these credentials with partners like Segment. If you go to the Integrations page, you’ll see your new credentials listed under Customer.io API: Track as Journeys API: <credential name>. We list each set of credentials as its own integration On the Integrations page, we’ll show each individual set of Track API credentials as its own integration in the format: Customer.io Track API: Name. In general, you’ll want to use different credentials for different purposes and environments. For example, you might want to have a separate set of credentials for your production website and your testing environment—so you can easily differentiate between calls from each environment, connect them to different integrations, and so on. By default, these credentials send data directly to Customer.io, but you can also forward data to other data out integrations—like your analytics platform, CRM, and other tools. Track API Credentials Track API integrations What’s the Track API? If you’re asking this question, you probably don’t need to worry about it. Check out our integration directory or our API documentation and start integrating with Customer.io! The Track API is an older (but still great!) way to get data into Customer.io. The major difference between it and other integrations has to do with the API keys you’ll use (the Site ID and API Key) and the names of a few parameters or endpoints . What if I want to transform my data? When you connect your Journeys integration traffic to a destination, we automatically transform your data to the newer Pipelines API format to support our data-out integrations without writing any code. --- ## Advanced: transform data URL: https://docs.customer.io/integrations/data-in/connections/classic-api/cio-journeys-api/  Try using one of our SDKs or the Pipelines API instead Changing actionsA block in a campaign workflow—like a message, delay, or attribute change. for data coming into Customer.io can be complex. If you’re just getting started, we suggest that you format your data based on our default actions and don’t change them. These integrations are based on the Pipelines API already, and our libraries make it easier to understand and manage action mappings for destinations. How it works By default, we automatically map traffic from integrations based on our Track API to our newer Pipelines APIs to support downstream integrations—services outside of Customer.io. But, if you want more control over how we map incoming data to your workspace, you can edit your workspace’s actionsA block in a campaign workflow—like a message, delay, or attribute change.. Be careful: changing the default workspace mappings means you’re no longer processing data as explained in our documentation. flowchart LR a(Incoming Track API Call)-->b{Are actions edited (Not recommended)"} b-->|no, process track calls normally|c(Customer.io processes call) c-->d(Transform to Pipelines API) d-->e(Send traffic to destinations) b-.->|"yes, transform call first (advanced)"|h(Transform to Pipelines API) h-.->i(Customer.io processes call) i-.->e Edit your workspace’s actions Updating your workspace’s actions determines how we handle incoming data, including whether we process it at all. While you can change the default mappings, keep in mind that changing mappings affects the data that goes to your workspace! Go to Integrations. Hover over the icon in an integration that sends data to Customer.io and then select your workspace’s name. Go to the Actions tab. Click and click Edit to change an action. From here, you can edit or add actions—including filtering the calls that go to your workspace. For example, you may want to handle an event with a specific name differently from other events. When you edit an action, you can change: Trigger: the conditions that trigger the action. Common conditions include the type (like track or identify), the event name, or an Event Property. Data Structure: the transformations for data matching the trigger. In general, most changes are typically limited to the Optional Data section—additional properties or attributes you want to set when we receive an event or a call matching your trigger conditions.  Review the default mappings Before you change actions for your workspace, review our default mappings to understand how we translate incoming data to the newer format. Changing these mappings affects the way we process data in your workspace! --- ## Invalid Track API Requests URL: https://docs.customer.io/integrations/data-in/connections/classic-api/invalid-api-requests/ When you send an invalid request to our Track API, we respond with a `400 Bad Request` including additional details. You can use these details to troubleshoot your request. { "meta": { "error": "name cannot be blank" } }  We strongly encourage you to implement some logging on your end to capture our responses to any invalid API requests your integration may send by mistake. This will give you something to look back on when you get notifications from our team about such errors. Common Error Messages What you see below is the list of error messages shown when API calls are rejected due to limits mentioned in our API documentation. In addition to the responses we give to your API calls, our system is set up to automatically notify you about API limit infractions we find problematic. When available, you will be given samples of the data we received that exceeded the limits. Note that the middle of the sample data we send may be truncated (" …<truncated>… “) in order to limit the size of the exported error data. To prevent data loss, you’ll want to address the issues mentioned in that file and/or listed below as soon as possible whenever they come up. Errors related to identify calls (sending People profile data) id attribute must be present id attribute must be a string value id attribute cannot be an empty string cannot identify more than 300 attributes in one request attribute name cannot be longer than 150 bytes value for attribute ‘ATTRIBUTE-NAME’ cannot be longer than 1000 bytes Errors related to track calls (sending event data) event name must not be blank event name cannot be longer than 100 bytes event name must be less than 500 characters event data must be a hash (e.g., send "data":{} instead of "data":"") event data cannot be longer than 100000 bytes Why only 300 attributes at a time and only 1000 bytes per attribute value? These limits have been put in place to ensure the performance and reliability of your Customer.io account. What happens if I don’t change anything? If you get these errors and do not fix your integration so that, for example, it only sends a maximum of 300 attributes per identify call and a maximum of 1000 bytes for attribute values, these types of requests will continue to fail. If you have a use-case which needs these limits relaxed and it doesn’t interfere with our ability to provide the service, we can raise them on request. Please contact us so that we can discuss your use-case in detail. --- ## Getting Started URL: https://docs.customer.io/integrations/data-in/connections/hubspot/getting-started/ How it works This integration sends data from HubSpot to Customer.io on regular intervals called syncs. You’ll set up syncs for each kind of data you want to use in Customer.io. As you set up syncs, you’ll map your HubSpot data to Customer.io. These will be the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you use to personalize messages and segment members of your HubSpot audience. Set up your HubSpot integration When you set up your integration, you’ll configure your first Sync—the type of data that you want to send to Customer.io. We suggest that you start with Contacts—the people you’ve identified in HubSpot. Go to Integrations. In the Directory tab, pick the Data In HubSpot integration Give the integration a friendly Name and click Connect HubSpot. Here’ you’ll pick the account you want to connect to Customer.io and then click Choose Account. HubSpot will list the permissions you’ll grant to Customer.io. If you accept, click Connect app. While we have access to your data, it’s important to note that the scope of your data is limited to your workspace and account. We don’t share it with anyone else. Click Next: Create Sync. Pick the kind of data you want to sync to Customer.io. We suggest that you start with Contacts because it’s the easiest and most natural data type to send into Customer.io and other services. By default, we’ll bring in all contacts, but you can set up filters to limit your sync to Marketing Contacts or other people you want to bring into Customer.io. See Filter incoming data for more information. In the three fields below Sync, set up the data you want to bring into Customer.io. If this is your first sync, and you’re syncing Contacts, you probably only need to update Fields to sync. Unique identifier: This is the value we’ll use to identify individual people or objects. If HubSpot is your source of truth, you should leave this as id. If that value doesn’t make sense for your setup, see setting identifiers for more information. Fields to sync: This is the data you want to bring into Customer.io. You can toggle the fields you want to bring into Customer.io on or off and click Settings > Edit to change the names of the fields in Customer.io. See mapping fields for more information.  We automatically remove the properties. prefix from HubSpot field names By default, HubSpot field names contain a properties. prefix like properties.fieldName. When we bring data into Customer.io, we remove this prefix to make the field names easier to use in Customer.io, so properties.fieldName becomes fieldName. Pipelines format: Unless you know the data type you want to use in Customer.io, don’t change this value. See Pipelines format for more information. Give your sync a Name and configure the Frequency—how often you want to sync data from HubSpot to Customer.io. Click Next: choose destinations. Select the places you want to connect your HubSpot data to. By default, we’ll sync data to your workspace (shown as Journeys Workspace), but you can also send your data to other destinations—like your data warehouse or analytics platforms. Click Enable HubSpot. Your integration is now set up and your first sync is running! You can go to your integration’s Syncs tab to see the status of your sync and create more syncs to send other kinds of data from HubSpot into Customer.io. Set up additional syncs During the initial setup you created your first sync. Now, you can set up additional syncs to import other kinds of data from HubSpot. Just go to your HubSpot integration’s Syncs tab and click Add Sync. You likely synced your Contacts in your first sync. For other syncs, you’ll probably sync Companies or Deals. You’ll need to perform a few extra steps for these data types. See Syncing Custom Objects for more information.  Don’t try to sync forms or form submissions yet We’re working on a solution to make forms easier to sync from HubSpot. In the meantime, you’ll have a better experience handling submissions as webhooks from HubSpot instead. See Syncing Forms for more information. Syncing custom objects When you sync non-people data, like Companies or Deals, you’ll use the custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. Data Pipelines format. You need to define your new data type in Customer.io and tell us how to relate it to contacts (or other people) in Customer.io.  You can filter for specific objects you want to sync to Customer.io If you don’t want to bring all companies, deals, or other kinds of HubSpot data into Customer.io, you can set up filters. For objects, you’ll likely set up an actionA block in a campaign workflow—like a message, delay, or attribute change. where type is group, context.sync.recordType is companies (or whatever data you want to sync) and then the other criteria you want to filter on. See Filter incoming data for more information. In the Sync step, select Identifier. Determine the Relationship ID: in most cases, this is just the Contacts data type—because contacts represent people in HubSpot. If you’ve changed this value, you’ll need to select the field that represents your People in Customer.io—this could be an email address or another value. Select the Fields you want to sync for this data type. These fields are the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. associated with your objects in Customer.io. For example, if you sync Companies you might pick fields like the company name, address, and industry. Make sure that the Data Pipelines format is set to Custom Object. When you move on to the Destinations step, select Customer.io Journeys. Here you’ll select the HubSpot data you want to sync to Customer.io as a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the custom object doesn’t already exist, click New Object Type and set the singular and plural forms—like Company and Companies. Custom Objects appear in the left-side navigation in Customer.io. If you created a new object as a part of this process, it’ll appear in the navigation when your first sync finishes. The first sync and subsequent syncs When you set up a new sync, the first sync interval captures all of the data of the specified type. Subsequent syncs capture records that changed since the previous sync. This means that your first sync may take significantly longer than subsequent syncs, as we pull all your HubSpot data into Customer.io. For example, when you sync contacts, your first sync will send all of the current contacts in your HubSpot environment. The next sync will gather all changes to contacts—all your new contacts, updates to existing contacts, and deleted contacts—since the previous sync. Updating syncs When you update a sync to add or remove fields, your updates take affect at the next sync interval. This means that your changes don’t affect data that you’ve already sent from HubSpot (or any syncs in progress). For example, if you sync contacts to Customer.io and add the birthday from the sync, the next sync will add birthdays for new contacts but it won’t update your existing contacts—because nothing changed in HubSpot. To update all data you’ve passed from HubSpot to Customer.io (and other places you send your HubSpot data to), you can Resync all data. This sends all of your corresponding HubSpot data through Customer.io again, updating all of your records with the changes you made to your sync. You typically only need to do this when you add fields to a sync. Following our birthday example above, if you use the Resync all data option, we’ll set the birthday attribute for all HubSpot contacts synced to Customer.io—both new contacts and contacts that have already been synced from HubSpot. We’ll present this option whenever you update a sync. You can also manually resync your data by going to the Syncs tab, clicking next to the sync you updated, and then clicking Resync all data.  Resyncing doesn’t remove existing data from Customer.io Resyncing data only adds data from HubSpot. It doesn’t delete data in Customer.io. For example, if you’ve removed a birthday field from a sync, resyncing won’t remove the birthday attribute from contacts that have already been synced from HubSpot. --- ## Map HubSpot data to Customer.io URL: https://docs.customer.io/integrations/data-in/connections/hubspot/mapping-to-customerio/ When you set up a HubSpot integration with Customer.io, you'll need to map your HubSpot data to people, custom objects, or events. In most cases when you map a type of HubSpot data to Customer.io, you should use our default settings and focus only on the Fields that you want to bring into Customer.io. But if the default settings don’t quite work for your use case, you can update mappings in a few different ways: The primary identifiers for the data you bring into Customer.io (its id in HubSpot, email address for contacts, or another field). The fields you want to sync to Customer.io for your HubSpot data type—these are the attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you’ll use, like a contact’s name or a company’s address. The “Data Pipelines format” for your data in Customer.io: whether your data represents people, events, or custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Mapping fields to Customer.io For any kind of data you send from HubSpot to Customer.io, you’ll need to select the Fields you want to accompany your data. For custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and people, these are called attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. For events, these are event properties. By default, we only map the record ID to Customer.io. You need to pick the fields you want to sync to Customer.io. For each field, you’ll see properties.fieldName. We automatically rename these fields without the properties. prefix. But, beyond that, you can rename the fields to fit your schema in Customer.io. For example, HubSpot may have a properties.mobile_phone field. If you send SMS messages, you might want to rename this field to phone; that’s the field we typically use for phone numbers when sending SMS messages. Data Pipelines format: how we represent HubSpot data in Customer.io HubSpot has highly structured data: your contacts, companies, and other things are all separate. Customer.io has generic concepts like People and custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and you’ll map your HubSpot data to the right format for Customer.io. We do our best to automatically map your HubSpot data to the right format for Customer.io when possible. These are usually cases where the data format is obvious: your HubSpot contacts are people, and your companies are custom objects that are related to people. But you know your data best. You might need to set or change these mappings to fit your needs and use cases! HubSpot record Typical Data Pipelines format Contacts People Companies, Deals Custom Objects Tasks, Orders Events  Don’t worry about the Relationship type Some platforms (like Customer.io) treat relationships between people and groups (like companies) as their own information type. HubSpot doesn’t. You can relate companies to contacts, but there’s no separate data for the relationship, so you can safely ignore this option when you set up your syncs. Map a HubSpot record to a custom object In most cases, we set object names automatically for you. The Custom Object Name is a value we set in Customer.io that tells us what kind of HubSpot data your object originates from. If you create a new sync and the custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. doesn’t exist yet, don’t worry: you’ll create it in the Destination step when you set up your sync. Custom Object ID When you set up a sync using the Custom Object format, you’ll select the HubSpot value that represents a unique object in Customer.io. In most cases you shouldn’t change this setting from the default id value. You should only change this value if you store a unique value in HubSpot that represents a record in a source of truth outside of HubSpot—like a backend database ID. Related Record ID When you set up a sync using the Custom Object format, you’ll select a value for the people related to the HubSpot object you want to sync to Customer.io. In most cases, this is the contacts data type—because contacts represent people in HubSpot. How do I know if data is a person, event, or an object? We’ve created a little decision tree to help you map data accordingly, but in short: If you want to send messages to it, or you want to log events it performs, it’s a person. In Customer.io, you’re concerned with individual people. People can receive messages and perform events. Objects themselves can’t receive messages or perform events. Think of it this way: you’ll never send a message directly to a deal logged in HubSpot; a company can’t visit a page on your website. Those are things people do! If something is related to people and the data has a lasting impact, it’s an object. Custom objects are things that people are related to—like an account they belong to, a company they work for, or an opportunity they represent. They’re like people, in that they have attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and, unlike events, they’re important long after they initially appear in your workspace. If something represents people’s activities, it’s an event. Events are things that people do, like when a user visits a page on your website, adds an item to their shopping cart, or watches a video. Events are really useful for responding to people’s activities in an automated way, like sending a calend.ly link to people who want to hear from you about your product. flowchart LR a{Will you send it messages?} a--->|yes|b(It's a person) a-.->|maybe|c{Can it perform events?} c--->|yes|b a-.->|no|d{Is it related to more than one person?} d-->|yes|e(It's an object) c-.->|no|d d-.->|no|f(It's either an event or you don't need it) Mapping HubSpot Activities to events HubSpot has a concept of Activities—interactions associated with contacts, companies, deals, and other records. In Customer.io, we call these events, and they can only be performed by people. Activities (and similar data) in HubSpot represent the things people do: like having a call with a representative or placing an order. When you send this data to Customer.io, you can trigger campaigns to automate communications with your HubSpot contacts based on their activities.  You can’t map events to custom objects In HubSpot, non-people records like companies and deals can perform activities. But in Customer.io, only people can perform events. Setting identifiers In general, you don’t need to touch the identifier setting. We recommend that you use the default HubSpot ID as the identifier for the data you bring into Customer.io. In HubSpot, each entity has a unique id value. By default, this is the value we use to represent your people or custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. As long as you don’t change this value, you can easily send data back and forth between HubSpot and Customer.io using our HubSpot data-out integration. If you use a different identifier for the data you bring into Customer.io, you’ll need to store the HubSpot id as an attribute in Customer.io. This is the only way we can reliably send data back to HubSpot from Customer.io. --- ## Filter incoming data URL: https://docs.customer.io/integrations/data-in/connections/hubspot/filter-incoming-data/ By default, all data you send into Customer.io shows up in your workspace. But for some integrations, you might want to filter out certain records, attributes, or event data. You can do this by updating your workspace's actions. How it works When you sync data from an integration like HubSpot to Customer.io, we re-shape it to match Customer.io’s data model through Actions. By default, your Journeys Workspace Actions are set up to process and use all of the data you sync to Customer.io. This works for most integrations. But what do you do if you want to filter out certain requests from HubSpot? You’d create a new Customer.io Journeys (Workspace) instance to handle data from these integrations independently from default settings! In your new Customer.io Journeys (Workspace) integration, you’ll modify Actions to handle requests from these integrations without affecting other integrations. flowchart LR a(Data sources)-->|Incoming Data|b{Does data source have its own filter?} b-..->|no|c(Data goes into Customer.io) b-->|yes|d{Does data match an action filter?} d-->|yes|c d-.->|no|f(Data doesn't go into Customer.io) Basic process to filter data You likely want to follow this process and set up filters before you add your new integration to your workspace—or at least before you finish setting it up and sending data to Customer.io. If you set up your source integration first, you might send unwanted data to Customer.io that you’ll have to remove later! Add a new Customer.io (Workspace) integration. Modify the Actions for the new integration to filter data from HubSpot. Connect your HubSpot integration to your new Customer.io (Workspace) instance. 1. Add a new Customer.io (Workspace) instance You’ll add a new Customer.io (Workspace) integration to store your filters. Setting up a separate integration here ensures that your filters don’t affect other integrations in your workspace; they’ll only affect your incoming HubSpot data. Go to > Workspace Settings > API and webhook credentials. Click Create Track API Key and give your credentials a name—you might want to name it something like “Filtering for ”. Now go to Integrations and click Create Integration. Select Customer.io (Workspace) and click Create. Give the new integration a name, like “Filtering for <integration name>.” Keeping the name aligned with the API credentials you created in the previous step can help you keep track of your filters. Enter the Site ID and API Key you copied in the previous step and click Next: Configure Data. Deselect the Messaging data integration and select the API credentials you created in the previous steps. This ensures that we don’t send any data through these filters yet. Click Save. Now you can modify your actions to filter data from your new integration. 2. Modify actions to filter requests When you look at your new Customer.io (Workspace) instance, you’ll see a list of actions. You’ll modify these actions to filter requests coming from HubSpot. For example, you might: Filter out requests that don’t contain specific traits or properties. You’ll do this by editing the Trigger for one or more actions. Filter for marketing contacts to only sync marketing contacts to Customer.io. You’ll do this by editing the Trigger for one or more actions. Set up filters for objects to handle specific objects (like companies, deals, and so on). You’ll do this by editing the Trigger for one or more actions. Add new actions to handle specific requests differently from others. You’ll do this by creating a New Action and editing the trigger for an existing action. Filter requests Most of the default triggers are based on the incoming request type—track, identify, page, screen, group, and alias. We also have a few additional triggers for specific events—like Object Deleted and User Suppressed. By default, these actions process any data matching the Type and Event name criteria. Here’s where you’ll add conditions to filter out requests that don’t contain specific traits or event properties. For example, when you sync Contacts from your HubSpot data source, you might not want to process user profiles that don’t contain a phone number. In this case, you would add a condition to the Identify action and add a condition where the Traits phone exists. That means that we’ll only process identify requests where the phone trait exists. Filter for marketing contacts If you use HubSpot’s Marketing Cloud, you have a concept of marketing contacts. You might want to filter these contacts into, or out of, Customer.io to make sure you contact the right people. You can do this by updating the identify action to filter on the hs_marketable_status trait. If hs_marketable_status is “true”, the contact is marketable. If it’s “false”, the contact is not marketable. Note that you can’t use the is true operator because hs_marketable_status is a string, not a boolean. Filters for objects You might want to create filters for different objects—companies, deals, and so on. To do this, you’ll set up your filter based on the recordType from HubSpot. The recordType represents the kind of data you bring in from HubSpot. To manage object filters, you’ll need to make two changes to your actions: Create a filter for the recordType type you want to filter. Exclude the recordType from other group actions. If you don’t do this, then the recordType you want to create a specific case for will get handled along with other objects! This would effectively override the filter you created for the specific object. For example, if you sync Companies from HubSpot, recordType will be companies. And if you wanted to filter requests for companies that have a phone trait, you’d filter on context.sync.recordType is companies and the phone trait exists. So you’d great a new Group action with this criteria. Then, on your existing group actions (sometimes called Create or Update Object), you’ll add a condition to the trigger where recordType is not companies. This ensures that you respect the filter you created for the companies record type! Step 1: create an action to handle the specific object Step 2: exclude the object from other group actions Add new actions to handle specific requests Imagine that we capture event_sign_up events from one integration, but we capture them as event_signup events from all our other integrations and we want to make the event name consistent. In this case, we would: Create a new action for the event_sign_up event and set the trigger to Track Event Name is event_sign_up. In the Data Structure, change The name of the event from $.event to event_signup and Save the action. Edit the existing Track Event action and add a condition to the trigger where Track Event Name is not event_sign_up. 3. Connect your HubSpot integration to your filters Now that you’ve set up your filters, you can connect your HubSpot integration and start syncing data to Customer.io. If you haven’t already, set up your new HubSpot integration. When you set up Syncs make sure that you include the fields or other data that you use to filter requests in the sync. For example, if you filter requests based on the phone trait, you’ll need to include the phone field in the sync. In the final step where you select the places you want to send your data, select your new Customer.io (Workspace) destination. Deselect the default Journeys Workspace destination. Click Enable Integration. Now your new integration is set up and will start accepting data from your sources. If you want to tailor the data that your sources send to destinations, go to your destination’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. tab. See Actions for more information. --- ## Deleting Data URL: https://docs.customer.io/integrations/data-in/connections/hubspot/delete/ We represent data you delete in HubSpot with events. You can use these events to determine how you handle deleted data in integrations downstream of Customer.io. But *in* Customer.io, you don't need to do anything to handle deleted data; we do it for you.  This information is for services outside of Customer.io Customer.io automatically handles deleted HubSpot data. You only need to pay attention to the information on this page if you send your HubSpot data through Customer.io to other services. How it works We use Semantic Events, events with special meanings, to handle delete operations when you sync data from HubSpot to Customer.io. For example, if you delete a contact in HubSpot, we represent the deletion in Customer.io as an event called Delete Person. flowchart LR a(Incoming HubSpot Data) a-->b{What kind of data is it?} b-->|person|c(Identify) b-->|object|d(Group) b-->|event|e(Track) b-.->|delete Here's where we use semantic events|f("Track event where the event name is Person/Object Deleted") Semantic events for delete operations The data-deleting operations you can perform from HubSpot correspond to simple event names. Delete Person: Removes a person Delete Object: Removes an object, like an account or opportunity Delete Relationship: Removes a relationship between a person and an object, like when a contact leaves a company, or an account transfers to a new account manager. You’ll notice that there’s no Delete Event operation. You can’t delete an event. An event is something that happened at a point in time. You can’t go back in time and stop that thing from happening! Remove an attribute from a person or object We don’t use events to remove or unset attributes. Instead, we set an attribute to an empty string or null to remove it in Customer.io. You may notice this in Customer.io if you check data for an individual sync. --- ## HubSpot forms URL: https://docs.customer.io/integrations/data-in/connections/hubspot/forms/ Our HubSpot integration doesn't currently make it easy to sync form submissions. We're working on that while we're in beta. But, if you're a HubSpot premium or enterprise user, you can send form submissions using a [HubSpot workflow](https://knowledge.hubspot.com/workflows/create-workflows) to support form submissions in the meantime. How it works You’ll set up a workflow in HubSpot that sends a webhook to Customer.io when someone submits your form. Then, in Customer.io, you can: Convert that webhook to an event that triggers downstream campaigns Store attributes on your audience’s profiles in Customer.io. These attributes can add people to segments, trigger campaigns, or personalize messages. flowchart LR subgraph HubSpot direction TB a(Person submits form)-->|Hubspot workflow|b(Generate webhook) end b-->c subgraph Customer.io direction TB c(Trigger campaign)-.->d(Generate event) c-.->e(Set attributes) d-.->f(Send follow-up messages) e-.->g(Add person to segment) end 1. Create your HubSpot workflow If you’re unfamiliar with HubSpot workflows, check out HubSpot’s documentation. In this case, we’ll create a workflow where the trigger is a form submission and the action is to send a webhook to Customer.io. In HubSpot, go to Automation > Workflows. In the upper right, click Create workflow and select From scratch. Select the Websites & media option and continue setting up your form submission trigger. Add the Send a webhook action to your workflow but don’t configure it yet. You’ll need to get your webhook address from Customer.io first. 2. Create your Customer.io webhook In Customer.io, you’ll create a new campaign. This will generate a webhook address that HubSpot will call when people submit your form. Go to Campaigns and click Create Campaign. Click Choose Trigger and select Webhook. Copy the webhook address and click Save and build workflow. 3. Finish setting up your HubSpot workflow Go back to your HubSpot workflow. In your Send a webhook action: Set the method to POST. Set the Webhook URL to the webhook URL you previously copied. Include the form field properties you want to send to Customer.io, or select Include all [object] properties to send all of the properties from the form submission. Now that you’ve set up your workflow in HubSpot, send a test form submission. This provides your Customer.io campaign with sample data that’ll help you map form data to attributes or event properties in Customer.io. When you send a test, you’ll see your sample data in your Customer.io campaign. 4. Finish setting up your Customer.io campaign Now return to your Customer.io campaign. Here, you can map the incoming form submission to an event that can trigger downstream campaigns, attributes that you store on your audience’s profiles in Customer.io—or both. Send an event: Add a Send Event action to your workflow. This converts the incoming webhook to an event that can trigger campaigns in Customer.io. Set the Event Name to a name that you want to use to trigger downstream campaigns or use in segments. Set Event Attribute names to the values you want to use to personalize downstream campaigns. Set the Value to the field values from the event. Set attributes: Add a Create or update person action to your workflow. Set the Identifier for the person you want to add or update based on your HubSpot form—a person’s email or their canonical ID. Click Add attribute and set the values you want to store on the person’s profile from the incoming form submission. --- ## About Reverse ETL URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/about-reverse-etl/ Reverse ETL (Extract, Transform, Load) extracts data from a data warehouse so you can take advantage of your data in services outside of Customer.io. You'll write a query determining the data that you want to send into Customer.io. For example, you might write a query representing `track` events so you can sync records from Snowflake to Mixpanel. How it works Our data warehouse and database “reverse ETL” (Extract, Transform, Load) integrations extract data from a data warehouse so you can take advantage of your data in Customer.io and other services downstream. These integrations help you leverage your big data storage in Customer.io. For example, you might write a query representing track events so you can sync records from Snowflake to Mixpanel. When you set up a reverse ETL integration, you’ll set up one or more syncs. This is the kind of API call you want to transform your data into (like track, identify, etc) and the query that returns the data you want to use in Customer.io. Each row returned from the query represents an API method. You’ll have to create a sync for each kind of method or data you want to use in Customer.io or other integrations. We run your query on an interval, and expose a last_sync_time value that you can use to make sure that you only sync data that changed since the previous sync interval. Add a reverse ETL or database integration Before you add an integration, we suggest that you set up a service account or create a user account specifically for Customer.io with read-only access to the tables you want to sync. This helps you ensure the security of your data, and gives you a way to revoke access if you need to. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations and make sure you’re in the Directory tab. Find and select your Data Warehouse or Database. Make sure it says Data In; we offer some data-out integrations for these same data warehouses and databases! Give your database a name and connect your database to Customer.io Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can connect your database to places outside of Customer.io. SSH Tunneling For added security, we support SSH tunnelling for our MySQL, PostgreSQL, and Microsoft SQL Server integrations. To set up your tunnel, you’ll generate a public key when you add your database: Enable SSH tunneling. Provide the hostname and port of your SSH server. Click Generate Public Key to generate a public key for your SSH server. Copy the public key to your SSH server and click Connect. Rotating SSH Keys You can return to your integration and generate a new public key for your SSH server at any time. Go to Integrations and select your database in the Connections tab. Go to the Databases tab and click the settings icon and go to Edit. Click Generate Public Key to generate a new public key and copy the key to your SSH server to continue connecting to your database. We won’t save or use the key until you click Save Database, and we won’t let you save your changes until your database connection is active with the new key. Add additional sync models A sync defines the type of call you want to make to Customer.io. Your query that returns the data you want to capture on every interval. Each row returned from the query represents an individual API call. After you set up your integration, you can add subsequent syncs to extract and transform data into additional calls by going to the Sync tab for your integration and clicking Add sync. Last sync time: sync only the records you want We expose a last_sync_time variable that you should use in your query to sync only the records that have changed since the last sync. This helps you avoid syncing the same records over and over again—which can cause each query to take longer, risk sending duplicate information into Customer.io or other places. We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. The first time you sync, {{last_sync_time}} will be 0, so you’ll sync all records. After that, you’ll only sync records that have changed since the last sync. SELECT id AS userId, email, first_name, created AS created_at FROM my_people WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Update existing profiles without creating new ones By default, identify requests create new people in Customer.io for any userId that doesn’t already exist. If you want to update existing profiles without adding new ones, you can use the _update parameter in your query. SELECT userId, email, someAttribute, true AS '_update' FROM peopleTable WHERE updatedAt > {{last_sync_time}} Reserved properties and traits We automatically treat values that aren’t reserved by our API calls as traits or event properties depending on the type of call you send, so you don’t have to force your query to return data exactly in the shape our API expects. For example, if you want to set the email trait, you can return user.email AS email. For identify or group data, any property other than the following goes in the traits object: userId, anonymousId, groupId, integrations, messageId, timestamp, context. For track, page, and screen data, any property other than the following goes in the properties object: userId, anonymousId, event, integrations, messageId, timestamp, context. These fields correspond to the common fields that we reserve in each API call, and extend to children of these reserved fields. For example, context.ip is also reserved. Learn more about common fields in our APIs. Nested traits, properties, and relationship attributes When you set up a sync, you’re essentially propagating the result of a SELECT query to the Pipelines API. Some properties or attributes require you to nest data. Or you might simply want to nest properties based on how you expect them in Customer.io. Some reverse ETL integrations support dot notation, like SELECT product AS 'cart.product_name'. But some, like BigQuery, don’t. You can support nested items in a platform-agnostic syntax using JSON_OBJECT. This lets you build an object from items in your query by alternating keys and values in order, e.g. key, value, key, value. For example, if you relate someone to a company (a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.), and you want to set attributes for the relationship, you’d nest them inside a traits.relationshipAttributes object. But BigQuery doesn’t support dot notation, so here’s how you’d do that: SELECT id as groupId, name, json_object( 'job_title', job_title, 'primary_contact', primary_contact ) as relationshipAttributes You may need to do the same for other objects, like device context, relationship attributes, and so on. See our API documentation for more information about the structure of our payloads and properties you might need to pass as objects. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Enable and disable syncs In your integration, you can go into the Syncs tab to enable and disable syncs. You can also delete syncs from this tab. When you disable a sync, we stop running the associated query and sending the type of data to the places you’ve connected your integration to—including Customer.io. We don’t delete the sync, so you can re-enable it later. When you enable a sync, we’ll resume the sync. The next sync interval will send all data that has changed since the last sync. Errors and sync history In the Imports tab for your integration, we display a row for each sync interval showing the number of successful and unsuccessful rows—where each row represents an individual operation (like an identify call or a track call). You can click a sync to see errors. Typically errors occur when a row is either missing data or contains data that doesn’t map to the appropriate source call type. If a sync has errors, the row also includes a Download button, so you can download a list of errors. You can also go to the Syncs tab to see the status of your syncs, how often they run, and their last run time. You might use this tab to see if your syncs complete on time or if you need to decrease the frequency of syncs. The Data In tab also shows successful source calls. You can use this to see exactly how your rows map to source calls. Missing rows Missing rows are often due to the value you compare to last_sync_time. SELECT id AS userId, email, first_name, created AS created_at, message_id AS messageId FROM my_people WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} If you can, update last_updated based on when your syncs start. If you can’t do this, you’ll need to change your query. For example, you could query for results beginning 5 minutes before the previous sync to give yourself a buffer and make sure that your query doesn’t miss results you want to sync: WHERE UNIX_SECONDS(last_updated) >= {{last_sync_time}} - 300 If you query for results beginning some amount of time before the previous sync, you should also update your query to capture (or generate a) messageId. This is a string field that we use to prevent duplicate entries. We accept the first instance of any given event with a given messageId and ignore any duplicate events with the same messageId. If your “time-buffered query” includes events with the same messageId as an event in a previous sync, we’ll ignore the duplicates. If you backdate events, you’ll need to deduplicate them before you send them to Customer.io. We deduplicate the messageId for 12 hours after we receive the operation—not the timestamp on the event itself. Sync query limits Name Details Limit Maximum query length The maximum length allowed for any query. 131,072 characters userId column name length The maximum length allowed for the userId column name. 191 characters timestamp column name length The maximum length for the timestamp column name. 191 characters Sync frequency The shortest possible duration between syncs. 1 minutes Result limits A sync cannot return more than 40 million records and 300 total columns. Name Details Limit Record count The maximum number of records a single sync will process. Note: This is the number of records extracted from the warehouse not the limit for the number of records loaded to the integration (for example, new/update/deleted). 30 million records Column count The maximum number of columns a single sync will process. 512 columns Column name length The maximum length of a record column. 128 characters Record JSON size The maximum size for a record when converted to JSON (some of this limit is used by Customer.io). 512 KiB Column JSON size The maximum size of any single column value. 128 KiB --- ## Amazon Redshift URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/amazon-redshift/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages. Granting us access to your database We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. This integration supports SSH tunneling. You can configure SSH tunneling when you set up your database connection in Customer.io. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up a Service Account When you set up an Amazon Redshift reverse ETL integration, you should create a user account that Customer.io will use to connect to Redshift. This user has limited access to your Redshift instance and gives you granular control over access to your Redshift instance. Log in to Redshift and select the Redshift cluster you want to integrate with Customer.io. Run the commands below to create a user named customerio. -- create a user named "customerio" CREATE USER customerio PASSWORD '<enter password here>'; -- allows the "customerio" user to create new schemas on the specified database. -- (This is the name you chose when provisioning your cluster) GRANT CREATE ON DATABASE "<enter database name here>" TO "customerio"; Add Amazon Redshift to Customer.io As a part of this setup, you’ll provide Customer.io with user credentials that we’ll use to query your database. We recommend that you create a new user with Read Only access specifically for Customer.io, so you can manage Customer.io access to your database independent of any other Microsoft SQL users you have. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick Amazon Redshift Provide your database information, including credentials to connect to your database, and click Save database. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io. Enter Host address and the name of the database you want to connect to. Enter a database user’s credentials and click Add database. We suggest that you create a service account and use service account credentials to set up your database. Optional: Set up an SSL connection to your database. Optional: Set up SSH tunneling. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. Preserve column name casing with double quotes Redshift lowercases unquoted identifiers and attributes, which are case-sensitive in Customer.io. Column names like userId and groupId become userid and groupid in your query results. With identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., this can cause unexpected behavior; by default, Customer.io doesn’t recognize userid or groupid as valid identifiers. For attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages., this can cause unexpected behavior or mismatched attributes. SELECT id AS "userId", email_address AS "email" FROM users WHERE last_updated >= {{last_sync_time}}  Enable case-sensitive identifiers in your Redshift cluster By default, Redshift ignores the casing of double-quoted identifiers. You must set enable_case_sensitive_identifier to true for double-quoted aliases to preserve their casing. You can enable this for your cluster or workgroup in the parameter group settings, or set it per-session: SET enable_case_sensitive_identifier TO true; We recommend setting this in your parameter group so it applies automatically to every session, including the queries Customer.io runs on your behalf. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT id AS "userId", email_address AS "email", fname, lname, msisdn AS "phone" FROM users WHERE last_updated >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(CAST(user_id AS VARCHAR), email) AS "userId", email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} CASE CASE The CASE statement checks if user_id exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN user_id IS NOT NULL THEN CAST(user_id AS VARCHAR) ELSE email END AS "userId", email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS "userId", event_name AS event, products, total_price AS value FROM events WHERE timestamp > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT companyId AS "groupId", objectTypeId AS "objectTypeId", companyname, employees, personId AS "userId" FROM companies WHERE last_updated >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT companyId AS "groupId", objectTypeId AS "objectTypeId", companyname, employees, personId AS "userId", JSON_SERIALIZE( OBJECT( 'is_manager', is_manager, 'role', role, 'start_date', start_date, 'department', department ) ) AS "relationshipAttributes" FROM companies WHERE last_updated >= {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS "userId", metatitle AS name, url, time_on_page FROM pages WHERE timestamp > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS "userId", screen_name AS name, session_started FROM screens WHERE timestamp > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT id AS "userId", old_id AS "previousId" FROM user_resolution WHERE timestamp >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## Google BigQuery URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/google-bigquery/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages.  Sending DATETIME values as Unix timestamps In Customer.io, you need to format timestamps as Unix epochs to full take advantage of them. If you store date and times as DATETIME in Big Query you can use UNIX_SECONDS(column_name) AS column_name to convert the DATETIME values to Unix timestamps. Granting us access to your database As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up a Google BigQuery Service Account When you set up a Google BigQuery reverse ETL, you’ll need to provide a Service Account key. This grants Customer.io access to your BigQuery project and gives you control over the level of access you want to grant. You’ll need to set up the service account in BigQuery. In the Google BigQuery, go to IAM & Admin > Service Accounts and click Create Service Account. Enter a name and description for the account, and then click Create and Continue. In the Grant this service account access to project section, select the BigQuery User role you want to add. Click Add another role and add the BigQuery Job User role. Click Continue and then click Done. Find the service account you just created, click the 3 dots under Actions, and select Manage keys. Click Add Key > Create new key. Select JSON for the key type and click Create. This downloads a key file to your computer. Open the file you just downloaded and copy the contents. When setting up your integration, you’ll copy these credentials in the Service Account Credentials field. Set up your Google BigQuery integration As a part of this setup, you’ll provide Customer.io with user credentials that we’ll use to query your database. We recommend that you create a new user with Read Only access specifically for Customer.io, so you can manage Customer.io access to your database independent of any other Google BigQuery users you have. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick Google BigQuery. Provide your database information, including credentials to connect to your database, and click Save database. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io Add your service account credentials in JSON format. See Set up a Google BigQuery Service Account above for help setting up a service account. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration to one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT id AS userId, email_address AS email, fname, lname, msisdn AS phone FROM users WHERE last_updated >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(CAST(user_id AS STRING), email) AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} CASE CASE The CASE statement checks if user_id exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN user_id IS NOT NULL THEN CAST(user_id AS STRING) ELSE email END AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS userId, event_name AS event, products, total_price AS value FROM events WHERE timestamp > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. Remember, group calls represent both an organization/group and relationships with users (by userId). Your query should include not only the groupId, but the userId so that you can capture relationships between users and groups. If the userId doesn’t exist in Customer.io, we’ll create a new person to represent the new userId and their relationship to the group. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId FROM companies WHERE last_updated >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId, JSON_OBJECT( 'is_manager', is_manager, 'role', role, 'start_date', start_date, 'department', department ) AS relationshipAttributes FROM companies WHERE last_updated >= {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, metatitle as name, url, time_on_page FROM pages WHERE timestamp > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, screen_name as name, session_started FROM screens WHERE timestamp > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT id AS userId, old_id as previousId FROM user_resolution WHERE timestamp >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. Nested traits, properties, and relationship attributes When you set up a sync, you’re essentially using a SELECT query to model your data in the Pipelines API format. Some properties or attributes in our API require you to nest data; or you might want to nest properties based on how you expect them in Customer.io or other places you send your data to. To do that, you’ll build a json_object from items in your query by alternating keys and values in order, e.g. key, value, key, value. For example, if you relate someone to a company (a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.), and you want to set attributes for the relationship, you’d nest them inside a traits.relationshipAttributes object. SELECT id as groupId, name, json_object( 'job_title', job_title, 'primary_contact', primary_contact ) as relationshipAttributes You’ll need to do the same for other objects, like device context, relationship attributes, and so on. See our API documentation for more information about the structure of our payloads and properties you might need to pass as objects. --- ## Microsoft SQL Server URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/microsoft-sql/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages. Granting us access to your database We officially support Microsoft SQL 2014 SP3 CU4 (12.0.x) and newer. An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up your Microsoft SQL Server integration As a part of this setup, you’ll provide Customer.io with user credentials that we’ll use to query your database. We recommend that you create a new user with Read Only access specifically for Customer.io, so you can manage Customer.io access to your database independent of any other Microsoft SQL users you have. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick Microsoft SQL Server. Provide your database information, including credentials to connect to your database, and click Save database. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io. Enter Host address and the name of the database you want to connect to. Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While our integration won’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. Toggle the options for SSL or SSH tunneling if necessary. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration to one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT id AS userId, email_address AS email, fname, lname, msisdn AS phone FROM users WHERE last_updated >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(CAST(user_id AS NVARCHAR(255)), email) AS userId, email, first_name, last_name FROM users WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} CASE CASE The CASE statement checks if user_id exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN user_id IS NOT NULL THEN CAST(user_id AS NVARCHAR(255)) ELSE email END AS userId, email, first_name, last_name FROM users WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS userId, event_name AS event, products, total_price AS value FROM events WHERE timestamp > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. Remember, group calls represent both an organization/group and relationships with users (by userId). Your query should include not only the groupId, but the userId so that you can capture relationships between users and groups. If the userId doesn’t exist in Customer.io, we’ll create a new person to represent the new userId and their relationship to the group. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId FROM companies WHERE last_updated >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId, (SELECT is_manager, role, start_date FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS relationshipAttributes FROM companies WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, metatitle as name, url, time_on_page FROM pages WHERE timestamp > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, screen_name as name, session_started FROM screens WHERE timestamp > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT id AS userId, old_id as previousId FROM user_resolution WHERE timestamp >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## MySQL URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/mysql/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages. Granting us access to your database We officially support MySQL 5.7 and newer. An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up your MySQL integration As a part of this setup, you’ll provide Customer.io with MySQL user credentials that we’ll use to query your database. We recommend that you create a new user with Read Only access specifically for Customer.io, so you can manage Customer.io access to your database independent of any other MySQL users you have. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick MySQL. Provide your database information, including credentials to connect to your database, and click Save database. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io. Enter Host address and the name of the database you want to connect to. Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While our integration won’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. Toggle the options for SSL or SSH tunneling if necessary. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration to one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT id AS userId, email_address AS email, fname, lname, msisdn AS phone FROM users WHERE last_updated >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(CAST(user_id AS CHAR), email) AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} CASE CASE The CASE statement checks if user_id exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN user_id IS NOT NULL THEN CAST(user_id AS CHAR) ELSE email END AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS userId, event_name AS event, products, total_price AS value FROM events WHERE timestamp > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. Remember, group calls represent both an organization/group and relationships with users (by userId). Your query should include not only the groupId, but the userId so that you can capture relationships between users and groups. If the userId doesn’t exist in Customer.io, we’ll create a new person to represent the new userId and their relationship to the group. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId FROM companies WHERE last_updated >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId, JSON_OBJECT( 'is_manager', is_manager, 'role', role, 'start_date', start_date, 'department', department ) AS relationshipAttributes FROM companies WHERE last_updated >= {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, metatitle as name, url, time_on_page FROM pages WHERE timestamp > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, screen_name as name, session_started FROM screens WHERE timestamp > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT id AS userId, old_id as previousId FROM user_resolution WHERE timestamp >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## PostgreSQL URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/postgresql/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages. Granting us access to your database We officially support PostgreSQL 12 and newer. An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up your PostgreSQL integration As a part of this setup, you’ll provide Customer.io with PostgreSQL user credentials that we’ll use to query your database. We recommend that you create a new user with Read Only access specifically for Customer.io, so you can manage Customer.io access to your database independent of any other PostgreSQL users you have. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick PostgreSQL. Provide your database information, including credentials to connect to your database, and click Save database. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io. Enter Host address and the name of the database you want to connect to. Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While our integration won’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. Toggle the options for SSL or SSH tunneling if necessary. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration to one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT id AS userId, email_address AS email, fname, lname, msisdn AS phone FROM users WHERE last_updated >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(user_id::TEXT, email) AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} CASE CASE The CASE statement checks if user_id exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN user_id IS NOT NULL THEN user_id::TEXT ELSE email END AS userId, email, first_name, last_name FROM users WHERE last_updated >= {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS userId, event_name AS event, products, total_price AS value FROM events WHERE timestamp > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. Remember, group calls represent both an organization/group and relationships with users (by userId). Your query should include not only the groupId, but the userId so that you can capture relationships between users and groups. If the userId doesn’t exist in Customer.io, we’ll create a new person to represent the new userId and their relationship to the group. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId FROM companies WHERE last_updated >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT companyId AS groupId, objectTypeId, companyname, employees, personId AS userId, JSON_BUILD_OBJECT( 'is_manager', is_manager, 'role', role, 'start_date', start_date, 'department', department )::text AS relationshipAttributes FROM companies WHERE last_updated >= {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, metatitle as name, url, time_on_page FROM pages WHERE timestamp > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT id AS userId, screen_name as name, session_started FROM screens WHERE timestamp > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT id AS userId, old_id as previousId FROM user_resolution WHERE timestamp >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## Snowflake URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/snowflake/ Best Practices Before you add a Reverse ETL source, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your database and Customer.io workspace. Create a new database user/service account. Implement a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Avoid using your main database instance. Consider creating a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your sync frequency so you don’t sync more than necessary and consume unnecessary resources. If the previous reverse ETL operation is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. You should monitor your first few reverse ETL intervals to ensure that your sync doesn’t impact your system’s security and performance—frequently skipped operations may indicate that you’re syncing too often.  Sending excessive data can impact your account’s performance You should not run queries that return large data sets—millions of rows—more than once per day. Doing so may impact workspace performance, including delaying campaigns and messages. Granting us access to your database As a part of setup, you’ll need to provide a private key in PKCS#8 PEM format to authenticate with Snowflake. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Set up a Snowflake connector When you set up a Snowflake integration, you’ll need to set up a Snowflake connector that Customer.io will use to connect to Snowflake. We recommend that you use the ACCOUNTADMIN role to execute all the commands below. As a part of this process, you’ll also generate the private key that Customer.io will use to authenticate with Snowflake. Log in to your Snowflake account and go to Worksheets. Run this code to create a virtual warehouse. We need to execute queries on your Snowflake account, which requires a Virtual Warehouse to handle the compute. You can also reuse an existing warehouse. You may need tune the warehouse size later, depending on your query size or complexity. Please see the Snowflake documentation for more details. -- not required if reusing another warehouse CREATE WAREHOUSE customerio_reverse_etl WITH WAREHOUSE_SIZE = 'XSMALL' WAREHOUSE_TYPE = 'STANDARD' AUTO_SUSPEND = 600 -- 5 minutes AUTO_RESUME = TRUE; Create a specific role for Customer.io. Snowflake access is specified through roles. You’ll assign a role to the user you’ll create later. The following is an example of how you might set up the role: -- create role CREATE ROLE customerio_reverse_etl; -- warehouse access -- change the name if reusing another warehouse GRANT USAGE ON WAREHOUSE customerio_reverse_etl TO ROLE customerio_reverse_etl; -- table access -- change the names to match the schema, database and table you want to use GRANT USAGE ON DATABASE my_database TO ROLE customerio_reverse_etl; GRANT USAGE ON SCHEMA my_database.my_schema TO ROLE customerio_reverse_etl; GRANT SELECT ON TABLE my_database.my_schema.my_table TO ROLE customerio_reverse_etl; Run this code to create the username and password combination that Customer.io will use to execute queries. Make sure to enter your password where it says my_strong_password. -- create user CREATE USER customerio_reverse_etl_user MUST_CHANGE_PASSWORD = FALSE DEFAULT_ROLE = customerio_reverse_etl PASSWORD = 'my_strong_password'; -- Do not use this password -- role access GRANT ROLE customerio_reverse_etl TO USER customerio_reverse_etl_user; Create a key pair for the user you just created. This is how you’ll authenticate with Snowflake. You’ll either need to generate an unencrypted key or decrypt an encrypted key. # Generate an unencrypted key openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt Private key requirements We support unencrypted private keys in the PKCS#8 PEM format. If you already have a private key and you’re not sure what format it’s in, check the last line of the key. The format we support ends with: -----END PRIVATE KEY----- If your key ends with -----END RSA PRIVATE KEY-----, you need to convert it to the format we support. If your key ends with -----END ENCRYPTED PRIVATE KEY-----, you need to decrypt it. If your key is encrypted If your private key starts with -----BEGIN ENCRYPTED PRIVATE KEY-----, you’ll need to decrypt it before you can use it in Customer.io: openssl pkcs8 -in encrypted_key.pem -nocrypt -out decrypted_key.pem Converting from PKCS#1 format If you have a key in the older PKCS#1 format (ending with -----END RSA PRIVATE KEY-----), you can convert it to the format we support by running: # Convert from PKCS#1 to PKCS#8 (unencrypted) openssl pkcs8 -topk8 -inform PEM -outform PEM -in existing_key.pem -out new_key.p8 -nocrypt Set up your Snowflake integration Before you set up your integration, make sure that you’ve set up your Snowflake connector and generated the private key that we’ll use to authenticate with Snowflake first. You’ll use the user, role, and private key you generate in that process when you set up your integration in Customer.io. Your database or storage bucket must allow connections from the following IP addresses. If our IP addresses are blocked, we won’t be able to connect to your database. Account region IP Addresses US 34.29.50.4, 35.222.130.209 EU 34.22.168.136, 34.78.194.61 Go to Integrations. In the Directory tab, pick Snowflake. Provide your database information, including credentials to connect to your database, and click Connect. The Name is a friendly name you’ll use to recognize your database whenever you reference it in Customer.io. Enter the name and role of the user that will be used to authenticate with Snowflake. This must be the person associated with the private key you’ll provide. Copy your private key into the Private Key field. Set up a Sync. A sync is the type of data (identify, track, etc) you want to import from your database and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your integration’s Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Now you can set up additional syncs and connect your integration to one or more destinations. Adding syncs After you set up your incoming integration, you can add additional syncs to import different types of data from your database. For example, you might want to import identify data for your users, and track data for their actions. Subsequent syncs can rely on your existing database, or you can add another database within your integration. In your integration, go to the Syncs tab and click Add Sync. Select your database or add a new one and click Next: Create Sync. Set up a syncA sync is the type of source data (identify, track, etc) you want to import from your database. A sync is essentially the type of source call you want to make. and click Next: Define Query. You can set up syncs for each type of data you want to import. Provide a Name and Description for the sync. This helps you understand the sync at a glance when you look at your source Overview later. Select the type of data you want to import. Set the Sync Frequency, indicating how often you want to query your database for new data. You should set the frequency such that sync operations don’t overlap. Learn more about sync frequency. Select when you want to start the sync: whether you want to begin importing data immediately, or schedule the sync to start at a later date. Enter the query that selects the data you want to import. See Queries below for more information about the information you’ll want to select for your sync. Click Run Query to preview results and make sure that your query selects the right information. Click Enable to enable your sync. Sync Frequency You can sync data as often as every minute. However, we recommend that you set your sync frequency such that sync operations don’t overlap. If you schedule syncs such that a sync operation is scheduled to start while the previous operation is still we’ll skip the next sync operation. Semantic events: Deleting people, groups, and more You may notice that this integration doesn’t have sync types to delete people, groups, or other objects. To do these kinds of operations, you’ll use what we call semantic events. These are events with specific names that indicate a delete operation. When your Track sync picks up events with an event name we recognize, we’ll perform the associated action—like deleting a person or group. For example, if you send an event with the name User Deleted, we’ll delete the person from your workspace. See Customer.io Semantic Events for more information. The semantic events we support are: Event Name Action Device Added or Updated Add or update a mobile device. Device Deleted Delete a mobile device. User Deleted Delete a person. Object Deleted Delete a custom object. Relationship Deleted Delete a relationship. Suppress Person Suppress a person. Unsuppress Person Unsuppress a person. Report Delivery Event Report in-app message events (like delivery, open, click) outside of our JavaScript integration. Queries for each sync type When you create a database sync, you provide a query selecting the people or objects you want to import, and respective properties. You’ll build your queries using the same principles from our Pipelines API. Each row returned from your query represents an individual operation (like an identify call, a track event, etc). Columns represent the traits or properties that you want to apply to the person, group, or event that your sync imports. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you limit your results using the last_sync_time.  Make sure you compare timestamps against last_sync_time Our examples below include a last_sync_time value. You must compare a timestamp to this value to avoid sending duplicate traffic to Customer.io which could impact your workspace’s performance. Convert your column names from all caps Snowflake column names are in all caps, but Customer.io and other places you might send Snowflake data to don’t expect property names to be formatted that way. When you write your query, you should convert your column names to lower, snake-cased formats (or a format that fits your use case). For example, if you have a column named EMAIL, you must convert it to email to use it as an identifier in Customer.io Journeys. Queries aren’t case-sensitive, but the results are. For example, Snowflake returns the same results whether you use EMAIL, email, or Email. But the column name—and the trait we record in Customer.io—is called EMAIL unless you change it with AS. SELECT ID AS "id", EMAIL AS "email", PRIMARY_PHONE AS "phone" FROM people WHERE LAST_UPDATED >= {{last_sync_time}} Troubleshooting uppercase column names If your Snowflake database returns uppercase attributes despite using an AS clause, you may need to adjust the QUOTED_IDENTIFIERS_IGNORE_CASE setting for your Snowflake user or role. This setting controls whether Snowflake treats quoted identifiers as case-sensitive. Important: We recommend setting this at the role or user level rather than at the account level to prevent unintended consequences for other places that rely on your Snowflake data. To apply this setting to the Customer.io user you created for this integration: ALTER USER customerio_reverse_etl_user SET QUOTED_IDENTIFIERS_IGNORE_CASE = FALSE; Or, to apply it to the Customer.io role: ALTER ROLE customerio_reverse_etl SET QUOTED_IDENTIFIERS_IGNORE_CASE = FALSE; Convert boolean values from binary to true/false strings You may want to convert binary to T/F strings to make your data more readable to your teammates. This way, your teammates can set branch or action conditions as true/false rather than 0/1 for instance. Most databases, including Snowflake, automatically store boolean values as binary (0 and 1) to reduce file size on disk. While your database software’s UI may display these as “True” or “False” for easier readability, the underlying data is stored in binary format. As Snowflake’s documentation mentions: “BINARY input/output can be confusing because what you see is not necessarily what you get.” You can convert binary boolean values to string representations in your query using Snowflake’s IFF function. The IFF function works similarly to SQL’s IF function but with an extra “F”: SELECT ID AS "userId", IFF(IS_ACTIVE, 'true', 'false') AS "is_active" FROM users WHERE LAST_UPDATED >= {{last_sync_time}} This query converts IS_ACTIVE to the string “true” when the value is 1 and “false” when the value is 0. last_sync_time and limiting your results You can send data to Customer.io only for records that have changed since the last sync by comparing timestamps against the last_sync_time value. This helps you avoid syncing the same records over and over again—which can cause syncs to take longer and, in extreme cases, can impact your workspace’s performance. We expose last_time_sync as a Unix timestamp representing the date-time when the last successfully completed sync started. By comparing a timestamp against this value, you’ll only sync records that have changed since the last sync. For your first sync, the last_sync_time is 0, so you’ll sync all records. After that, you’ll just get the changeset. Identify The identify method tells us who someone is and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. You can identify people by anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data from the anonymousId with the person you identify by userId. SELECT ID AS userId, EMAIL AS email, MSISDN AS phone, FNAME AS first_name, LNAME AS last_name FROM users WHERE TIMESTAMP >= {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Identify people by email or ID If you identify people by email and a unique ID, you can use a CASE statement or the COALESCE function to set the userId to prioritize the customer ID when available, falling back to email for people who don’t have a unique ID yet. This kind of setup is common when you support both leads (identified by email) and customers (identified by a unique ID after they make a purchase, or otherwise convert). Note that you need to use AS with quotes to ensure the column name is lowercase. COALESCE COALESCE The COALESCE function returns the first non-null value from the list of arguments: SELECT COALESCE(CAST(USER_ID AS VARCHAR), EMAIL) AS "userId", EMAIL AS "email", FIRST_NAME AS "first_name", LAST_NAME AS "last_name" FROM users WHERE LAST_UPDATED >= {{last_sync_time}} CASE CASE The CASE statement checks if USER_ID exists. If it does, it converts the ID to a string; otherwise, it uses the email address: SELECT CASE WHEN USER_ID IS NOT NULL THEN CAST(USER_ID AS VARCHAR) ELSE EMAIL END AS "userId", EMAIL AS "email", FIRST_NAME AS "first_name", LAST_NAME AS "last_name" FROM users WHERE LAST_UPDATED >= {{last_sync_time}} Track The track method records things people do. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. Track calls require an event name describing what a person did. They must also include an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. In most cases, your query should compare a timestamp to the last_sync_time to ensure that you only import new events. SELECT id AS USERID As userId, EVENT_NAME AS event, PRODUCTS AS products, TOTAL_PRICE AS value FROM events WHERE TIMESTAMP > {{last_sync_time}} event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Backfilling events In your initial sync, the last_sync_time is 0, and we’ll capture all events that otherwise match your query. After that, we only capture events that occur after the last_sync_time—events that occurred since the previous sync. This prevents you from importing the same events multiple times, but also means that you can’t backfill event history. If you need to backfill event history after your initial sync, you’ll need to set up a new sync to import the events you want to backfill. In general, you’ll: Create a new sync with a new query that captures the events you want to backfill. Run the sync to backfill events. Disable the backfilling sync so that you don’t capture events that your normal event query would otherwise import. Group The Group method associates a person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If the group/object or person in your group call don’t exist, this operation creates them. Group calls require a groupId to represent the group. In almost every case, a group call should also include a userId to associate the person with the group. You can also include traits to provide additional information about the group (or the relationship between the person and the group). Find more details about the group method in our API specifications. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT COMPANYID AS groupId, OBJECT_TYPE_ID AS objectTypeId, COMPANYNAME as company_name, EMPLOYEES AS employees, PERSONID AS userId FROM companies WHERE LAST_UPDATED >= {{last_sync_time}}  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Relationship attributes In Customer.io, you can assign attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to both the group (called a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io) and to the relationshipThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. between the object and the person. By default, attributes are stored on the custom object itself, but you can assign relationship attributes using the relationshipAttributes JSON object. SELECT COMPANYID AS "groupId", OBJECTTYPEID AS "objectTypeId", COMPANYNAME AS "companyname", EMPLOYEES AS "employees", PERSONID AS "userId", OBJECT_CONSTRUCT( 'is_manager', IS_MANAGER, 'role', ROLE, 'start_date', START_DATE ) AS "relationshipAttributes" FROM companies WHERE LAST_UPDATED >= {{last_sync_time}} Page The Page method records page views on your website, along with optional extra information about the page a person visited. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT ID AS userId, METATITLE AS name, URL AS url, TIME_ON_PAGE AS time_on_page FROM pages WHERE TIMESTAMP > {{last_sync_time}} integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Screen The Screen method sends screen view events for mobile devices. These help you understand the screens that people use in your app. Your query should compare a timestamp to the last_sync_time to ensure that you only import new data. SELECT ID AS userId, SCREEN_NAME AS name, SESSION_STARTED AS session_started FROM screens WHERE TIMESTAMP > {{last_sync_time}} name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. name string Required The name of the screen the person visited. properties object Additional properties for your screen. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. SELECT ID AS userId, OLD_ID AS previousId FROM user_resolution WHERE TIMESTAMP >= {{last_sync_time}} previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. --- ## Reverse ETL Overview URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/reverse-etl/ Import people, objects, and relationships from your database. Reverse ETL integrations ensure that people in your workspace reflect the latest information from your CRM or other backend system.  These are legacy integrations If you’re new to Customer.io, you won’t see the integrations in this directory. You’ll use our newer Reverse ETL integrations instead. A "reverse ETL" integration extracts, transforms, and loads data from your database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. For more information, visit our documentation specific to your database: Amazon Redshift Google BigQuery Microsoft SQL server MySQL PostgreSQL Snowflake --- ## Amazon Redshift URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/redshift-reverse-etl/ Import people, objects, and relationships from an Amazon Redshift database. This reverse ETL integration makes sure that people in your workspace reflect the latest information from your CRM or other backend system.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer Amazon Redshift integration. A "reverse ETL" integration extracts, transforms, and loads data from your Amazon Redshift database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Query Requirements When you create a database sync, you provide a query selecting the people or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. you want to import, and respective attributes. Each row returned from your query is a person or object that you’ll add or update in Customer.io; each column is an attribute that you’ll set on the people or objects that you import. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Query rules: For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Your query can only use Select * when the table you import from contains columns called id or email. If a column does not map directly to an identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. For Objects: your query must select a column representing an object_id. Your query can only use Select * when the table you import from contains a column called object_id. If a column does not map directly to an object identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Go to Data & Integrations > Integrations and select Amazon Redshift Data In. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While we don’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Write query to move to the next step. If you added a database as a part of another reverse ETL integration, you can select it instead of adding a new database. Enter your query and click Run query to preview up to 100 rows of results. Your query: Should SELECT individual columns. Must include columns representing id or email to identify people. If your columns aren't named id or email, you can use AS to map them to attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates.  Use SELECT * to see available columns You can use SELECT * in the Query step to preview the first 100 rows in your query and all available columns. This can help you determine which columns you actually want to select. We may show errors if you need to rename columns using AS. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Review import to review your sync setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select Amazon Redshift Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select Amazon Redshift Data In. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select Amazon Redshift Data In. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select Amazon Redshift Data In. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query.  This value is 0 until at least one sync is Completed If you’re just getting started, or if all of your previous syncs have a Some Rows Failed status, this value is 0. If your previous syncs show Some Rows Failed, you should download the error report and fix those errors so that an import finishes completely and the last_sync_time obtains a non-zero value. SELECT id, email, first_name, created AS created_at FROM my_table WHERE extract(epoch from last_updated) > {{last_sync_time}} Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, queries are not case sensitive: if a column in your database is called Email, you can use AS "email" to map the column to the email attribute in your workspace. Attributes in Customer.io are generally lowercased. We recommend that you rename columns with uppercased characters accordingly. SELECT id, email, primary_phone AS phone FROM my_table WHERE extract(epoch from last_updated) > {{last_sync_time}} Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > Amazon Redshift Data In. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ Does this integration support SSH tunneling? No, this legacy Amazon Redshift integration does not support SSH tunneling. If you need SSH tunneling for your Redshift connection, please use our newer Amazon Redshift integration, which supports SSH tunneling. What other databases do you support for import operations? In addition to Amazon Redshift, we also support MySQL, Postgres, Microsoft SQL, Google BigQuery, and Snowflake. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## Google BigQuery URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/bigquery-reverse-etl/ Import people, objects, and relationships from a BigQuery instance. This reverse ETL integration makes sure that people in your workspace reflect the latest information in your data warehouse.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer Google BigQuery integration. A "reverse ETL" integration extracts, transforms, and loads data from your BigQuery database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Google Sheet Requirements If you connect a Google Sheet to your BigQuery instance, you’ll need to enable some permissions in BigQuery so that Customer.io can query and import your data. In BigQuery, enable Google Drive access and grant the permissions outlined in Google’s documentation. Query Requirements When you create a database sync, you provide a query selecting the people or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. you want to import, and respective attributes. Each row returned from your query is a person or object that you’ll add or update in Customer.io; each column is an attribute that you’ll set on the people or objects that you import. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Query rules: For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Your query can only use Select * when the table you import from contains columns called id or email. If a column does not map directly to an identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. For Objects: your query must select a column representing an object_id. Your query can only use Select * when the table you import from contains a column called object_id. If a column does not map directly to an object identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Before you begin, you should create a Service Account with the BigQuery User role and Data Viewer permissions for the dataset you want to sync with Customer.io. When you set up your BigQuery integration, you’ll provide Customer.io with the contents of service account’s key file, granting access to query your BigQuery dataset. Go to Data & Integrations > Integrations and select Google BigQuery Data In. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter your Project ID and copy the contents of your Service Account’s Key File Contents into the appropriate box. See Google’s documentation for help creating a Service Account. We recommend that you limit this account to Data Viewer permissions credentials to make sure that you can’t inadvertently make changes to your database through your query. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Next to move to the next step. If you added a database as a part of another reverse ETL integration, you can select it instead of adding a new database. Select the region containing your BigQuery instance. Then enter your query and click Run query to preview up to 100 rows of results. Your query: Should SELECT individual columns. Must include columns representing id or email to identify people. If your columns aren't named id or email, you can use AS to map them to attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates.  Use SELECT * to see available columns You can use SELECT * in the Query step to preview the first 100 rows in your query and all available columns. This can help you determine which columns you actually want to select. We may show errors if you need to rename columns using AS. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Next to review your reverse ETL setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select Google BigQuery Data In Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select Google BigQuery Data In. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select Google BigQuery Data In. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select Google BigQuery Data In. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query.  This value is 0 until at least one sync is Completed If you’re just getting started, or if all of your previous syncs have a Some Rows Failed status, this value is 0. If your previous syncs show Some Rows Failed, you should download the error report and fix those errors so that an import finishes completely and the last_sync_time obtains a non-zero value. SELECT ID as "id", EMAIL as "email", FIRSTN as "first_name", CREATED AS "created_at" FROM my_table WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, queries are not case sensitive: if a column in your database is called Email, you can use AS "email" to map the column to the email attribute in your workspace. Attributes in Customer.io are generally lowercased. We recommend that you rename columns with uppercased characters accordingly. SELECT ID AS "id", EMAIL AS "email", PRIMARY_PHONE AS "phone" FROM people WHERE last_updated >= {{last_sync_time}} Cast numerical values to strings If values in your tables use the NUMERICAL data type, we’ll store them as fractions. To avoid this problem, you’ll need to cast the values to strings, and then we’ll handle them appropriately. SELECT id as object_id, balance cast (balance as string) FROM 'mytable' Convert structs and arrays to JSON If values in your tables use the STRUCT or ARRAY types, and you want to use their whole values as attributes, then you’ll need to convert them to JSON. This will allow you to use them in liquid. E.g.: for a STRUCT column called nested and an ARRAY called arr: SELECT id, TO_JSON(nested) AS nested, TO_JSON(arr) AS arr If you just want specific fields from a STRUCT, you can query those. E.g.: if nested has fields a, b, c, and you only need field a, you can use a query like this: SELECT id, nested.a Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > Google BigQuery Data In. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ and Troubleshooting tips I get an Error 403 message when I try to query my database If your BigQuery instance is connected to a Google Sheet, you need to enable Google Drive access and grant the permissions outlined in Google’s documentation so that we can query your database. What other databases do you support for import operations? In addition to BigQuery, we also support MySQL, Postgres, Microsoft SQL, Amazon Redshift, and Snowflake. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## Microsoft SQL server URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/ms-sql-reverse-etl/ Import people, objects, and relationships from a Microsoft SQL database. This reverse ETL integration makes sure that people in your workspace reflect the latest information from your CRM or other backend system.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer Microsoft SQL integration. A "reverse ETL" integration extracts, transforms, and loads data from your Microsoft SQL database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements We officially support the oldest long-term service (LTS) version available for Microsoft SQL: 2014 SP3 CU4 (12.0.x). An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221  You cannot connect to your database via SSH We don’t support SSH tunnelling today. If this is a part of your use case, let our product team know! Query Requirements When you create a database sync, you provide a query selecting the people or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. you want to import, and respective attributes. Each row returned from your query is a person or object that you’ll add or update in Customer.io; each column is an attribute that you’ll set on the people or objects that you import. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Query rules: For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Your query can only use Select * when the table you import from contains columns called id or email. If a column does not map directly to an identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. For Objects: your query must select a column representing an object_id. Your query can only use Select * when the table you import from contains a column called object_id. If a column does not map directly to an object identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Go to Data & Integrations > Integrations and select Microsoft SQL. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While we don’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Write query to move to the next step. If you added a database as a part of another reverse ETL integration, you can select it instead of adding a new database. Enter your query and click Run query to preview up to 100 rows of results. Your query: Should SELECT individual columns. Must include columns representing id or email to identify people. If your columns aren't named id or email, you can use AS to map them to attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates.  Use SELECT * to see available columns You can use SELECT * in the Query step to preview the first 100 rows in your query and all available columns. This can help you determine which columns you actually want to select. We may show errors if you need to rename columns using AS. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Review import to review your sync setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select Microsoft SQL Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select Microsoft SQL. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select Microsoft SQL. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select Microsoft SQL. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query.  This value is 0 until at least one sync is Completed If you’re just getting started, or if all of your previous syncs have a Some Rows Failed status, this value is 0. If your previous syncs show Some Rows Failed, you should download the error report and fix those errors so that an import finishes completely and the last_sync_time obtains a non-zero value. SELECT id, email, first_name, created AS created_at FROM my_table WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, queries are not case sensitive: if a column in your database is called Email, you can use AS "email" to map the column to the email attribute in your workspace. Attributes in Customer.io are generally lowercased. We recommend that you rename columns with uppercased characters accordingly. SELECT id, email, primary_phone AS phone FROM people WHERE last_updated >= {{last_sync_time}} Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > Microsoft SQL. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ What other databases do you support for import operations? In addition to Microsoft SQL, we also support MySQL, Postgres, Google BigQuery, Amazon Redshift, and Snowflake. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## MySQL URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/sql-reverse-etl/ Import people, objects, and relationships from a MySQL database. This reverse ETL integration makes sure that people in your workspace reflect the latest information from your CRM or other backend system.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer MySQL integration. How it works A "reverse ETL" integration extracts, transforms, and loads data from your MySQL database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements We officially support the oldest long-term service (LTS) version available for MySQL: 5.7. An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Query Requirements When you create a database sync, you provide a query selecting the people or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. you want to import, and respective attributes. Each row returned from your query is a person or object that you’ll add or update in Customer.io; each column is an attribute that you’ll set on the people or objects that you import. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Query rules: For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Your query can only use Select * when the table you import from contains columns called id or email. If a column does not map directly to an identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. For Objects: your query must select a column representing an object_id. Your query can only use Select * when the table you import from contains a column called object_id. If a column does not map directly to an object identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Go to Data & Integrations > Integrations and select MySQL. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While we don’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Write query to move to the next step. If you added a database as a part of another reverse ETL integration, you can select it instead of adding a new database. Enter your query and click Run query to preview up to 100 rows of results. Your query: Should SELECT individual columns. Must include columns representing id or email to identify people. If your columns aren't named id or email, you can use AS to map them to attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates.  Use SELECT * to see available columns You can use SELECT * in the Query step to preview the first 100 rows in your query and all available columns. This can help you determine which columns you actually want to select. We may show errors if you need to rename columns using AS. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Review import to review your sync setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select MySQL Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select MySQL. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select MySQL. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select MySQL. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query.  This value is 0 until at least one sync is Completed If you’re just getting started, or if all of your previous syncs have a Some Rows Failed status, this value is 0. If your previous syncs show Some Rows Failed, you should download the error report and fix those errors so that an import finishes completely and the last_sync_time obtains a non-zero value. SELECT id, email, first_name, created AS created_at FROM my_table WHERE UNIX_TIMESTAMP(last_updated) > {{last_sync_time}} Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, queries are not case sensitive: if a column in your database is called Email, you can use AS "email" to map the column to the email attribute in your workspace. Attributes in Customer.io are generally lowercased. We recommend that you rename columns with uppercased characters accordingly. SELECT id, email, primary_phone AS phone FROM people WHERE last_updated >= {{last_sync_time}} Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > MySQL. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ What other databases do you support for import operations? In addition to MySQL, we also support Postgres, Microsoft SQL, Google BigQuery, Amazon Redshift, and Snowflake. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## PostgreSQL URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/postgres-reverse-etl/ Import people, objects, and relationships from a Postgres database. This reverse ETL integration makes sure that people in your workspace reflect the latest information from your CRM or other backend system.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer PostgreSQL integration. A "reverse ETL" integration extracts, transforms, and loads data from your Postgres database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements We officially support the oldest long-term service (LTS) version available for PostgreSQL: 12. An older database version might work, but we can’t guarantee it. We support both SSL and non-SSL database connections. As a part of setup, you’ll need to provide the credentials of a database user with read-access to the tables you want to select data from. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221  You cannot connect to your database via SSH We don’t support SSH tunnelling today. If this is a part of your use case, let our product team know! Query Requirements When you create a database sync, you provide a query selecting the people or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. you want to import, and respective attributes. Each row returned from your query is a person or object that you’ll add or update in Customer.io; each column is an attribute that you’ll set on the people or objects that you import. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Query rules: For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Your query can only use Select * when the table you import from contains columns called id or email. If a column does not map directly to an identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. For Objects: your query must select a column representing an object_id. Your query can only use Select * when the table you import from contains a column called object_id. If a column does not map directly to an object identifier, you’ll receive an error, and you’ll need to rewrite your query to select individual columns. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Go to Data & Integrations > Integrations and select Postgres. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter a database user’s credentials and click Add database. We suggest that you use someone with read-only credentials for your database. While we don’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Write query to move to the next step. If you added a database as a part of another reverse ETL integration, you can select it instead of adding a new database. Enter your query and click Run query to preview up to 100 rows of results. Your query: Should SELECT individual columns. Must include columns representing id or email to identify people. If your columns aren't named id or email, you can use AS to map them to attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates.  Use SELECT * to see available columns You can use SELECT * in the Query step to preview the first 100 rows in your query and all available columns. This can help you determine which columns you actually want to select. We may show errors if you need to rename columns using AS. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Review import to review your sync setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select Postgres Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select Postgres. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select Postgres. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select Postgres. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. When you write your query, you should add a WHERE clause comparing your “last updated” column to the {{last_sync_time}}. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query.  This value is 0 until at least one sync is Completed If you’re just getting started, or if all of your previous syncs have a Some Rows Failed status, this value is 0. If your previous syncs show Some Rows Failed, you should download the error report and fix those errors so that an import finishes completely and the last_sync_time obtains a non-zero value. SELECT id, email, first_name, created AS created_at FROM my_table WHERE extract(epoch from last_updated) > {{last_sync_time}} Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, queries are not case sensitive: if a column in your database is called Email, you can use AS "email" to map the column to the email attribute in your workspace. Attributes in Customer.io are generally lowercased. We recommend that you rename columns with uppercased characters accordingly. SELECT id, email, primary_phone AS phone FROM my_table WHERE extract(epoch from last_updated) > {{last_sync_time}} Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > Postgres. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ What other databases do you support for import operations? In addition to Postgres, we also support MySQL, Microsoft SQL, Google BigQuery, Amazon Redshift, and Snowflake. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## Snowflake URL: https://docs.customer.io/integrations/data-in/connections/reverse-etl/database-sync/snowflake-reverse-etl/ Import people, objects, and relationships from a Snowflake database. This reverse ETL integration makes sure that people in your workspace reflect the latest information from your CRM or other backend system.  This is a legacy integration If you’re new to Customer.io, you won’t see this integration in your integrations directory. You’ll use our newer Snowflake integration. A "reverse ETL" integration extracts, transforms, and loads data from your Snowflake database to your workspace. With this integration, you can automatically add or update people, objects, and their relationships in Customer.io from your database on a recurring interval. When you set up your integration, you can import people or objects—you cannot import both with the same query. In either case, you can also use a separate "relationships" query to set relationships between people and objects—relative to the thing you import. So, if you import people, you set relationships to objects; if you import objects, you relate them to people. When you sync people, you can also add people to, or update people in, a manual segment. This helps you trigger campaigns automatically based on changes from each sync interval. Requirements As a part of setup, you’ll need to provide a private key in PKCS#8 PEM format to authenticate with Snowflake. If you use a firewall or an allowlist, you must allow the following IP addresses so we can connect to your database. Make sure you use the correct IP addresses for your account region. US RegionEU Region 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Query Requirements When you set up a reverse ETL integration, you provide a query selecting the people and columns you want to import. Each row returned from your query is a person you’ll add or update in Customer.io; each column is an attribute that you’ll set for the people you add or update. While we support queries that return millions of rows and hundreds of columns, syncing large amounts of data more then once a day can impact your account’s performance—including delaying campaigns or messages. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Your query must: Use as to map columns to lowercase attributes like id, email, and object_id. Because Snowflake column names are all caps, and our required attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. are lowercase, you must use as or your query will fail in Customer.io. SELECT ID as "id", EMAIL as "email" from MY_TABLE For People: your query must select at least one column representing a person’s identifier—email and/or id, depending on your workspace settings. Because Snowflake column names are in all caps, you must convert them to lowercase using AS to import data—SELECT ID AS id. For Objects: your query must select a column representing an object_id. If your query doesn’t include an object_type, we assume that the object_type is 1—your first or original object type. For Relationships: your query must contain one column representing object_id and one representing an ID or email for people. Each Row represents a relationship. Best Practices Before you add this integration, you should take some measures to ensure the security of your customers’ data and limit performance impacts to your backend database. The following “best practice” suggestions can help you limit the potential for data exposure and minimize performance impacts. Create a new database user. You should have a database user with minimal privileges specifically for Customer.io import/sync operations. This person only requires read permissions with access limited to the tables you want to sync from. Do not use your main database instance. You may want to create a read-only database instance with replication in place, lightening the load and preventing data loss on your main instance. Sync only the data that you’ll use in Customer.io. Limiting your query can improve performance, and minimizes the potential to expose sensitive data. Select only the columns you care about, and make sure you use the {{last_sync_time}} to limit your query to data that changed since the previous sync. Limit your frequency so you don’t sync more than necessary, overloading your database and Customer.io workspace. If the previous sync is still in progress when the next interval occurs, we’ll skip the operation and catch up your data on the next interval. Frequently skipped operations may indicate that you’re syncing too often. You should monitor your first few syncs to ensure that you haven’t impacted your system’s security and performance. Observe regional data regulations. Your data in Customer.io is stored in your account region—US or EU. If your database resides in Europe, but your Customer.io account is based in the US, GDPR and other data regulations may apply. Before you connect your database to Customer.io, make sure that you’re abiding by your regional data regulations.  Sending excessive data can impact your account’s performance While we support queries that return millions of rows and hundreds of columns, returning large data sets more then once a day can impact your account’s performance. When you set up your query, consider how much data you want to send and how often; and make sure you optimize your query with a last_sync_time. Set up a Private Key You’ll use an unencrypted private key (in PKCS#8 format) to authenticate your Customer.io integration with Snowflake. This means your key must end with -----END PRIVATE KEY-----. You can generate an unencrypted private key in your terminal with the following command: # Generate unencrypted private key openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt If your key is encrypted If your existing private key starts with -----BEGIN ENCRYPTED PRIVATE KEY-----, you’ll need to decrypt it before you can use it in Customer.io: openssl pkcs8 -in encrypted_key.pem -nocrypt -out decrypted_key.pem Converting from PKCS#1 format If you have a key in the older PKCS#1 format (ending with -----END RSA PRIVATE KEY-----), you can convert it to the format we support by running: # Convert from PKCS#1 to PKCS#8 (unencrypted) openssl pkcs8 -topk8 -inform PEM -outform PEM -in existing_key.pem -out new_key.p8 -nocrypt Add a sync When you set up a sync, you’ll choose whether you want to import People or objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple syncs. But, after you configure a database, your database information persists, making it easy to set up subsequent database sync operations. If you use a firewall or an allowlist, you must allow the following IP addresses (corresponding to your account region—US or EU), so that we can connect to your database. Account region IP Address US 34.122.196.49 EU 104.155.37.221 Go to Data & Integrations > Integrations and select Snowflake Data In. You can search for your database type or click Databases to find it. Click Set up sync. Enter a Name and Description for your database and click Sync settings. These fields describe your database import for other users in your workspace. Set your sync settings and click Select database. How often should this import sync? Set an interval for reverse ETL operations that you’re comfortable with. Schedule start time lets you set the date and time when you want to begin importing from your database. Choose what to sync is where you determine whether you want to import People or ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. If you want to import both, you’ll need to set up multiple sync operations. How do you want to identify people? Select whether you want to add and/or update people. If your workspace supports both email and ID as identifiers, select the value you’ll use to identify people—email or id. Sync these people to a segment?: As a part of each sync, you can add people to a new or existing segment. Use Create a new segment to set up a new segment specifically for your sync and Sync to an existing segment to add people to another segment in your workspace. What should we do with empty values?: Choose whether to ignore them or delete existing attribute value. What should we do with suppressed people?: The option you choose impacts the amount of data we will process with your next sync and the value of last_sync_time. If you select, Ignore them and mark the sync successful, then we update your last_sync_time, and we will not reprocess the data updated/imported with the sync with subsequent imports (unless, of course, there are changes to this data). The suppressed profiles are not reprocessed with subsequent syncs either. We will not report an error on suppressed profiles, but we will exclude them from import. If you select, Ignore them and mark the sync unsuccessful, then we do not update last_sync_time, and we will reprocess the data updated/imported with the sync in the next import, as well as the suppressed profiles. We will report an error including the row number and “person is suppressed.” Enter the Role, Username and Private Key of a user to grant Customer.io access to your database. We suggest that you use someone with read-only credentials for your database. While we don’t write to your database, using read-only credentials ensures that you can’t inadvertently make changes to your database through your query. The Warehouse field represents the snowflake virtual warehouse that you want to run your query against. When you add your database, we’ll try the connection to make sure your settings are correct. When you’re done, click Write query to move to the next step. If you added a database as a part of another sync operation, you can select it instead of adding a new database.  Don’t enter the full domain in the Account field You only need to provide your Snowflake server’s subdomain in the Account field. For example, if your Snowflake URL is cio-testing.us-west-2.snowflakecomputing.com then you should only enter cio-testing.us-west-2. Enter your query and click Run query to preview up to 100 rows of results. Remember, Snowflake’s column names are in all caps. You’ll need to convert at least your id or email field to lowercase using AS to import data—SELECT ID AS id. Your query: Must SELECT individual columns. Must include columns representing id or email to identify people. You must use AS to rename at least your ID or EMAIL columns to match attributes in Customer.io. Should include a WHERE clause, comparing recent updates against the last_sync_time (Unix epoch timestamp) to limit syncs to the most recent updates. (Optional) Set up a Relationship Query. If you don’t use our objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. feature, you can skip this step. When you set up a relationship query, you have to indicate how you’ll identify people—by id or email, regardless of the fields in your query. See Relationship Query below for more information. Click Review import to review your sync setup. Click Set up sync to start the import process. Relationship Query As a part of your sync, you can add a secondary query that imports relationships between people and objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. This query is independent of your initial import; it doesn’t matter whether your initial Query imports people or objects. Your relationship query must contain one column representing Object IDs and another representing identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. for people—one of email or id. If this person doesn’t exist in Customer.io, we’ll create them. By default, each row returned from your query represents a relationship that you want to add. You can also include a boolean column called deleted, where true removes a relationship and false sets the relationship.  Compare data to your last_sync_time to avoid duplicating data If you set a relationship for a person or object you previously deleted, we’ll re-add them. You should compare the timestamps on your data to the {{last_sync_time}} to make sure you don’t add people or objects that you deleted in a previous sync. SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted from customer_obj_relationships where datetime_updated > {{last_sync_time}} Relationship attributes Relationship attributes are data you can store on the relationship itself, much like you can store attributes on objects and people. If you see relationship attributes in the UI, then your account is set up so you can sync them in your relationship queries: SELECT person_id as id, company_id as object_id, new_relationship_bool as deleted, attr1, attr2 from customer_obj_relationships where datetime_updated > {{last_sync_time}} Check the status of a sync The Imports tab for your integration shows recent sync intervals. Click an interval to see how many people you imported, how long the sync operation took to complete, and other information. Sync operations will show Failed if the query contained any failed rows. While some rows may have synced normally, we report a failure to help you find and correct individual failures. See Import failures for more information. Go to Data & Integrations > Integrations and select Snowflake Data In Click the sync you want to check the status of and go to the Imports tab. Pause or resume a sync Pausing a sync lets you skip sync intervals, but doesn’t otherwise change your configuration. If you resume a sync after you pause it, your sync will pick up at its next scheduled interval. Go to Data & Integrations > Integrations and select Snowflake Data In. Click next to the sync you want to modify and select Pause. If your sync is paused and you want to resume it, click Activate. Update a sync When you update or change the configuration of a sync, your changes are reflected on the next sync interval. Go to Data & Integrations > Integrations and select Snowflake Data In. Click the sync you want to update. Make your changes. Click between Query and Settings tabs to make changes to different aspects of your sync. Click Save Changes. Delete a sync Deleting a sync stops syncing/updating people from your database using a particular query. It does not delete or otherwise modify anybody you imported or updated from the database with that query. Go to Data & Integrations > Integrations and select Snowflake Data In. Click next to your sync and select Delete. Optimize your query Because your database sync operates on an interval, you should optimize your query to ensure that we import the right information, quickly, with the least noise. When setting up your query, you should consider: Your database timeout value: Queries selecting large data sets may timeout. Cost: Are you charged per query or for the amount of data returned? Can you narrow your query?: Add a “last_updated” or similar column to tables you import, and index that column. You’ll use this column to select the changeset for each sync. How often do you need to sync (frequency)?: Syncing large data sets too frequently can impact your account’s performance. If a sync operation is still in progress when the next sync interval occurs, we’ll skip the sync interval. This generally isn’t an issue. The next operation will pick up any data from the skipped sync, but frequently skipped syncs may indicate that you’re attempting to sync too frequently. SELECT id AS "id", email AS "email", firstn AS "first_name" , created AS "created_at" FROM my_table WHERE last_updated >= {{last_sync_time}} Last Sync Time We strongly recommend that you index a column in your database representing the date-time each row was last-updated. The {{last_sync_time}} is a Unix timestamp beginning at 0 for the first sync. When you compare your “last_updated” column to the last sync time, you’ll need to use to_timestamp_ntz{{last_sync_time}} to convert the value to a usable ISO-8601 date-time. The last sync time is a Unix timestamp representing the date-time when the previous sync started. Comparing a “last-updated” column to this timestamp helps you limit your sync operations to the columns that changed since the previous sync. If you use ISO date-times, you can convert them to unix timestamps in your query. SELECT ID as "id", EMAIL as "email", FIRSTN as "first_name", CREATED AS "created_at" FROM my_table WHERE last_updated > to_timestamp_ntz({{last_sync_time}}) Mapping columns to attributes We map column names in your query to attributes in your workspace, exactly as formatted in your query. However, columns in Snowflake are all caps and queries are not case sensitive. You must use AS to map at least your ID and EMAIL columns to people-identifiers in Customer.io. Attributes in Customer.io are generally lowercased. You should make sure all of your columns map to attributes that already exist in your workspace so that you don’t create duplicate attributes with mismatched cases (i.e. FIRST_NAME and first_name). SELECT ID AS "id", EMAIL AS "email", PRIMARY_PHONE AS "phone" FROM people WHERE last_updated >= {{last_sync_time}} Sync intervals and ‘skipped’ syncs You can set up a sync to import from your database on an interval of minutes, hours, days, etc—but we only process one sync operation per workspace at a time. Sync operations cannot occur concurrently in your workspace. If we’re still processing a significant amount of data from the previous sync operation (roughly 300,000 or more operations) when it’s time to start the next sync, we’ll skip the next sync and try again at the next interval. Skipped sync operations show a Skipped status in the UI. For example, let’s say a sync is scheduled for every hour. If the first sync starts at 1:30 PM, then the next sync will start one hour after the last one started, 2:30 PM in this case. If the last sync is still in progress, the next sync won’t start until the next hour: 3:30 PM, 4:30 PM, etc. We tested reverse ETL performance for a MySQL server against an empty workspace with no concurrent operations (API calls, running campaigns, etc) with the following results. Your results may vary if your query is more complex, or your workspace has multiple concurrent, active users during the sync interval. Adjust your sync intervals to provide significant buffer between syncs and account for concurrent users in your workspace or other operations (active campaigns, segmentation, or other operations that affect your audience). Database rows Database columns Average sync time (mm:ss) 100,000 10 4:20 250,000 10 10:36 500,000 10 21:49 750,000 10 31:22 1,000,000 10 40:39 Import failures Rows that fail to add or update a person report errors. You can find a count of errors with any sync and download a list of errors for failed rows by going to Data & Integrations > Integrations > Snowflake Data In. If a sync interval contained any failed rows, the operation shows Failed. Rows may still have been imported, but we report a failure so that it’s clear that the sync interval contained at least one failure. Click the row for more information. Click Download to get a CSV file containing errors for each failed row.  If you see Failed Attribute Changes, try changing your workspace settings Reverse ETL syncs that change a person’s email address can be a frequent source of Failed Attribute Change errors. You can enable the Allow updates to email using ID setting under Settings > Workspace Settings > General Workspace Settings to make it easier to change people’s email values after they are set and avoid Failed Attribute Change errors. In general, most issues are of the Failed Attribute Change type relating to changes to id or email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You are likely to see this error if: You set an id or email value that belongs to another person. If your workspace identifies people by either email or id, these values must be unique. Attempting to set a value belonging to another person will cause an error. You attempt to change an id or email value that is already set for a person. You can set an id or email if it is blank; you cannot change these values after they are set. You can only change these values from the People page, or when you identify people by cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).), which you cannot use in a Reverse ETL operation. You set an invalid email value Emails must conform to the RFC 5322 standard. If they do not, you’ll receive an attribute change failure. FAQ What other databases do you support for import operations? In addition to Snowflake, we also support MySQL, Postgres, Microsoft SQL, Google BigQuery, and Amazon Redshift. Contact us to let us know if you want us to support another database as a part of our reverse ETL integrations. How can I convert boolean values from 0/1 to true/false strings? You may want to convert binary to T/F strings to make your data more readable to your teammates. This way, your teammates can set branch or action conditions as true/false rather than 0/1 for instance. Most databases, including Snowflake, automatically store boolean values as binary (0 and 1) to reduce file size on disk. While your database software’s UI may display these as “True” or “False” for easier readability, the underlying data is stored in binary format. As Snowflake’s documentation mentions: “BINARY input/output can be confusing because what you see is not necessarily what you get.” You can convert binary boolean values to string representations in your query using Snowflake’s IFF function. The IFF function works similarly to SQL’s IF function but with an extra “F”: SELECT ID AS "userId", IFF(IS_ACTIVE, 'true', 'false') AS "is_active" FROM users WHERE LAST_UPDATED >= {{last_sync_time}} This query converts IS_ACTIVE to the string “true” when the value is 1 and “false” when the value is 0. Do you support SSL or TLS connections? We support SSL connections. You can also secure your connection by limiting access to approved IP addresses. Do you support connections via SSH? No. We do not support SSH tunneling. Is there a limit to the number of people I can import at a time? You should not add or update many millions of people (rows) at a time. Consider adding a LIMIT and ORDER BY to your query, or using a WHERE clause to limit updates to people who have been added or updated since the {{last_sync_time}}. See optimize your query for more information. Your query cannot SELECT more than 300 columns, where each column represents an attribute. Contact us if you want to import more rows or columns. --- ## Getting Started URL: https://docs.customer.io/integrations/data-in/connections/salesforce/getting-started/ Quick start video Here’s a quick overview of the setup process for our incoming Salesforce integration. In this video, we set up Salesforce to send Contacts and Accounts to Customer.io. Before you begin Before you begin, you should ask yourself how you want to use your Salesforce data. The data that you send to Customer.io—as People, Events, Custom Objects, or Relationships between people and objects—may depend on what you want to do with your data in Customer.io and what downstream services you want to send it to. See Working with Salesforce Data for help mapping data. Working with Salesforce data Unlike Customer.io and other platforms, Salesforce doesn’t have generic concepts like People and Groups—or things that map easily to those terms. Instead, Salesforce has highly structured data: your leads, contacts, opportunities, accounts, and other things are all separate. When you send Salesforce data to Customer.io, you’ll have to determine whether each type of data represents People, Events, Custom ObjectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., or Relationships between people and objects. In most cases, this should be easy: contacts are people, because you’ll send them messages; an account is something many people are connected to, so it’s probably a custom object. On this page, we recommend that you start with Contacts because they map to people. For other types of data, see Using Salesforce with Customer.io Journeys to learn more about using data with Customer.io specifically, or Mapping Salesforce data to destinations to learn more about mapping Salesforce data to other destinations. flowchart LR a{Is it a contact?} a------>|yes|b(Person) a-.->|no|c{Is it associated with >1 person?} c--->|yes|d(Custom Object) c-.->|no|e{Does it have an email or phone?} e-->|yes|b e-.->|maybe/no|h{Does it relate a person to a group?} h-.->|no|f{Is it something people do? Will it trigger messages?} h--->|yes|i(Relationship) f-->|yes|g(Event) f-.->|no|d Prerequisites To connect Salesforce to Customer.io, you need: A Salesforce Data Cloud (sometimes called Sales Cloud) account that is either: At the Professional, Enterprise, level or higher. Includes Web Services API access. A Salesforce user with access to the data you want to send from Salesforce to Customer.io. In general, we suggest that you set up a user specifically for your syncs, making it easy to limit the data Customer.io can access and control your integration independently of your day-to-day Salesforce users. Set up your Salesforce integration When you set up your integration, you’ll configure your first Sync—the type of data that you want send to Customer.io and other destinationsAn integration that sends data out of Customer.io—your data’s ultimate destination.. We suggest that you start with Contacts because it’s the easiest, and most natural thing to send into Customer.io and other services. You’ll set up subsequent syncs after you finish configuring your integration, where each sync brings in a different kind of data (leads, opportunities, and so on). Go to Integrations. In the Directory tab, pick the Data In Salesforce integration Set up your Salesforce connection: Give the integration a friendly Name so you’ll recognize it in Customer.io. Set up your sync. See Scheduled syncs for more information. If you’re logging in with your Salesforce Sandbox account, you’ll need to enable the Sandbox setting to test your integration. Click Connect Salesforce and log into Salesforce. While you can login with any Salesforce user, you might want to login with an integration user—an account made specifically for this integration that helps you manage and limit the data available to Customer.io. When you’ve logged into Salesforce, click Create Sync. Create your first sync. This is a type of data that you send through your pipeline. Give your sync a Name—something that describes the data you’re importing and what you’re mapping it to. In the Sync dropdown, pick the kind of Salesforce record you want to send to Customer.io. Again, we suggest starting with Contacts. Click Edit next to Unique identifiers map your identifier—the field that uniquely identifies each person or object. If you’re using Contacts, pick the Contact ID as your Contact ID. Go to the Fields tab and pick the fields you want to capture in Customer.io. Salesforce contains a lot of data, not all of which is useful in Customer.io. For example, you don’t need the mailing latitude or longitude to send email. Click Add optional data filters if you want to bring a subset of your data into Customer.io. See Filter incoming data for more information. If your sync isn’t Contacts, check the Pipelines format. For popular Salesforce data types, we’ll set these fields for you. For example, if you’re sending Accounts, Data Pipelines Format is automatically set to Custom Object and the Custom Object Name is set to Accounts. Otherwise, these fields tell Customer.io how to interpret the data you send—whether your Salesforce data represents people, custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., or relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins.. The Custom Object Name is an attribute we store so you’ll know which Salesforce data type your Customer.io data originated from. Choose how often you want to sync data and when you want to start. Click Next: choose destinations and select places you want to connect your Salesforce data to. The destinations you connect to might depend on how you model your data! See Mapping data to destinations for more information. If your Data Pipelines format is set to Custom Object or Relationship, you need to map them to a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io. In most cases, this is the same name as the Salesforce object you want to sync (or relate people to). If you’re mapping something like Accounts to Customer.io, and Accounts don’t already exist, click New Object Type and set the singular and plural forms—like Account and Accounts. Click Enable Salesforce. Approve Customer.io as a “Connected App” in Salesforce. You must do this before data will begin syncing to Customer.io. Now that you’ve created a sync for one type of Salesforce data, you may also want to create syncs for other kinds of data you want to import from Salesforce. The Syncs tab will show the status of your sync and note that we’re processing your Salesforce data. If you go into , you should begin seeing your Salesforce data—though it may take a few minutes to populate all your people, especially if you have millions of records. Approve Customer.io as a “Connected App” in Salesforce Salesforce is removing support for “Connected Apps” in Salesforce in 2026—and that’s how our integration is currently built. We’re working on updates to our integration to support their “External App” framework, but, in the meantime, you’ll need to approve Customer.io as a “Connected App” in Salesforce before you can sync data from Salesforce to Customer.io. Go to Salesforce, click the Setup icon and then click Setup. Under Platform Tools go to Apps > Connected Apps > Connected Apps OAuth Usage. Click Install next to Customer.io Data Pipelines and follow the instructions to approve Customer.io as a “Connected App” in Salesforce. Set up additional syncs During the initial setup you’ll create a single sync when you set up your Salesforce integration. After that, you can set up additional syncs to import other kinds of data from Salesforce. To learn more about whether your data types are people, objects, or events, see Mapping Salesforce data to destinations. Go to your Salesforce integration’s Syncs tab and click Add Sync. Give your sync a Name—something that describes the data you’re importing and what you’re mapping it to. Pick the kind of Salesforce record you want to send to Customer.io. If you’ve already synced contacts, you might want to sync the Accounts they’re connected to. Click Edit next to Unique identifiers map your identifier—the field that uniquely identifies each person or object. For Accounts, this is usually the Account ID. Go to the Fields tab and select the fields you want to capture for that data type and then click Save Changes. If your sync isn’t Contacts, check the Data Pipelines format. For popular Salesforce data types, we’ll set these fields for you. For example, if you’re sending Accounts, Data Pipelines Format is automatically set to Custom Object and the Custom Object Name is set to Accounts. For more information about the data pipelines format, see Mapping Salesforce data to Customer.io. Otherwise, these fields tell Customer.io how to interpret the data you send—whether your Salesforce data represents people, custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., or relationships. The Custom Object Name is an attribute we store so that you’ll know which Salesforce data type your Customer.io data originated from. Click Add optional data filters to add filters to the data you want to bring into Customer.io. See Filter incoming data for more information. If you don’t add any filters, we’ll bring all data of the selected type from Salesforce. Choose how often you want to sync data, when you want to start, and then click Configure Destinations. If your Data Pipelines format is set to Custom Object or Relationship, you need to map them to a custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io. In most cases, this is the same name as the Salesforce object you want to sync (or relate people to). If you’re mapping something like Accounts to Customer.io, and Accounts don’t already exist, click New Object Type and set the singular and plural forms—like Account and Accounts. Click Enable Sync When you finish setting up your sync, the Syncs tab will show the status of your new sync and note that we’re processing your Salesforce data. If you go into the Journeys tab , you should begin seeing your Salesforce data—though it may take a few minutes to populate all your people, especially if you have millions of records Filter incoming data When you set up a sync, we bring all data of the selected type from Salesforce by default. For example, if you sync your Salesforce contacts, we’ll bring all your contacts into Customer.io. But you might not want all of your contacts—maybe you qualify your contacts or you only want to bring in contacts who meet certain thresholds. That’s what the Add optional data filters button does: it lets you provide criteria for data you want to bring into Customer.io. If you’re not comfortable writing your own query, you can use AI to generate a filter for you! Provide a plain-text description of the data you want to capture or exclude, and we’ll try to generate a filter for you. If you write your own query, note that you don’t have to write the SELECT, FROM, or WHERE clauses. The filter acts as criteria for the WHERE clause of a Salesforce SOQL query, and the other sync settings cover the SELECT and FROM clauses. For example, if you only want to bring contacts into Customer.io if they have an email address, you could write a filter like this: Email != null  Salesforce capitalizes field names When you write your filter, remember that Salesforce uses pascal case field names (like FirstName). While you can change field names when you bring them into Customer.io, you’ll use the original field name from Salesforce for your filter. What data is captured in each sync? Your first sync captures all your current Salesforce data of the specified type. Subsequent syncs capture records that changed since the previous sync. This means that your first sync may take longer than subsequent syncs, as we gather all your Salesforce data with the initial sync. For example, if you sync contacts, your first sync will send all of the current contacts in your Salesforce environment. The next sync will gather all changes to contacts—all your new contacts, updates, and deleted contacts—since the first sync.  We handle deletions differently from create and update operations! When you delete records in Salesforce, we send events to record the deleted items. Customer.io handles these events automatically. But you may need to set up actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to delete data! Updating syncs When you update a sync to add or remove fields, your updates take affect at the next sync interval. This means that your changes don’t affect data that you’ve already sent from Salesforce (or any syncs in progress). To update all data you’ve passed from Salesforce to Customer.io (and other places you send your Salesforce data to), you can Resync all data. This sends all of your corresponding Salesforce data through Customer.io again, updating all of your records with the changes you made to your sync. On the Syncs tab, click next to the sync you updated and click Resync all data. Manage access to Salesforce data with an “Integration User” When you connect to Salesforce, you don’t need to use your own credentials. You can set up an Integration User in Salesforce with limited access to the data you want to send to Customer.io. This helps you secure and limit the data that you expose to Customer.io—both the types of data you can sync and the fields within each data type that Customer.io can access. See Salesforce’s documentation for more about Integration Users. --- ## Map Salesforce data to Customer.io URL: https://docs.customer.io/integrations/data-in/connections/salesforce/mapping-to-customerio/ When you set up a Salesforce integration with Customer.io, you'll need to map your Salesforce data to people, custom objects, or events. You’ll set up a sync for each kind of data you want to send through your pipeline. In most cases, you’ll want to follow our typical mappings. But the way you map your data to Customer.io depends on what you want to do with it—send messages, use it to trigger messages, or manage relationships! Typical mappings Remember, you’ll convert your Salesforce data to people, events, custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., or relationships. You’ll see our typical mapping suggestions below. Notice the * next to Leads! Leads fill multiple roles in Salesforce, so you might map them to people, objects, or both. We’ve provided more information about leads below to help you figure out how you want to map them to Customer.io. Data Pipelines format Data type Typical Salesforce objects People identify Contacts, Leads* Events track Tasks Custom Objects group Accounts, Opportunities, Campaigns, Leads* Relationships group Account-Contact Relationships, Opportunity-Contact Relationships The image below doesn’t show relationships. That’s because relationships aren’t a type of data themselves; rather, they’re a relationship between two data types. You won’t set the Data Pipelines format to relationships unless you have two other data types you need to relate to each other. For example, if you only sync contacts, you won’t need to sync relationships. But if you sync contacts and accounts, you’ll need to sync relationships to relate contacts to accounts! See Relationships: matching people to objects below for more information. Note that the Data Pipelines format for objects and relationships affects which fields are available to sync. Different formats expose different sets of fields from your Salesforce objects and associated data (typically Contacts). In general, you should use the Relationships format for if the Sync includes Relationships in the name. If you want to sync both object fields and relationship fields for the same Salesforce data type, you should create two separate syncs—one for the object itself (like “Accounts”) and another for the relationships to that object (like “Account Contact Relationships”). What custom object name do I set for a custom object or relationship? In most cases, we set this automatically for you. The Custom Object Name is simply an attribute we set in Customer.io that tells us what kind of Salesforce data your object (or relationship) originates from. If you create a new sync and the custom objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. doesn’t exist yet, don’t worry: you’ll create it in the Destination step when you set up your sync. Relationship sync identifiers: matching people to objects Salesforce treats people, custom objects, and the relationships between the two as discrete pieces of information. So, whenever you sync people and a kind of object, you’ll probably want to setup a separate Relationship sync between people and that object. For example, when you sync accounts and contacts to Customer.io, you’ll also want to sync Account Contact Relationship information to capture relationships between your contacts and accounts. When you do this, you need to associate the ID of a person with the ID of an object. For accounts and contacts: The Field Containing Person ID is either the Contact ID or Contact.email The Field Containing Object ID is the Account ID  We try to mark important fields for you When you look at relationship identifiers, we’ll tell you which values are references to a contact, user, object, and so on. We fetch values from your contacts or objects to make setting relationships easier. For example, contact.email isn’t in the Account Contact Relationship sync, but we fetch it from your contacts to make things easier for you! Relationship attributes Like people and custom objects, relationships also contain attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. describing the relationship—like whether someone is directly related to an account, a primary contact, and so on. These attributes help you find the right person by association with an object, like when you need to find an account’s primary contact, or all active participants in an opportunity. These are the things you can bring in on the Fields tab. You’ll see them when you look at a person’s relationships! Field mappings Turn into relationship attributes! How do I know if data is a person, event, or an object? We’ve created a little decision tree to help you map data accordingly, but in short: If you want to send messages to it, or you want to log events it performs, it’s a person. In Customer.io, people are the basic unit you’ll operate against. People can receive messages and perform events. Objects themselves can’t receive messages or perform events. Think of it this way: an opportunity doesn’t have an email address; you’ll never send a message directly to an opportunity; an account can’t visit a page on your website. Those are things people do! If something is related to people and the data has a lasting impact, it’s probably an object. Custom objects are things that people are related to—like an account they belong to, a company they work for, or an opportunity they represent. They’re like people, in that they have attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and, unlike events, they’re important long after they initially appear in your workspace. If something represents people’s activities, it’s probably an event. Events are things that people do, like when a user visits a page on your website, adds an item to their shopping cart, or watches a video. Events are really useful for responding to people’s activities in an automated way, like sending a calend.ly link to people who want to hear from you about your product. flowchart LR a{Will you send it messages?} a--->|yes|b(It's a person) a-.->|maybe|c{Can it perform events?} c--->|yes|b a-.->|no|d{Is it related to more than one person?} d-->|yes|e(It's an object) c-.->|no|d d-.->|no|f(It's either an event or you don't need it) Leads are people in Customer.io In Salesforce, a Lead represents multiple things: it’s an unqualified contact, an unqualified account, and a potential opportunity. But in Customer.io, the most common use case is to map leads to people so that you can foster your relationship with a person as they move through your sales process and become a qualified contact. (Recommended) map leads to people: If you want to send messages to your lead, attempting to qualify and nurture them, then they must be people. This is the most common use case when sending leads to Customer.io Journeys. Map leads to objects: If you just want to track the opportunity or company represented by a lead until you have a qualified contact, you can map them to objects. But remember: if you do this, you must have a person that you want to relate the object to! Map leads to relationships: If you want to capture a lead as both a person and an object related to that person, you can set up two separate syncs that map leads to both people and objects! Identify people by email address A common use case for syncing leads to Customer.io is to send messages to leads to nurture, qualify, and convert them to contacts. But where Salesforce gives a person a lead ID and then a different contact ID when they graduate to a contact, Customer.io expects a single ID for a person throughout their lifecycle. So how do we make sure that a lead and contact in Salesforce resolve to the same person in Customer.io? We identify leads and contacts by email address. Beyond identifying people by email address, you should make sure that you store the lead_id and contact_id attributes in Customer.io. This gives you: A way to determine when a lead graduates to a contact. (If the contact_id attribute exists, the person is a contact!) A way to send data back into Salesforce, which expects you to identify leads and contacts by their Salesforce IDs. See Sending data back to Salesforce below for more information. sequenceDiagram participant a as Person in Salesforce participant b as Person in Customer.io a-->>a: Add new lead to Salesforce a-->>b: Sync lead by email with lead_id attribute b-->>b: Nurture lead with messaging campaign a-->>a: Lead is qualified and becomes a contact a-->>b: Sync contact by email with contact_id attribute note over b: Person now has lead_id and contact_id attributes What happens if emails aren’t unique in Salesforce? By default, emails aren’t required to be unique in Salesforce, but they are required to be unique by default in Customer.io. This means that you can have multiple contacts (or leads) with the same email address—though this is rare and generally not something you want to happen. If two leads or contacts have the same email address in Salesforce, and you sync them to Customer.io, they’ll end up representing the same person in Customer.io—which could result in a muddled identity for your users. If you’re worried about this happening, you can: Make email a unique field in Salesforce by creating a custom field in Salesforce to represent email addresses. Deduplicate your contacts and leads by using a Salesforce app. Search the Salesforce AppExchange for “dedupe” or “deduplicate” to find an app that will help you deduplicate your contacts and leads. Leads become contacts associated with accounts and opportunities When you qualify a lead in Salesforce, the record splits into those three things: a Contact related to an Account and an Opportunity. If you sync all three data types to Customer.io, then you’ll create or update records when you qualify the lead in Salesforce. So, whether you bring in a lead as a person or an object, it can eventually become a person and two related objects when you process or qualify it. flowchart LR subgraph 1[Salesforce Lead] direction LR a(Contact) b(Account) c(Opportunity) end 1-->d{Is lead qualified and ready for you to act on?} d-->|yes|e(Salesforce lead graduates into:) e-->f(Contact) e-->g(Account) e-->h(Opportunity) d-.->|no|i{Is the lead old, or did they respond negatively to outreach?} i-->|yes|j(Lead fails to graduate) i-.->|no, continue nurturing lead|d Sending data back to Salesforce If you want to send contact and lead data from Customer.io back into Salesforce, you’ll need to store Contact IDs or Lead IDs in Customer.io. In most cases, you’ll want to identify people by email address and store the Salesforce Contact ID or Lead ID in Customer.io as a contact_id or lead_id attribute. You’ll use these attributes to send data back to Salesforce, which expects you to identify people by their Salesforce IDs. When you set up your Salesforce Destination, you’ll set the Who ID—the value Salesforce uses to identify a person; and this could be a lead_id or a contact_id. To make sure that you use the correct ID, you’ll use the coalesce function to use the contact ID if it exists, and fall back to the lead ID if it doesn’t in the format coalesce($.customer.contact_id, $.customer.lead_id). Tasks are often events In general, we suggest that you map tasks to events because: Tasks, like events, are assigned to a single person They represent things that people do You can use events to trigger campaigns, automating your task Salesforce list Tasks in Salesforce are basically a single person’s to-do list for leads, accounts, opportunities and so on. While Salesforce doesn’t have a concept of “events,” tasks fit this bill, more or less, by changing states when you mark something off the to-do list. Think of it this way: You aren’t likely to manage your task list in Customer.io. Instead, you probably want to automate your tasks or respond to changes in your to-do list. You may want to send a message to a contact when their bill is due, or send a message that invites a lead to schedule a meeting with you. Convert your timestamps Customer.io and Salesforce use different formats for dates and times. Salesforce uses ISO-8601 timestamps (like 2021-09-01T09:00:00Z) and Customer.io uses Unix timestamps (like 1630512000). This difference can prevent you from using Salesforce date and time values with some Customer.io features. Luckily, you can automatically convert Salesforce’s timestamps to the format Customer.io expects. Go to Integrations and select the Connections tab. Then select your workspace and click Actions. Edit each actionThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. and set Convert dates to Unix timestamps setting to true, and we’ll automatically convert incoming timestamps to support Customer.io. Converting Salesforce timestamps to Customer.io’s Unix format lets you use Salesforce dates and times for things like for date-based conditions in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. or date-triggered campaigns.  You need to convert timestamps again if you send data back to Salesforce When you send data to Customer.io, you’ll convert dates and times to Unix timestamps. But if you send data from Customer.io to Salesforce you’ll need to convert those timestamps back to ISO 8601 formatted dates and times. --- ## Map data to other services URL: https://docs.customer.io/integrations/data-in/connections/salesforce/mapping-data/ When you send data from Salesforce to Customer.io, you'll convert it to people, events, or custom objects. But when you send data _outside_ of Customer.io, you'll need to map it to the right format for your destination.  Is Customer.io where you’re using your Salesforce data? Do you want to send messages? Check out Salesforce with Customer.io page for help connecting your Salesforce data to Customer.io, so you can send messages. This page is meant to help you send Salesforce data to places outside of Customer.io. How it works Unlike most platforms, Salesforce doesn’t have generic concepts like People and Groups—or things that map easily to those terms. Instead, Salesforce has highly structured data: your leads, contacts, opportunities, accounts, and other things are all separate. When you send Salesforce data to Customer.io, you’ll need to decide how you want to represent these entities downstream—whether they’re people, events, custom objects, or relationships. You’ll also pick the fields you want to capture from Salesforce for each type of incoming data. How you represent Salesforce data downstream depends on where you send your Salesforce data to and what you want to do with it: you may model data differently when you connect your data to Customer.io Journeys, as opposed to an analytics platform like Mixpanel. People, events, and custom objects People, events, and objects correspond to Customer.io’s identify, track, and group methods respectively. Relationships also correspond to group calls, but they’re a little different, so we’ll discuss them below. It can help to think of your data in the following terms: Is it an individual person? Do you want to send it messages? Map to people. Is it something a person does? Map to events. Is it a group of people or some kind of non-person thing? Map to custom objects. People, created and updated by identify calls, represent individual users, customers, contacts, so on. In Customer.io, people are who you’ll send messages to, track events for, etc. Events, created by track calls, are things that people do, like visiting pages on your website, adding items to shopping carts, or beginning online courses. Objects can’t perform events. When Salesforce converts data to an event, it’ll be associated with a person, not an object (or “group”). Custom Objects, created and updated by group calls, are entities that you want to track like people, but aren’t people. They’re things like accounts, companies, or opportunities. People are often related to custom objects, like a person who works for a company or a person who manages an account. flowchart LR a{Is it a contact?} a----->|yes|b(Person) a-.->|no|c{Is it associated with >1 people?} c--->|yes|d(Custom Object) c-.->|no|e{Does it have an email or phone?} e-->|yes|b e-.->|no|f{Is it something people do? Will it trigger messages?} f-->|yes|g(Event) f-.->|no|d Relationships: matching people to objects Salesforce treats people, custom objects, and the relationships between the two as discrete pieces of information. So, for example, when you sync accounts and contacts to Customer.io, you’ll also sync Account Contact Relationship information to capture relationships between your contacts and accounts. Whenever you need to capture a relationship between people and a group—like opportunities, companies, etc—you’ll need to set up a relationship sync to maintain those relationships. Where people need a userId, and custom objects need a groupId, relationships need both. When you sync relationships, we associate the userId with the groupId. And, like people and custom objects, relationships also contain traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. describing the relationship—like whether someone is directly related to an account, a primary contact, and so on. These attributes help you find the right person by association with an object, like when you need to find an account’s primary contact, or all active participants in an opportunity. What about other Salesforce data types? You can create custom data types in Salesforce to represent things meaningful to your business. When you set up a sync, you’ll convert that data to a call supported by our API—like identify, track, or group. We’ll show you the most popular data types at the top of the list, and then we’ll show all of your other data types. It’s up to you to decide how you want to represent your custom data downstream! The same principles above apply to both Salesforce’s most popular data types (like contacts) and any custom data types you’ve created in Salesforce. Mapping identifiers and fields When you setup your sync, you’ll tell people which fields represent your people and custom objects; you’ll also tell us which fields you want to pass to places outside Customer.io. Unique Identifiers tells us which values represent people or objects—like a Contact ID or Account ID. Even when you send data representing an event, we either associate the event with a person! Objects and relationships have a Related Record ID. For relationships, this tells us how to relate people to objects. For custom objects, you can probably ignore this field. The Fields to sync are the traits you want to pass downstream. You can toggle fields on and off when you set up or edit a sync.  Some Salesforce data maps to reserved traits We map some capitalized Salesforce data, like FirstName, to the snake-cased format Customer.io expects, firstName. We don’t otherwise change the data. Learn more about the fields we reserve. What’s the related record ID when I sync custom objects? In most cases, you can ignore the Related Record ID when you sync custom objects. For each object in Salesforce, like an account, Salesforce typically keeps a canonical contact, user, or some kind of ID. For an account, this might be something like the Owner ID. This value is unlikely to change, and often represents the person who created an object in Salesforce. While you can map this value in your sync, it’s unlikely to change and isn’t very useful unless you only care about the relationship between an object and its owner or creator in Customer.io. You’ll get far more value out of syncing relationships than you will by associating a single userID with an object/group. Reserved traits We reserve some traits for people and objects. So, when you map your incoming Salesforce data to people or objects, we convert some incoming properties—which are typically capitalized in Salesforce—to our snake-cased format. For example, a contact’s FirstName trait becomes firstName. Reserved traits for people Salesforce property Customer.io trait Notes CreatedDate created_at An epoch timestamp (in seconds) based on CreatedDate. We don’t modify CreatedDate Description description An optional description of the person. Email email The email address of a user. FirstName firstName The first name of a user, contact, or lead. LastName lastName The last name of a user, contact, or lead. Name name For users, contacts, or leads, this is a concatenation of the FirstName, MiddleName, LastName, and Suffix fields. Phone phone The person’s phone number. Title title The person’s title, like CFO or CEO. Website website A person’s website, typically associated with leads. Reserved traits for objects Salesforce property Customer.io trait Notes Description description An optional description of the group, like an explanation of an opportunity. Email email The email address of the group—like the primary email address for an account. Name name The name of the group. Phone phone The primary phone number of the group. --- ## Scheduled syncs URL: https://docs.customer.io/integrations/data-in/connections/salesforce/syncs/ When you setup your Salesforce integration, you'll determine how often you want to sync data to Customer.io. We pull data from Salesforce on a regular interval (that you set) called a sync. sequenceDiagram participant a as Salesforce participant b as Customer.io participant c as Destination service note over a, c: Sync Data: occurs on a schedule a->>a:Changes happen in Salesforce b->>a: Query for changes a->>b: Send changeset b->>b: Convert to people, events, objects b->>c: Send to destination While you can see incoming data in the Data In tab, syncs also show in the Imports tab. Here, you’ll see each scheduled sync operation and how many records were successful or failed. It gives you an at-a-glance understanding of the data that makes it into your pipeline. Scheduled syncs use Salesforce’s API limits. These limits are fairly generous if you’re using Salesforce’s Enterprise edition or Professional edition with the API access add-on. It’s highly unlikely you’ll hit the limit. How Salesforce handles API limits Salesforce has daily API limits which depend on your plan. These limits are shared across every application that uses the Salesforce API, not just Customer.io. When you reach an API limit, Salesforce stops sending data and responds with an error. When you reach your limit, you’ll stop seeing data in Customer.io for the rest of the 24 hour period. If you notice a conspicuous lack of Salesforce data for a period of time, it’s likely that you’ve reached your Salesforce’s API limit(s). You can find errors related to API limits by inspecting your latest syncs in the Imports tab. Salesforce OAuth connection limits You should not create more than five Salesforce connections (between data-in and data-out) per user. Salesforce enforces a limit of five connections per OAuth-enabled Connected Application, per user. When you hit this limit, Salesforce revokes the oldest connection. This limit is not configurable and Customer.io is not notified when Salesforce revokes a connection. Because this limit is per user, you can circumvent the limit by creating multiple Salesforce integration users and carefully managing their connections—no more than five connections per user.  What counts towards the limit? Salesforce counts an “authorized connection” when you click Connect and set up your Salesforce integration. Even if you don’t complete the connection flow, Salesforce counts it towards the limit! Syncs batch incoming data Syncs batch incoming data up to 1000 records at a time. This means that a sync can process up to 1000 Salesforce records with a single Salesforce API call. This helps stay within your Salesforce REST API limits. --- ## Deleting Data URL: https://docs.customer.io/integrations/data-in/connections/salesforce/delete/ We represent data you delete in Salesforce with events. You can use these events to determine how you handle deleted data in each of your downstream integrations.  Customer.io Journeys automatically handles deleted Salesforce data The information below is important if you want to handle deletions in services outside of Customer.io. But, if you don’t send your Salesforce data to anywhere other than Customer.io, then you don’t need to do anything to handle deleted data; we handle it for you. How it works You can delete data in Salesforce, but the API that Salesforce uses to ingest data from Salesforce doesn’t have Delete operations. So how do your other services know when to delete data from Salesforce? We use Semantic Events: events that have special meanings in Customer.io. Imagine you delete a contact in Salesforce, and you’ve mapped contacts to people in Customer.io. When you create and update people in Salesforce, we’ll send identify calls to create/update people. But when you delete a person, we’ll send a track call with the event name Delete Person. And you’ll use this event to handle the deletion. flowchart LR a(Incoming Salesforce Data) a-->b{What kind of data is it?} b-->|person|c(Identify) b-->|object|d(Group) b-->|event|e(Track) b-->|relationship|g(Group with userID) b-.->|delete Here's where we use semantic events|f("Track event where the event name is Person/Object Deleted") Semantic events for delete operations The data-deleting operations you can perform from Salesforce correspond to simple event names. You’ll use these event names when you set up actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to delete Salesforce data. Delete Person: Removes a person Delete Object: Removes an object, like an account or opportunity Delete Relationship: Removes a relationship between a person and an object, like when a contact leaves a company, or an account transfers to a new account manager. You’ll notice that there’s no Delete Event operation. You can’t delete an event. An event is something that happened at a point in time. You can’t go back in time and stop that thing from happening! Customer.io Journeys automatically handles deleted data When you connect Salesforce to Customer.io Journeys, you’re already set to handle deleted data. We handle the semantic events coming out of Salesforce by default. Handling semantic events in places outside of Customer.io If you send your Salesforce data to places outside of Customer.io, you may need set up actionsA block in a campaign workflow—like a message, delay, or attribute change. to handle delete operations. When you set up an action, you only need to pay attention to the Action and Trigger fields. You likely don’t care about the data Structure, because you’re deleting data! In outbound integration, go to Actions and click Add Action. Set the action to the appropriate operation—like Delete Person or Delete Object. In your Trigger, set a condition based on the semantic event name in the format: Track Event Name is <Name of semantic event>. This means that when an event with the correct name comes in, you’ll perform the appropriate delete operation. Click Save Action. --- ## API Call Calculator URL: https://docs.customer.io/integrations/data-in/connections/salesforce/api-calls-simulator/ Estimate the number of API calls that your Salesforce integration will make for its initial and incremental syncs. This can help you figure out if your Salesforce integration will exceed your Salesforce plan limits. Number of records for initial sync: Number of new/updated records per sync: Sync interval: Minutes Hours Days Weeks Use Bulk API: Bulk API REST API We require one REST call at the start of each sync, to fetch the record's fields configuration. Number of API calls for initial sync For the initial sync, we fetch all data going back to the epoch (Jan 1st 1970). We break that data down into 30-day increments, and that's how we get to such a specific (and static) number here. Number of API calls for each incremental sync Number of API calls per day Number of API calls per week Number of API calls per month 1Value used for Bulk API max requests per 24-hour period: 2Value used for Bulk API max records per request: 3Value used for REST API max requests per 24-hour period: 4Value used for REST API max records per request: This calculator is for estimation purposes only. It cannot provide an exact count of API calls consumed by your integration. Your Salesforce API limits will vary depending on your Salesforce plan. See Salesforce’s documentation to learn more about Bulk API and REST API limits. Our Salesforce integration attempts to use Salesforce’s bulk APIs whenever possible, which can handle up to 10000 records per request, depending on your plan. We fall back to the REST API, where each call can retrieve 2000 records per request, when we cannot use the Bulk API. We cannot use the Bulk API when: your records contain fields with base64- or object-formatted values due to limitations with Salesforce’s output format. the record type does not support Bulk APIs (ex: KnowledgeArticle, CaseStatus). This calculator is based on a normal integration scenario and cannot account for all possible integration scenarios. For example, if we receive a temporary error from the Salesforce API we’ll retry operations up to 3 times, which increases your API usage. --- ## Node.js URL: https://docs.customer.io/integrations/data-in/connections/servers/node/ How it works Our Node.js library helps you record source events from your node-side code. Requests from your Node.js server go to our servers, and we route your data to your destinations. This library uses an internal queue so that your identify and track calls are fast and non-blocking. It also batches requests and flushes asynchronously to Customer.io’s servers. Like our other libraries, you can log anonymous activity—track and page events—with an anonymousId. When you identify a person, you can pass the anonymousId and we’ll associate the anonymous activity with the identified person. Getting Started  We support node 14 or later If you’re on an earlier version of node, you should upgrade to take advantage of our Node.js library. Go to the tab and click Sources. Click Add Source and pick Node.js. Give the source a Name and click Complete Setup. The name is simply a friendly name to help you find and recognize your source in Customer.io. On your Node server, install the source: # npm npm install @customerio/cdp-analytics-node # yarn yarn add @customerio/cdp-analytics-node # pnpm pnpm install @customerio/cdp-analytics-node Use the Analytics constructor and initialize Customer.io with your API Key. If you’re in our EU region, make sure you set the host parameter to https://cdp-eu.customer.io. import { Analytics } from '@customerio/cdp-analytics-node' // or, if you use require: const { Analytics } = require('@customerio/cdp-analytics-node') // instantiation const cioanalytics = new Analytics({ writeKey: '<YOUR_API_KEY>' // if you're in our EU region // host: 'https://cdp-eu.customer.io', }) This creates an instance of Analytics that you can use to send data to Customer.io. The default initialization settings are production-ready and queue 20 messages before sending requests. Now you’re ready to send requests to Customer.io. Check out our Pipelines API reference, or read further to see example requests and understand the types of requests you can make using our Node.js library. As you work on your integration, you might want to use development settings. If you’re in our EU data center You’ll need to set the endpoint parameter to set our EU URL (https://cdp-eu.customer.io). Note that our EU regional endpoints account for the location of your data in Customer.io; they don’t account for the locations of your sources and destinations. import { Analytics } from '@customerio/cdp-analytics-node' const cioanalytics = new Analytics({ writeKey: '<YOUR_API_KEY>' host: 'https://cdp-eu.customer.io', }) Identify The identify method tells us who the current website visitor is, and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. You should call identify when a user creates an account, logs in, etc. You can also call it again whenever a person’s traits change. We’ve shown a typical call with a traits object, but we’ve listed all the fields available in an identify call below. You can send an identify call with an anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data sent in previous anonymous page, track, and identify calls with the person you identify by userId. cioanalytics.identify({ userId: '019mr8mf4r', traits: { name: 'Cool Person', email: 'cool.person@example.com', plan: 'Enterprise', friends: 42 } }); integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Track The track method tells us about actions people take—the events people perform—on your site. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. You can send events with an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. Track calls require an event name describing what a person did. And they generally include a series of properties, providing additional information about the event. Beyond that, we’ve provided a complete schema for writable event fields below, and you can find more information in our API documentation. track with userId track with userId cioanalytics.track({ userId: '019mr8mf4r', event: 'added_to_cart', properties: { product: "shoes", revenue: 39.95, qty: 1, size: 9 } }); track with anonymousId track with anonymousId cioanalytics.track({ anonymousId: '48d213bb-95c3-4f8d-af97-86b2b404dcfe', event: 'added_to_cart', properties: { product: "shoes", revenue: 39.95, qty: 1, size: 9 } }); event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Enable automatic geolocation support You can automatically geolocate people when you identify them and pass their IP addresses in the context.ip field in your identify requests. This helps you gather information about your audience’s location and time zone so you can schedule messages at the right times or send messages relevant to their communities. If you’ve already set up your integration to capture IP addresses, and you’ve enabled the workspace-level Automatic Geolocation Data Collection setting, you can enable geolocation for your integration. After you set up your integration, go to your integration’s Settings tab and turn on the Enable Geolocation setting.  Make sure you capture your users’ IP addresses If you don’t set the context.ip in your requests, we won’t be able to capture geolocation data for your users. If our libraries infer the address as your server’s IP address, it’ll look like everyone is in the same location as your server. Page The Page method records page views on your website, along with optional extra information about the page a person visited. If you’re using Customer.io’s client-side set up in combination with the Node.js library, page calls are already tracked for you by default on any page that loads the client-side script. But, if you have a single page app or you don’t use our JavaScript client library on your website, you’ll need to send your own page calls. cioanalytics.page({ userId: '019mr8mf4r', category: 'Docs', name: 'Customer.io CDP', properties: { url: 'https://customer.io/cdp/', path: '/cdp/', title: 'Customer.io CDP', referrer: 'https://customer.io' } }); integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Group The Group method associates an identified person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Group calls are useful for integrations where you maintain relationships between people and larger organizations, like in Customer.io! In Customer.io Journeys, you can store groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and trigger campaigns based on a person’s relationship to an object—like an account, online class, and so on. Find more details about group, including the group payload, in our API spec. cioanalytics.group({ userId: '019mr8mf4r', groupId: '56', traits: { name: 'Initech', description: 'Accounting Software' } });  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. Here’s how you might use the alias call. In this case, we start with an anonymous_user and switch to an email address when a person provides their userId. // the anonymous user does actions ... cioanalytics.track({ userId: 'anonymous_user', event: 'Anonymous Event' }) // the anonymous user signs up and is aliased cioanalytics.alias({ previousId: 'anonymous_user', userId: 'identified@example.com' }) // the identified user is identified cioanalytics.identify({ userId: 'identified@example.com', traits: { plan: 'Free' } }) // the identified user does actions ... cioanalytics.track({ userId: 'identified@example.com', event: 'Identified Action' }) previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. Configuration The first argument for the Analytics constructor is a dictionary of configuration settings, including your API key and optional settings. var cioanalytics = new Analytics({ writeKey: 'YOUR_API_KEY', maxEventsInBatch: 20, flushInterval: 10000, }); Setting Details maxEventsInBatch (Number) The number of messages to enqueue before flushing. flushInterval (Number) The number of milliseconds to wait before flushing the queue automatically. Error Handling You can listen for error events on the Analytics instance. Errors contain the following properties: code: The code of the error. reason: The error, like an HTTP error, network error, etc. ctx: The context object (for delivery failures). const { Analytics } = require('@customerio/cdp-analytics-node'); const client = new Analytics({ writeKey: 'api key' }); // Listen to the 'error' event client.on('error', (err) => { console.error('cdp-analytics-node error occurred:'); console.error('Code:', err.code); console.error('Reason:', err.reason); if (err.ctx) { console.error('Context:', err.ctx); } }); // Now you can make analytics calls client.track({ userId: '123', event: 'Test Event' }); Development While integrating with Customer.io, you might want to make our library flush after every event or call. This can help you test your implementation and make sure that all of your calls work properly before you start making calls from your production environment. var cioanalytics = new Analytics({ writeKey: 'YOUR_API_KEY', maxEventsInBatch: 1 }); Selecting Destinations You can pass an integrations object to outgoing calls to turn certain destinations on or off. By default all destinations are enabled. Passing false for an integration disables the call to that destination. You might want to do this for things like alias calls, which aren’t supported by all destinations. All: false disables all destinations except the ones you explicitly specify. cioanalytics.track({ event: 'Membership Upgraded', userId: '97234974', integrations: { 'All': false, 'Mixpanel': true, 'Google Analytics': false } }) Destination flags are case sensitive. You’ll find each integration’s name at the top of each integration’s page in our documentation.  You can filter track calls on the source’s Schema tab We recommend that you filter events in our UI if you can. It’s easier than writing code, and you can update your source or make changes to your filters without involving developers! Backfilling historical data You can backfill data by adding a timestamp to your calls. This can be helpful if you’ve just switched to Customer.io or you’re getting started with Customer.io and want to send historical data. You can only do this for destinations that accept timestamped data (most analytics tools like Mixpanel and Amplitude do). The notable destination that doesn’t support timestamped data is Google Analytics.  Leave out the timestamp if you’re tracking real-time events If you’re only tracking things as they happen, you can leave the timestamp out of your calls and we’ll timestamp requests for you. Batching Our libraries are built to support high performance environments. It’s safe to use this library on a web server that serves hundreds of requests per second. But every method you invoke does not result in an HTTP request. Instead, we queue requests in memory and then flush them in batches, which allows for more efficient operation. By default, our Node.js source library flushes: The very first call. Every 20 messages (controlled by options.maxEventsInBatch). If 10 seconds pass after the previous flush (controlled by options.flushInterval) There is a maximum of 500KB per batch request and 32KB per call. If you don’t want to batch messages, you can turn batching off by setting the maxEventsInBatch option to 1. Batching means that your message might not get sent right away. Every method call takes an optional callback, which you can use to know when a particular message is flushed from the queue. cioanalytics.track({ userId: '019mr8mf4r', event: 'Ultimate Played' }, function(err, batch){ if (err) // There was an error flushing your message... // Your message was successfully flushed! }); Serverless applications When using the library from serverless applications such as AWS Lambda, Cloudflare Workers or Vercel Functions, you should invoke the closeAndFlush method to process all data before your lambda exits or is suspended. Make sure you create the cioanalytics object and call the closeAndFlush method from the handler. When you call closeAndFlush, you’ll no longer be able to use the cioanalytics object to send messages. export default async function handler(req, res) { var cioanalytics = new Analytics({ writeKey: 'YOUR_API_KEY' }); await cioanalytics.track({ userId: '019mr8mf4r', event: 'Ultimate Played' }); await cioanalytics.closeAndFlush(); console.log('Flushed, and now this program can exit!'); } Flush long running processes Because we queue messages, you’ll want to capture interruptions (for example, a server restart) and call closeAndFlush so that you don’t inadvertently drop requests when you need to perform maintenance on your server. import { randomUUID } from 'crypto'; import Analytics from 'cdp-analytics-node' const API_KEY = '...'; const cioanalytics = new Analytics({ writeKey: API_KEY, maxEventsInBatch: 10 }); cioanalytics.track({ anonymousId: randomUUID(), event: 'Test event', properties: { name: 'Test event', timestamp: new Date() } }); const exitGracefully = async (code) => { console.log('Flushing events'); await cioanalytics.closeAndFlush(function(err, batch) { console.log('Flushed, and now this program can exit!'); process.exit(code); }); }; [ 'beforeExit', 'uncaughtException', 'unhandledRejection', 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT','SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM', ].forEach(evt => process.on(evt, exitGracefully)); function logEvery2Seconds(i) { setTimeout(() => { console.log('Infinite Loop Test n:', i); logEvery2Seconds(++i); }, 2000); } logEvery2Seconds(0); Multiple Clients Different parts of your application may require different types of batching, or even sending to multiple Customer.io sources. In these cases, you can initialize multiple instances of Analytics with different settings! var Analytics = require('cdp-analytics-node'); var marketingAnalytics = new Analytics({ writeKey: 'MARKETING_API_KEY' }); var appAnalytics = new Analytics({ writeKey: 'APP_API_KEY' }); --- ## Python URL: https://docs.customer.io/integrations/data-in/connections/servers/python/ How it works Our python library helps you record source events from your node-side code. Requests from your python app go to our servers, and we route your data to your destinations. This library uses an internal queue so that your identify and track calls are non-blocking and fast. It also batches requests and flushes asynchronously to Customer.io’s servers. Like our other libraries, you can log anonymous activity—track and page events—with an anonymousId. When you identify a person, you can pass the anonymousId and we’ll associate the anonymous activity with the identified person. Getting Started Go to the tab and click Sources. Click Add Source and pick Python. Give the source a Name and click Complete Setup. The name is simply a friendly name to help you find and recognize your source in Customer.io. Install the python library. If you use a system to manage dependencies, you should pin the library to 1.X to avoid breaking changes when we make updates. pip install customerio-cdp-analytics Import the library in your app and set your write_key before making any analytics. If you’re in our EU data center, you can also set the host parameter to https://cdp-eu.customer.io. from customerio import analytics analytics.write_key = 'YOUR_WRITE_KEY' # If you're in our EU data center # analytics.host = 'https://cdp-eu.customer.io' Now you’re ready to make calls to Customer.io! The default initialization settings are production-ready and will queue individual analytics calls. A separate background thread is responsible for making the requests to Customer.io, so calls to the library won’t block your program’s execution.  You can send multiple sources If you need to send data from multiple sources, you can initialize a new Client for each write_key! If you’re in our EU data center You’ll need to set the host parameter to our EU URL (https://cdp-eu.customer.io). Note that our EU regional endpoints account for the location of your data in Customer.io; they don’t account for the locations of your sources and destinations. from customerio import analytics analytics.write_key = 'YOUR_WRITE_KEY' analytics.host = 'https://cdp-eu.customer.io' Enable automatic geolocation support You can automatically geolocate people when you identify them and pass their IP addresses in the context.ip field in your identify requests. This helps you gather information about your audience’s location and time zone so you can schedule messages at the right times or send messages relevant to their communities. If you’ve already set up your integration to capture IP addresses, and you’ve enabled the workspace-level Automatic Geolocation Data Collection setting, you can enable geolocation for your integration. After you set up your integration, go to your integration’s Settings tab and turn on the Enable Geolocation setting.  Make sure you capture your users’ IP addresses If you don’t set the context.ip in your requests, we won’t be able to capture geolocation data for your users. If our libraries infer the address as your server’s IP address, it’ll look like everyone is in the same location as your server. Development settings By default, the python library is set to queue and send requests directly to Customer.io. But, while you’re integrating this library, you should enable some settings to help you troubleshoot problems. Use analytics.debug to log debugging information to the python logger Set an on_error handler to print the response you receive from our API. def on_error(error, items): print("An error occurred:", error) analytics.debug = True analytics.on_error = on_error You can also prevent the library from sending data to Customer.io during testing. This can save you the trouble of cleaning out bogus data later. analytics.send = False Identify The identify method tells us who the current website visitor is, and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. You should call identify when a user creates an account, logs in, etc. You can also call it again whenever a person’s traits change. We’ve shown a typical call with a traits object, but we’ve listed all the fields available in an identify call below. You can send an identify call with an anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data sent in previous anonymous page, track, and identify calls with the person you identify by userId. analytics.identify('f4ca124298', { 'email': 'cool.person@example.com', 'first_name': 'cool', 'last_name': 'person' }) integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Track The track method tells us about actions people take—the events people perform—on your site. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. You can send events with an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. Track calls require an event name describing what a person did. And they generally include a series of properties, providing additional information about the event. Beyond that, we’ve provided a complete schema for writable event fields below, and you can find more information in our API documentation. analytics.track('f4ca124298', 'added_to_cart', { 'product': "shoes", 'revenue': 39.95, 'qty': 1 'size': 9 }) event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Deduplicate events Generally, we’ll generate a message_id for each event you send to Customer.io. But, you can set your own message_id, which might be helpful if you need to deduplicate events. We’ll accept the first instance of any operation with a given message_id and ignore any operations with the same message_id for the next 12 hours. The message_id is can be any string value, but we recommend a hash of the event data or a UUID/ULID to ensure that you don’t inadvertently deduplicate events. If you backdate events, you’ll need to deduplicate them before you send them to Customer.io. We deduplicate the message_id within 12 hours from when we receive the event—not the timestamp on the event itself. analytics.track( user_id = 'f4ca124298', event = 'added_to_cart', properties = { 'product': "shoes", 'revenue': 39.95, 'qty': 1, 'size': 9, }, message_id = 'message_id_here', ) Page The Page method records page views on your website, along with optional extra information about the page a person visited. If you’re using Customer.io’s client-side JavaScript library in combination with our python library, then the client side JavaScript library already captures page calls for you by default. But, if you have a single page app or you don’t use our JavaScript client library on your website, you’ll need to send your own page calls. Structure Structure analytics.page('<user_id>', 'category', 'name', { 'properties': 'any' }, { #options 'integrations': { #Enable/disable integrations #By default, all destinations are enabled } }) Example Example analytics.page('<user_id>', 'Retail Page', 'shoes', { 'url': 'https://example.com/products/showes' }) integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Group The Group method associates an identified person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Group calls are useful for integrations where you maintain relationships between people and larger organizations, like in Customer.io! In Customer.io Journeys, you can store groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and trigger campaigns based on a person’s relationship to an object—like an account, online class, and so on. Find more details about group, including the group payload, in our API spec. analytics.group('user_id', 'group_id', { 'name': 'Initech', 'domain': 'Accounting Software' })  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. analytics.alias(previous_id, user_id) Here’s how you might use the alias call. In this case, we start with an anonymous_user and switch to an email address when a person provides their userId. # the anonymous user does actions under an anonymous ID analytics.track('92734232-2342423423-973945', 'Anonymous Event') # the anonymous user signs up and is aliased to their new user ID analytics.alias('92734232-2342423423-973945', '1234') # the user is identified analytics.identify('1234', { 'plan': 'Free' }) # the identified user does actions analytics.track('1234', 'Identified Action') previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. Configuration and Library Options If you want to change the library’s default settings want to send data to multiple sources, you can create your own client(s). Remember that each client runs a separate background thread, so you won’t want to create new clients on every request. from analytics import Client Client('YOUR_WRITE_KEY', debug=True, on_error=on_error, send=True, max_queue_size=100000, upload_interval=5, upload_size=500, gzip=True) Field Description debug bool Set True to enable verbose logging, False by default. send bool Set False to avoid sending data to Customer.io, True by default. on_error function Set an error handler to be called whenever errors occur. max_queue_size int The maximum number of elements allowed in the queue. Hitting the max queue size means you’re identifying / tracking faster than you can flush. If this happens, let us know! upload_interval float The frequency, in seconds, of sends to Customer.io. Default value is 0.5. upload_size int The number of items per batch upload. Default value is 100. gzip bool Set True to compress data with gzip before sending, False by default. Selecting Destinations You can pass an integrations object to alias, group, identify, page and track calls that lets you turn certain destinations on or off. By default all destinations are enabled. Passing false for an integration disables the call to that destination. You might want to do this for things like alias calls, which aren’t supported by all destinations. In this case, Customer.io specifies the track to only go to Vero. All: false disables all destinations except the ones you explicitly specify. analytics.track('user_id', 'Membership Upgraded', integrations={ 'All': False, 'Mixpanel': True, 'Google Analytics': False }) Destination flags are case sensitive. You’ll find each integration’s name at the top of each integration’s page in our documentation.  You can filter track calls on the source’s Schema tab We recommend that you filter events in our UI if you can. It’s easier than writing code, and you can update your source or make changes to your filters without involving developers! Backfilling historical data You can backfill data by adding a timestamp to your calls. This can be helpful if you’ve just switched to Customer.io. You can only do this for destinations that accept timestamped data—most analytics tools like Mixpanel and Amplitude do. The notable destination that doesn’t support timestamped data is Google Analytics. import datetime from dateutil.tz import tzutc timestamp = datetime.datetime(2538, 10, 17, 0, 0, 0, 0, tzinfo=tzutc()) analytics.track('019mr8mf4r', 'started_class', { 'class': 'How to Use CDP' }, timestamp=timestamp)  Leave out the timestamp if you’re tracking real-time events If you’re only tracking things as they happen, you can leave the timestamp out of your calls and we’ll timestamp requests for you. Time zones in Python Python’s datetime module supports two types of date and time objects: naive objects without time zone information, and aware objects that include time zones. By default, newly created datetime objects are naive. Make sure that you use time zone aware objects when you import data so that you send time zone information correctly. We created an aware datetime object in the previous section using the tzinfo argument to the datetime constructor. If you omitted this argument, we would not pass time zone info: >>> naive = datetime.datetime(2015, 1, 5, 0, 0, 0, 0) >>> aware = datetime.datetime(2015, 1, 5, 0, 0, 0, 0, tzinfo=tzutc()) >>> naive.isoformat() '2015-01-05T00:00:00' >>> aware.isoformat() '2015-01-05T00:00:00+00:00' If you have an ISO format timestamp string that contains time zone information, dateutil.parser can create aware datetime objects. >>> import dateutil.parser >>> dateutil.parser.parse('2012-10-17T18:58:57.911Z') datetime.datetime(2012, 10, 17, 18, 58, 57, 911000, tzinfo=tzutc()) >>> dateutil.parser.parse('2016-06-06T01:46:33.939388+00:00') datetime.datetime(2016, 6, 6, 1, 46, 33, 939388, tzinfo=tzutc()) >>> dateutil.parser.parse('2016-06-06T01:46:33.939388+07:00') datetime.datetime(2016, 6, 6, 1, 46, 33, 939388, tzinfo=tzoffset(None, 25200)) >>> dateutil.parser.parse('2016-06-06T01:46:33.939388-07:00') datetime.datetime(2016, 6, 6, 1, 46, 33, 939388, tzinfo=tzoffset(None, -25200)) If you find yourself with a naive object, and know what time zone it should be in, you can also use pytz to create an aware datetime object from the naive one. >>> import datetime >>> import pytz >>> naive = datetime.datetime.now() >>> aware = pytz.timezone('US/Pacific').localize(naive) >>> naive.isoformat() '2016-06-05T21:52:14.499635' >>> aware.isoformat() '2016-06-05T21:52:14.499635-07:00' The pytz documentation contains additional information on time zone usage, and can help you handle edge cases. Batching Our libraries are built to support high performance environments. It’s safe to use this library on a web server that serves hundreds of requests per second. But every method you invoke does not result in an HTTP request. Instead, we queue requests in memory and then flush them in batches, which allows for more efficient operation. By default, our Python source library flushes: every 100 messages (control with upload_size) if 0.5 seconds has passed since the last flush (control with upload_interval) There is a maximum of 500KB per batch request and 32KB per call. What happens if there are too many messages? If our python module can’t flush calls faster than it’s receiving them, it’ll simply stop accepting requests. This means your program will never crash because of a backed up analytics queue. The default max_queue_size is 10000. Flush events on demand You can flush your queue on demand. For example, at the end of your program, you’ll want to flush to make sure there’s nothing left in the queue. Just call the flush method. analytics.flush() This method blocks the calling thread until there the message queue is empty. You’ll want to use it as part of your cleanup scripts and avoid using it as part of the request lifecycle. How do I gzip requests? You can compress batched requests before you send them to Customer.io by setting the gzip argument when constructing your Client. from analytics import Client Client('YOUR_WRITE_KEY', gzip=True) Detecting errors You can listen to events on failed flush attempts. def on_error(error, items): print('Failure', error) analytics.on_error = on_error Logging Our library uses the standard python logging module. By default, logging is enabled and set at the WARNING level. If you want more verbose logs, you can set a different log_level: import logging logging.getLogger('customerio').setLevel('DEBUG') --- ## Go URL: https://docs.customer.io/integrations/data-in/connections/servers/go/ How it works Our Go library helps you record source events from your server-side code. This lets your Go-based app send requests to our servers, and we route your data to your cloud-mode destinations. Like our other libraries, you can log anonymous activity with an anonymousId. When you identify a person, you can pass the anonymousId and we’ll associate the anonymous activity with the identified person. This library uses a configurable buffer to batch messages, optimized to reduce network activity. Getting Started Go to the tab and click Sources. Click Add Source and pick Go. Give the source a Name and click Complete Setup. The name is simply a friendly name to help you find and recognize your source in Customer.io. Install analytics-go using go get: go get github.com/customerio/cdp-analytics-go Import the library and initialize an instance with your source’s API Key. You’ll find your key in your source’s Settings tab. You can initialize the library with configuration settings—like using our EU data center or development settings to help you test your implementation. package main import ( "github.com/customerio/cdp-analytics-go" ) func main() { client := analytics.NewWithConfig("YOUR_API_KEY", analytics.Config{ // Set config options here // If you're in our EU data center, set Endpoint: // Endpoint: "https://cdp-eu.customer.io", }) // Flush queued messages and close the client. defer client.Close() } This creates a client that you can use to send data to Customer.io. The client uses our default, production settings but you can tune these settings to fit your need. While you develop your integration, you may want to use development settings so it’s easier to test and debug your work! Now you’re ready to send requests to Customer.io. Check out our Pipelines API reference, or read further to see example requests and understand the types of requests you can make using our Go library. If you’re in our EU data center You’ll need to update your config to use our EU endpoint. Note that this accounts for the location of your data in Customer.io; it doesn’t account for the locations of your sources and destinations. func main() { client, err := analytics.NewWithConfig("YOUR_API_KEY", analytics.Config{ Endpoint: "https://cdp-eu.customer.io", }) // use the SDK here // Flush queued messages and close the client. defer client.Close() } Enable automatic geolocation support You can automatically geolocate people when you identify them and pass their IP addresses in the context.ip field in your identify requests. This helps you gather information about your audience’s location and time zone so you can schedule messages at the right times or send messages relevant to their communities. If you’ve already set up your integration to capture IP addresses, and you’ve enabled the workspace-level Automatic Geolocation Data Collection setting, you can enable geolocation for your integration. After you set up your integration, go to your integration’s Settings tab and turn on the Enable Geolocation setting.  Make sure you capture your users’ IP addresses If you don’t set the context.ip in your requests, we won’t be able to capture geolocation data for your users. If our libraries infer the address as your server’s IP address, it’ll look like everyone is in the same location as your server. Identify The identify method tells us who the current website visitor is, and lets you assign unique traitsA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. to a person. You should call identify when a user creates an account, logs in, etc. You can also call it again whenever a person’s traits change. We’ve shown a typical call with a traits object, but we’ve listed all the fields available in an identify call below. You can send an identify call with an anonymousId and/or userId. anonymousId only: This assigns traits to a person before you know who they are. userId only: Identifies a user and sets traits. both userId and anonymousId: Associates the data sent in previous anonymous page, track, and identify calls with the person you identify by userId. client.Enqueue(analytics.Identify{ UserId: "019mr8mf4r", Traits: analytics.NewTraits(). SetName("Cool Person"). SetEmail("cool.person@example.com"). Set("plan", "Enterprise"). Set("fav_number", 42), }) integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Track The track method tells us about actions people take—the events people perform—on your site. Every track call represents an event. You should track your audience’s activities with events both as performance indicators and so you can respond to your audience’s activities with campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. in Journeys. For example, if your audience performs a Video Viewed or Item Purchased event, you might respond with other videos or products the person might enjoy. You can send events with an anonymousId or a userId. Calls that you make with an anonymousId are associated with a userId when you identify someone by their userId. Track calls require an event name describing what a person did. And they generally include a series of properties, providing additional information about the event. Beyond that, we’ve provided a complete schema for writable event fields below, and you can find more information in our API documentation. track with userId track with userId client.Enqueue(analytics.Track{ UserId: "f4ca124298", Event: "added_to_cart", Properties: analytics.NewProperties(). Set("product", "shoes"), Set("price", 39.95), }) track with anonymousId track with anonymousId client.Enqueue(analytics.Track{ anonymousId: "48d213bb-95c3-4f8d-af97-86b2b404dcfe", Event: "added_to_cart", Properties: analytics.NewProperties(). Set("product", "shoes"), Set("price", 39.95), }) event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. event string Required The name of the event integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean properties object Additional properties for your event. Event Properties* any type Additional properties that you want to capture in the event. These can take any JSON shape. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Deduplicate events By default, we automatically assign a messageId to each call you make to Customer.io. But, you can set your own messageId if you need to deduplicate calls to Customer.io, ensuring that you don’t bog down your workspace with unnecessary traffic or trigger unnecessary downstream actions. We’ll accept the first instance of any operation with a given messageId and ignore any operations with the same messageId for the next 12 hours. The messageId is can be any string value, but we recommend a hash of the event data or a UUID/ULID to ensure that you don’t inadvertently deduplicate events. If you backdate events, you’ll need to deduplicate them before you send them to Customer.io. We deduplicate the messageId for 12 hours after we receive the operation—not the timestamp on the event itself. client.Enqueue(analytics.Track{ UserId: "f4ca124298", Event: "added_to_cart", Properties: analytics.NewProperties(). Set("product", "shoes"). Set("price", 39.95), MessageId: "message_id_here", }) Page The Page method records page views on your website, along with optional extra information about the page a person visited. If you’re using Customer.io’s client-side set up in combination with the Go library, page calls are already tracked for you by default on any page that loads the client-side script. But, if you have a single page app or you don’t use our JavaScript client library on your website, you’ll need to send your own page calls. client.Enqueue(analytics.Page{ UserId: "f4ca124298", Name: "Customer.io CDP", Category: "Docs", Properties: analytics.NewProperties(). SetURL("https://customer.io/cdp/"), }) integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean name string Required The name of the page. properties object Additional properties for your event. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. Page Properties* any type Additional properties tha tyou want to send with the page event. By default, we capture `url`, `title`, and stuff. timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. Group The Group method associates an identified person with a group—like a company, organization, project, online class or any other collective noun you come up with for the same concept. In Customer.io Journeys, we call groups objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.. Group calls are useful for integrations where you maintain relationships between people and larger organizations, like in Customer.io! In Customer.io Journeys, you can store groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course., and trigger campaigns based on a person’s relationship to an object—like an account, online class, and so on. Find more details about group, including the group payload, in our API spec. client.Enqueue(analytics.Group{ UserId: "019mr8mf4r", GroupId: "56", Traits: map[string]interface{}{ "name": "Initech", "description": "Accounting Software", }, })  Include objectTypeId when you send data to Customer.io Customer.io supports different kinds of groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) where each object has an object type represented by an incrementing integer beginning at 1. If you send group calls to Customer.io, you should include the object type ID or we’ll assume that the object type is 1. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. groupId string Required ID of the group integrations object Contains a list of booleans indicating the integrations that are enabled (true) or disabled (false). By default, all integrations are enabled (returning an empty object). Set "All": false to reverse this behavior. Enabled/Disabled integrations* boolean timestamp string  (date-time) The ISO-8601 timestamp when the event originally took place. This is mostly useful when you backfill data past events. If you’re not backfilling data, you can leave this field empty and we’ll use the current time or server time. traits object Additional information about the group. Group Traits* any type Additional traits you want to associate with this group. Alias The Alias method combines two previously unassociated user identities. Some integrations automatically reconcile profiles with different identifiers based on whether you send anonymousId, userId, or another trait that the integration expects to be unique. But for integrations that don’t, you may need to send alias requests to do this. In general, you won’t need to use the alias call; we try to handle user identification gracefully so you don’t need to merge profiles. But you may need to send alias calls to manage user identities in some data-out integrations. For example, in Mixpanel it’s used to associate an anonymous user with an identified user once they sign up. Here’s how you might use the alias call. In this case, we start with an anonymous_user and switch to an email address when a person provides their userId. // the anonymous user does actions ... client.Enqueue(analytics.Track{ Event: "Anonymous Event", UserId: anonymousUser, }) // the anonymous user signs up and is aliased client.Enqueue(analytics.Alias{ PreviousId: anonymousUser, UserId: "019mr8mf4r", }) // the identified user is identified client.Enqueue(analytics.Identify{ UserId: "019mr8mf4r", Traits: map[string]interface{}{ "name": "Michael Bolton", "email": "mbolton@example.com", "plan": "Enterprise", "friends": 42, }, }) // the identified user does actions ... client.Enqueue(analytics.Track{ Event: "Item Viewed", UserId: "019mr8mf4r", Properties: map[string]interface{}{ "item": "lamp", }, }) previousId string Required The userId that you want to merge into the canonical profile. userId string Required The userId that you want to keep. This is required if you haven’t already identified someone with one of our web or server-side libraries. Development settings While implementing Customer.io, you might want to make our library flush after every outgoing call. This can help you test your implementation and make sure that all of your calls work properly before you use this library in your production environment. Set the BatchSize field in your configuration to 1 to make the library flush every after each request, so that you can make sure your calls are working properly. func main() { client, _ := analytics.NewWithConfig("YOUR_API_KEY", analytics.Config{ BatchSize: 1, }) } Logging The Verbose field of your configuration controls the level of logging, while the Logger field provides a hook to capture the log output: func main() { client, _ := analytics.NewWithConfig("YOUR_API_KEY", analytics.Config{ Verbose: true, Logger: analytics.StdLogger(log.New(os.Stderr, "customerio ", log.LstdFlags)), }) } Selecting Destinations You can pass an integrations object to alias, group, identify, page and track calls that lets you turn certain destinations on or off. By default all destinations are enabled. Passing false for an integration disables the call to that destination.  You can also disable destinations in the user interface If you disable a destination in the UI, you can’t enable a destination through the integrations object. Disabling an integration in the UI prevents us from sending data to a downstream integration all together. You might want to do this for things like alias calls, which aren’t supported by all destinations. In this case, Customer.io specifies the track to only go to Mixpanel. All: false disables all destinations except the ones you explicitly specify. client.Enqueue(analytics.Track{ Event: "Membership Upgraded", UserId: "019mr8mf4r", Integrations: map[string]interface{}{ "All": false, "Mixpanel": true, }, }) Destination flags are case sensitive and match the integration’s name in our documentation.  You can filter track calls on the source’s Schema tab We recommend that you filter events in our UI if you can. It’s easier than writing code, and you can update your source or make changes to your filters without involving developers! Backfilling historical data You can backfill data by adding a timestamp to your calls. This can be helpful if you’ve just switched to Customer.io. You can only do this for destinations that accept timestamped data—most analytics tools like Mixpanel and Amplitude do. The notable destination that doesn’t support timestampped data is Google Analytics.  Leave out the timestamp if you’re tracking real-time events If you’re only tracking things as they happen, you can leave the timestamp out of your calls and we’ll timestamp requests for you. Context Context fields provide “context” for the events that you send—things like your app’s version. Our library sets some defined context fields by default, but you can override these fields or set custom context fields in a couple of different ways: Globally, for all calls At the event/call level You can set custom context fields—fields outside the ones we’ve defined. You must set custom context in the Extra field. We’ll automatically inline these properties in the serialized context structure. Global Context Global Context client, _ := analytics.NewWithConfig("h97jamjwbh", analytics.Config{ DefaultContext: &analytics.Context{ App: analytics.AppInfo{ Name: "myapp", Version: "myappversion", }, }, }) Event-level context Event-level context client.Enqueue(analytics.Identify{ UserId: "019mr8mf4r", Traits: analytics.NewTraits(). Set("friends", 42), Context: &analytics.Context{ Extra: map[string]interface{}{ "active": true, }, }, }) If you added both the global and event-level context changes above, then the identify call under Event-level context above would be serialized to: { "type": "identify", "userId": "019mr8mf4r", "traits": { "friends": 42, }, "context": { "active": true, "library": { "name": "analytics-go", "version": "3.0.0" } } } Batching Our libraries are built to support high performance environments. It’s safe to use this library on a web server that serves hundreds of requests per second. But every method you invoke does not result in an HTTP request. Instead, we queue requests in memory and then flush them in batches, which allows for more efficient operation. If batch messages do not arrive in your debugger and don’t throw errors, you may want to slow your script down. We run a message batching loop in a go-routine so if the script runs too quickly it won’t execute network calls before it exits the loop. By default, our Go source library flushes: every 20 messages (control with FlushAt) if 5 seconds has passed since the last flush (control with FlushAfter) There is a maximum of 500KB per batch request and 32KB per call. If you don’t want to batch messages, you can turn batching off by setting the flushAt option to 1. Flushing and closing the client When your application is shutting down or you need to ensure all queued events are sent, you should call the Close() method. This sends any queued events and then closes the client, preventing any further data from being sent. // Flush queued messages and close the client client.Close() --- ## Segment URL: https://docs.customer.io/integrations/data-in/connections/cdps/segment/ When you send data from Segment to Customer.io, you'll treat Customer.io as a *destination* in Segment. Use Segment's destination actions to capture events and shape data from any of your Segment [source integrations](https://segment.com/catalog/) in Customer.io, including anonymous events.  Do you have a standard Segment destination? This page helps you use Customer.io with Segment’s “destination actions” feature. If you’re using Segment’s classic integration with Customer.io, and you don’t want to upgrade, see the Segment Destination (classic) page. Unlike our standard Segment Destination integration, the Destination Actions integration supports anonymous events. Segment’s Destination Actions feature lets you filter the events that you send to Customer.io, helping you determine exactly what data you want to send to your destination. It also lets you map properties from the source event to destination calls, helping you shape information from your source to Customer.io without having to write code. If you already have a standard Segment Destination integration and want to upgrade, check out our upgrade path. Set up Segment Destination Actions If you want to take advantage of anonymous events from Segment in Customer.io, you need to use Segment’s Destination Actions feature. This framework makes it easy to map anonymous events that you capture from another source to Customer.io. In your Segment workspace, go to Destinations and click Add Destination. Click Destination Actions in the left navigation, and then select Customer.io Actions. Click Configure Customer.io Actions. If prompted, select the source that you want to connect to Customer.io. Enter your Customer.io Site ID and API Key. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. Click Configure Actions. Select the Source that you want to connect to your destination and then click Authenticate. Enter your Customer.io Site ID and API Key. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. Select how you want to set up your actions and click Create Destination. In general, you should use Quick Setup and take advantage of our default mappings, but you can also click Customized Setup to filter the events that you send into Customer.io and re-map event properties. See the section below for more information about filtering and mapping actions. Filter and Map Actions When you configure Customer.io with Destination Actions, the actions are pre-populated with default settings. You can select an action to filter the source events that you send to Customer.io and map the properties from the source event to the Customer.io action. If this is something you want to do, you should understand the information that you send in identify and track calls to Segment before you begin. To get to your Actions, go to Destinations, select your Customer.io destination, and then click Actions. Select any of the actions to filter the source event and map to the destination event. Use Set up event trigger to filter the event(s) that Segment sends to Customer.io. Use Configure action fields to re-map source properties to Customer.io. For example, if you know that a specific event type represents your leads—people you identify by email address—you may want to filter the event trigger by that event name, and then set the Person ID action field to email. This ensures that the source event represents a person that you identify by email address. Converting timestamps When you map some actions, you’ll see a Convert Timestamps setting. This setting is on by default, and converts traits containing ISO-8601 timestamps to Unix timestamps (seconds since epoch). We strongly suggest that you leave this setting enabled. While we support ISO-8601 timestamps in 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}}., you must use Unix timestamps to take advantage of timestamp conditions when segmenting your audience. Note that we only convert timestamps with millisecond precision. We won’t convert timestamps with nanosecond precision (more than 3 decimal places in the seconds field). For example, if you send an event with a purchase_time trait of 2006-01-02T18:04:07Z, we’ll automatically convert it to 1136253847. If the timestamp is not in ISO-8601 format, we cannot convert it. This prevents us from inadvertently converting values like phone numbers or IDs. We make an exception for the created_at trait, converting ISO-8601 timestamps or any values supported by JavaScript Date. Here are some examples of the kinds of timestamps we can, and can’t, convert: Original timestamp Traits except created_at created_at 25 Mar 2015 ❌ ✅ Mar 25 2015 ❌ ✅ 01/01/2019 ❌ ✅ 2019-02-01 ✅ ✅ 2007-01-02T18:04:07 ✅ ✅ 2006-01-02T18:04:07Z ✅ ✅ 2006-01-02T18:04:07+01:00 ✅ ✅ 2006-01-02T15:04:05.007 ✅ ✅ 2006-01-02T15:04:05.007Z ✅ ✅ 2006-01-02T15:04:05.007+01:00 ✅ ✅ 2025-08-13T15:45:17.451Z ✅ ✅ 2025-08-13T02:08:40.1418726Z ❌ ❌ Event tester and common errors On your destination actions page in Segment, you can go to the Event Tester tab to send test events to validate your action subscriptions. Test events will appear in your event destination as real data; most event types contain a userId, so take care not to inadvertently represent a real person with your test data. You may want to change the userID to the value of a designated “test person” in your Customer.io workspace. Use the Type option to determine the type of event you want to test. By default, the Event Tester shows the JSON tab, but you can use the Event Builder for a no-code option to build your event. In either case, the event is pre-formatted, but you can populate properties to better represent your audience. Common errors include: The root value is missing the required field url. The default Page event subscription expects page views as sent from Segment’s JavaScript snippet, which populates URLs at properties.url. You must provide this value in your test event unless you set up a subscription to capture events page views using Segment’s API. If you capture page view events using Segment’s API, which expects URLs in the name field, you can set up a new subscription on the Actions tab. Set the event trigger when the Event Type is Page and set Page URL to name. Segment did not send any requests to the destination We don’t support Segment’s Screen event type; you can’t set up a subscription using this event type. Attempting to test against this event will result in an error. Migrate to Destination Actions If you already have a Segment Destination integration, and want to take advantage of anonymous events or other mapping functions, you can migrate to Destination Actions in Segment. You’ll need to set up Destination Actions first then turn your standard destination integration off. While migrating over, you may receive duplicate events. Set up your new Destination Action. In Segment, click Destinations and select your original Customer.io Segment destination. Click the toggle to disable it. You may receive duplicate events in the time after you set up your new destination action and before you disable the old destination. In Customer.io, go to Integrations and click your legacy Segment integration. Click Remove Integration so it no longer displays as active in your workspace integrations. Identify people The Identify method corresponds to the Create or Update Person action (or any custom subscriptions you create to identify people in Customer.io). Segment’s method to identify people is very similar to adding or updating people in Customer.io. Here’s an example request: analytics.identify('userId123', { email: 'john.doe@example.com' }); When you identify a new user, the user is added to your Customer.io workspace. If the user already exists, the request updates the existing person’s attributes. If your Customer.io workspace supports both email and id as identifiers (the default setting as of June 2021), you can identify people either by id or their email trait. This provides a path to identify people who enter your system as leads (by email) and then assign them an ID in Customer.io when they become a customer or user. If you don’t have an ID, send your identify call an empty userId in the identify call and an email trait. Customer.io automatically maps the email trait to a person’s email attribute. If you have an ID, set the userId in the identify call to the person’s ID and pass email as a trait. This sets both a person’s id and email attributes in Customer.io. If you previously identified a person by email, your request updates that person and assigns them an id. Converting leads to customers If your workspace supports both id and email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., you may want to identify a lead first by email, and then assign them an id when they become a customer or user. To create a lead and then assign them an id when they become a customer or user: Identify a lead by sending an identify request with an email trait and empty userId passing their email as the userId. This creates your lead in Customer.io—a person who has an email, but does not yet have an id. analytics.identify('', { // The email address does not belong to anybody // so we create a new person email: 'person@example.com' first_name: 'person', interested_in: 'baseball' }); Later, when that person becomes a customer, pass their new id as the userId in your Identify call, and pass their email as a trait. We’ll update the lead (who you first identified by email) with the new ID—as long as this person does not already have an id value and the id you use does not already exist. analytics.identify('userId123', { // the email belongs to the lead you identified in the previous step // so we'll assign an id to this person—userId123. email: 'person@example.com', account_created: 1629224941 }); In this second request, if the ID already belongs to a person, we’ll attempt to update that person. If the ID and email already exist, you’ll receive an error. Update email and ID values that have already been set After you set a person’s email or id identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. in Customer.io, changing them can be tricky. Email: if you allow updates to email using id, you can change the email address to any value that isn’t already in use with id. Otherwise you need to pass the cio_id in the format cio_<cio_id value> to change the email address. ID: you must pass the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). in the format cio_<cio_id value> to change id values. analytics.identify('cio_<cio_id value>', { email: 'new.email@example.com', id: 'new-id-value' }); You can find a person’s cio_id value by going to their People page or by looking up a person with the customers API. Unsubscribe Users To unsubscribe a user, simply pass unsubscribed: true in your Segment identify call. Be sure the id and/or email values match the values in Customer.io. You can find these values by selecting a person on the People page, and clicking Attributes. Here is an example that unsubscribes a user: analytics.identify('userId123', { email: 'john.doe@example.com', unsubscribed: true } ) Identifying people in workspaces that only support ID In older workspaces, or workspaces using “classic” settings, you can only identify people by ID. You do not need to pass an email address unless you intend to send emails. If you intend to send emails, you must provide the email address as a trait labeled email. Segment Group Calls You can use Segment’s group call to create or relate people to an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. in Customer.io—like a company people might belong to or a flight they might have booked. To take advantage of group calls, you need to enable the Create or Update Object action. You can only use group calls with Segment’s Destination Actions feature; you cannot use it with Segment’s “Customer.io (Classic)” integration. A group call specifies an object_id representing the group that you want to create, modify, and associate people with. If you’ve sent an identify call, a group call automatically associates an object with the identified person. If you haven’t identified a person yet, we won’t process the group call until you identify the user. See Group calls and anonymous people for more information. Beyond the object_id, a group call also takes an optional object containing: object_type_id: Each id is an integer, starting at 1. If you don’t pass this key as a trait, we check for the property in the Create or Update Object mapping. If it does not exist there, we default the object_type_id to 1. Additional attributes: any other keys you pass in the object are attributes assigned to the object itself. This call does not assign attributes to a person. analytics.group("<object_id>", { name: "Initech", industry: "Technology", employees: 329, plan: "enterprise", object_type_id: 1 }); See Segment’s group specification for more information about this call.  New objects and object types If you send an object_id that doesn’t exist, we’ll create it. For unrecognized object_type_id values, we create a new one named after a type of animal! If an object_type_id is not passed as a trait, we check for the property in the Create or Update Object mapping. If it does not exist there, we default the object_type_id to 1. Group calls and anonymous people You can send group calls before you identify a person. If you don’t pass a userID in the request, and you haven’t already identified a person, we won’t process the request until you identify a person. When you identify the user, we’ll process the group call and relate it to the identified person. If your group call references a new object_id or object_type_id, we won’t create the new object or object type until you identify the associated person. Group calls and object attributes You can create and update object attributes with the traits you send in your group call. Segment’s group call requires either a userId or an anonymousId, however. That means in order to update an object attribute through Segment, an object must also be related to a user in the same call. Passing null or an empty string for an attribute removes it from an object.  You cannot use relationship as an object attribute name. relationship is reserved for referencing relationship attributes in Liquid. Group calls and relationship attributes Through our Track APIs, you can manage relationship attributes. You can do the same thing through Segment’s group calls, by placing relationship attributes in a traits.relationshipAttributes object. For example, if you relate people to an online class, you might use relationship attributes to differentiate between teachers and students. If you used Segment’s JavaScript library, you might send relationship attributes like this: analytics.group(groupId, { name: "Acme, Inc.", industry: "anvils, etc" // relationship attributes go here! relationshipAttributes: { buyer_type: "coyote", frequent_buyer: true, total_spend: 1000000 } }) Segment Suppression and Deletion To help comply with GDPR and CCPA regulations, Segment supports their own Suppression and Deletion tools. These tools are separate from Customer.io’s methods to delete and suppress people. In Segment, suppressed users are blocked across all sources; any message sent to Segment via a suppressed userId is blocked at their API (with the exception of device-mode destinations). Suppressions are not synchronized between Segment and Customer.io. However when you suppress and delete a customer via Segment’s UI, that deletion is passed along to Customer.io to ensure that the person you suppressed and deleted in Segment is removed in Customer.io. Anonymous Events Segment generates an anonymousId trait for people that you haven’t identified with a userId. Events generated by unidentified people are passed to Customer.io as “anonymous events”—events attributed to this anonymousId. If the anonymous event merge feature is turned on in your Customer.io workspace (the default setting for new workspaces), Customer.io automatically merges anonymous events with people when you identify them. This all happens automatically with the Segment Destination Actions integration. Add and Update Devices A standard identify call can add and update mobile devices. This might happen if a user logs into your service from inside your mobile app. In this case, you’re both identifying a person and associating a device with them. However, there are also default Destination Actions to add, update, and delete devices based on track events. These actions are filtered by the event value. Application Installed events add or update devices. Application Opened events add or update devices. Application Uninstalled events remove devices. Here’s an example Application Installed event. { "action": "track", "userId": "019mr8mf4r", "event": "Application Installed", "properties": { "version": "1.2.3", "build": 1234 }, "context": { "device": { "token": "ff15bc0c20c4aa6cd50854ff165fd265c838e5405bfeb9571066395b8c9da449", "type": "ios" } } } --- ## Segment data-in (classic) URL: https://docs.customer.io/integrations/data-in/connections/cdps/segment-destination-classic/ This page describes Segment's classic integration with Customer.io as a destination. This integration is in maintenance mode. In general, we suggest that you use the newer [Destination Actions](/integrations/data-in/connections/cdps/segment-destination) integration to pipe data from any one of Segment’s hundreds of Source [integrations](https://segment.com/catalog/) into your Customer.io workspace. You can find more details in [Segment's documentation](https://segment.com/docs/connections/destinations/catalog/customer-io/). Set up a Segment Destination integration  This integration is in maintenance mode Segment no longer actively maintains this integration. If you’re just getting started, we suggest that you use the newer Segment Destination Actions integration integration, which supports anonymous events and lets you filter the events that you send to Customer.io. Go to Settings > Integrations and click the Segment Destination integration. Click Configure Segment. Pick the Workspace and Source you want to associate your integration with, and then click Allow. You can also set up this integration from the Segment interface using the API credentials provided on the Customer.io Segment Destination page. Identifying people through segment If you’re not familiar with the Segment API, take a look at the Identify method. It’s very similar to Adding or Updating people in Customer.io Here’s an example request: analytics.identify('userId123', { email: 'john.doe@example.com' }); When you identify a new user, the user is added to your Customer.io workspace. If the user already exists, the request updates the existing person’s attributes. If your Customer.io workspace supports both email and id as identifiers (the default setting as of June 2021), you can identify people either by id or their email trait. This provides a path to identify people who enter your system as leads (by email) and then assign them an ID in Customer.io when they become a customer or user. If you don’t have an ID, send your identify call an empty userId in the identify call and an email trait. Customer.io automatically maps the email trait to a person’s email attribute. If you have an ID, set the userId in the identify call to the person’s ID and pass email as a trait. This sets both a person’s id and email attributes in Customer.io. If you previously identified a person by email, your request updates that person and assigns them an id. Converting leads to customers If your workspace supports both id and email identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., you may want to identify a lead first by email, and then assign them an id when they become a customer or user. To create a lead and then assign them an id when they become a customer or user: Identify a lead by sending an identify request with an email trait and empty userId passing their email as the userId. This creates your lead in Customer.io—a person who has an email, but does not yet have an id. analytics.identify('', { // The email address does not belong to anybody // so we create a new person email: 'person@example.com' first_name: 'person', interested_in: 'baseball' }); Later, when that person becomes a customer, pass their new id as the userId in your Identify call, and pass their email as a trait. We’ll update the lead (who you first identified by email) with the new ID—as long as this person does not already have an id value and the id you use does not already exist. analytics.identify('userId123', { // the email belongs to the lead you identified in the previous step // so we'll assign an id to this person—userId123. email: 'person@example.com', account_created: 1629224941 }); In this second request, if the ID already belongs to a person, we’ll attempt to update that person. If the ID and email already exist, you’ll receive an error. Update email and ID values that have already been set After you set a person’s email or id identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. in Customer.io, changing them can be tricky. Email: if you allow updates to email using id, you can change the email address to any value that isn’t already in use with id. Otherwise you need to pass the cio_id in the format cio_<cio_id value> to change the email address. ID: you must pass the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). in the format cio_<cio_id value> to change id values. analytics.identify('cio_<cio_id value>', { email: 'new.email@example.com', id: 'new-id-value' }); You can find a person’s cio_id value by going to their People page or by looking up a person with the customers API. Unsubscribing Users To unsubscribe a user, simply pass unsubscribed: true in your Segment identify call. Be sure the id and/or email values match the values in Customer.io. You can find these values by selecting a person on the People page, and clicking Attributes. Here is an example that unsubscribes a user: analytics.identify('userId123', { email: 'john.doe@example.com', unsubscribed: true } ) Identifying people in workspaces that only support ID In older workspaces, or workspaces using “classic” settings, you can only identify people by ID. You do not need to pass an email address unless you intend to send emails. If you intend to send emails, you must provide the email address as a trait labeled email. Segment Suppression and Deletion To help comply with GDPR and CCPA regulations, Segment supports a set of Suppression and Deletion tools. These tools are separate from Customer.io’s own suppression and deletion tools. In Segment, suppressed users are blocked across all sources; any message sent to Segment via a suppressed userId is blocked at their API (with the exception of device-mode destinations). Suppressions are not synchronized between Segment and Customer.io, however when you suppress and delete a customer via Segment’s UI, that deletion is passed along to Customer.io to ensure that the suppressed and deleted person is removed. Segment Alias Call Customer.io does not support alias calls or anonymous events from Segment. Segment uses ananonymousId trait to identify users, and it’s not possible to connect anonymous user data with a newly created person in Customer.io. As a result, you could end up with duplicate profiles. You might have come across this issue if You’re integrated via Segment and want to change ids. Rather than using anonymous events, we suggest that you identify people by email, and then assign them an id as they become customers. See identifying users through segment above. If you send an identify call with some attributes attached, we look at the identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. in the call. If we don’t recognize any of the identifiers in the call, we create a new user with those attributes. If any of the identifiers in the call already exist, we update only the attributes that are different compared to the previous call. We do not update or change the id. So if the id is new to Customer.io, we create a new (duplicate) user. Resolve existing duplicates  You can now resolve duplicates without deleting people You can merge two people together on the People page or through the API. This process makes it easy to retain information while resolving duplicate profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}.. If you have duplicate people in your system, you can resolve profiles through the following process: Export a CSV of your customers from the People page by clicking on the Export to CSV button. Identify the duplicates. We recommend keeping the first instance of each—the one with an oldest created_at timestamp. Create a new CSV file with all of the people (by id or email) you want to delete. Delete those users with our API. Prevent new users from causing duplicates For new users, we strongly suggest you identify people by email if they do not yet have an ID. Pass email as the userId in Segment’s identify call. Update a person’s ID using an identify call after they sign up for your service. Anonymous Events from Segment If you want to send anonymous events from Segment, you should consider upgrading to Segment’s new Destination Actions feature. We have instructions to set up Destination Actions with Customer.io here. If you can’t upgrade to Segment’s Destination Actions feature, and you need to message a customer before they’ve signed up, you should record an event by email and then send an invite campaign to people in Customer.io who have an email address but do not have an ID. Some useful links The full list of Segment methods are supported with Customer.io. Segment have outlined integrations that support the Alias method. As always, if you need any help with this process or have any questions, please let us know! --- ## Rudderstack URL: https://docs.customer.io/integrations/data-in/connections/cdps/rudderstack-in/ Set up Customer.io as a Rudderstack Destination, connecting data from any of Rudderstack's source integrations to your workspace.  You can find the open-source transformer code for this destination in Rudderstack’s GitHub repository. How it works When you set up Customer.io as a Rudderstack Destination, Rudderstack adds, updates, and sends events representing people to Customer.io from one or more sources. Rudderstack supports the same set of calls as our Pipelines API, with similar payload structures. This page provides some examples of source calls you can make to forward data to Customer.io using Rudderstack’s JavaScript snippet. You’ll see Rudderstack’s complete Customer.io destination specifications in their documentation. For all requests, Rudderstack identifies a person by userId or anonymousId, where a userId matches a person’s id in Customer.io. Before you begin Make sure that you’re using Rudderstack in a mode that supports your Customer.io integration. In Cloud mode, Rudderstack’s SDK sends events directly to Rudderstack, where they transform data and route it to Customer.io. In Device mode, you use a client-specific library in your app to forward information directly to Customer.io (without it going to Rudderstack first). Before you configure this integration, consider whether or not you can support Customer.io in device or cloud mode: Connection Web Mobile Server Device mode ✅ ❌ ❌ Cloud mode ✅ ✅ ✅ To learn more about the differences between Rudderstack’s Cloud and Device modes check out their connection modes guide. Set up a Rudderstack integration You’ll set up this integration almost entirely within Rudderstack. As a part of this integration, you’ll need to provide Rudderstack with your Track API credentials: your Site ID and API Key. Go to your RudderStack dashboard and click Add Destination. Pick Customer.io from the list of destinations, and provide a name for your new destination. If you use Rudderstack to connect with more than one workspace, you might want to name your destination for your workspace in Customer.io. Select a Source to connect with your destination and click Next. Enter the Site ID and API Key for your Customer.io workspace. You can find your Customer.io Site ID and API Key credentials under Integrations > Customer.io API. Enter the name of the event you want to send to Customer.io when someone registers a device. (Optional) Enable Device mode for the web SDK if you want to send events directly from your client to Customer.io. However, you can only use Device mode with your Web implementation. Enter OneTrust category name that maps OneTrust consent settings to RudderStack’s consent purposes. Click Next. Rudderstack is now set up to send events and calls into Customer.io Identify The identify call sends the event data from Rudderstack to Customer.io with the properties that you pass as the RudderStack traits. For more information about identify calls, see RudderStack’s API specification. RudderStack sends the createdAt field as created_at in Customer.io to register user sign up time in the dashboard. If you don’t send createdAt, created_at is not sent in to Customer.io. If your request doesn’t include userId, Rudderstack substitutes an anonymousId. rudderanalytics.identify("my-userID", { name: "Tintin", city: "Brussels", country: "Belgium", email: "tintin@herge.com" }); // created at is not present in this request so // Rudderstack appends the request with the current timestamp. Group The group call lets you manipulate objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and relationships in Customer.io. If you have multiple object types in your system, you can specify the object type with objectTypeId. This defaults to 1 (the first object type in your system) if you don’t include it in your call. The call hinges on the action property, which has four accepted values: identify: adds or updates an object. If you’re using the Rudderstack JavaScript snippet, this action also relates the currently-identified user to the group. delete: removes an object. add_relationships: sets a relationship between the object and a user. delete_relationships: removes a relationship between the object and a user. rudderanalytics.group("objectId", { objectTypeId: 2, action: "identify", traits: { classTitle: "Customer.io 101", teacher: "docs@customer.io" } }) Page The Customer.io JavaScript snippet captures page views automatically. However, if you want to pass additional properties, you can call rudderanalytics.page(). This sends the page event with additional properties that you want to pass in your page event. // "home" is the name of the page. rudderanalytics.page("home", { path: "path", url: "url", title: "title", search: "search", referrer: "referrer", }); Screen Rudderstack’s screen call records screen views for users in your app. If you turned on screen views in your Rudderstack app implementation from the iOS or Android SDKs, these events register as Viewed <screen name> Screen in the Activities tab for the user in Rudderstack. Rudderstack forwards the properties in the screen call to Customer.io dashboard as it is. Here is a sample screen call using RudderStack iOS SDK: [[RudderClient sharedInstance] screen:@"Main" properties:@{@"prop_key" : @"prop_value"}]; Track The rudderanalytics.track() call passes event properties to Customer.io. For more information about Rudderstack’s track call, see their API specification. rudderanalytics.track("Track me", { category: "category", label: "label", value: "value", }); Anonymous events cannot have a name over 100 bytes. Rudderstack trims event names over 100 bytes for you. Register device tokens from Rudderstack Rudderstack registers a deviceToken in Customer.io on the following application lifecycle events: Application Installed Application Opened Application Unistalled To send device tokens to Customer.io, you need to enable trackApplicationLifecycleEvents in your Rudderstack SDK implementation and register users’ deviceToken after initializing the SDK. Example for registering deviceToken is as follows: iOS iOS [[[RudderClient sharedInstance] getContext] putDeviceToken:[self getDeviceToken]]; Android Android rudderClient!!.rudderContext.putDeviceToken(getDeviceToken()) --- ## mParticle (Legacy) URL: https://docs.customer.io/integrations/data-in/connections/cdps/integrating-with-mparticle/ mParticle is a customer data platform that allows you to unify your customer data and connect it anywhere to improve marketing performance, enhance analytics, and transform the customer experience. With mParticle you can easily connect Customer.io to your data stream to begin receiving real time event and audience data in Customer.io. Send Events to Customer.io Send event data to Customer.io by setting up Customer.io as an Event Output in mParticle. From the mParticle activity overview screen select Setup > Outputs > Event > Add Event Output and select or search for Customer.io from the dropdown list. Next navigate to the Customer.io workspace you’d like to send your data to. From the main dashboard select > Integrations > Customer.io API (Settings). Here you will find your Site ID and API Key. Next enter your API Key and Site ID keys into mParticles event configuration, name the configuration, and select the settings relevant to you. Success! –– You have connected mParticle to Customer.io for Events Send Audience data to Customer.io Send audience data to Customer.io by setting up Customer.io as an Audience Output in mParticle. Start by navigating to the mParticle activity overview screen. Click Setup > Outputs > Audience > Add Audience Output and select or search for Customer.io from the dropdown list. Next navigate to the Customer.io workspace you’d like to send your data to. From the main dashboard select > Integrations > Customer.io API (Settings). Here you will find your Site ID and API Key Next enter your API Key and Site ID keys into mParticles audience configuration, name the configuration, and select the settings relevant to you.  Create One user Attribute per Segment: If enabled, mParticle will forward membership information for each segment as a seperate user attribute. For example, if you’re forwarding a segment named “New Users” mParticle will forward membership information for this segment in a user attribute called “In New Users, with a value of “true” or “false”. If disabled, mParticle will forward a single user attribute called “Segment Membership”, and it’s value will be a comma-separated list of mParticle segment IDs that the user is a member of, wrapped in single quotes. If you’re unsure what to select here we recommend enabling this feature. Success! – You have connected mParticle to Customer.io for Audience Send message event data to mParticle By setting up Customer.io as an mParticle Feed, you can pipe messaging activity out of Customer.io and into any one of mParticle’s hundreds of Output integrations. Data will be sent to mParticle as a custom event. Setup Start by creating a new Input Feed in mParticle. Click Setup > Inputs > Feeds > Add Feed Input and select or search for Customer.io from the dropdown list. In the Feed Configuration modal, name your configuration and click Save to get your Server Token and Secret. You’ll need these to configure the integration in Customer.io. In Customer.io go to Integrations and click the mParticle Source card. Enter your mParticle Server Key and Secret and click Connect to test the connection. Select the events that you want to send to mParticle, enable the integration, and save your changes to start sending events from Customer.io to mParticle.  Send only the first time the event occurs: This setting tells Customer.io to forward messaging events the first time they occur, and only the first time they occur. We won’t send subsequent events, even if a customer opens or clicks on a message multiple times. If you’re unsure what to select here we recommend that you select this option. Events The following events are available in the feed: Name Description 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 conversion criteria attributed to an email email_bounced The delivery provider was unable to deliver the email email_spammed An email was marked as spam by the recipient email_failed An email couldn’t be sent to the delivery provider push_sent A push notification was sent from Customer.io to the delivery provider push_opened The app on a person’s device reported the push notification was opened push_clicked A tracked link in a push notification has been clicked push_converted A person matched conversion criteria attributed to a push notification push_bounced The delivery provider reported at least one invalid device token push_failed A push notification couldn’t be sent to the delivery provider 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 conversion criteria 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 If you have a specific request for an event not listed here that you would like to be notified of, please let us know at win@customer.io. Event 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 full message details, including content, via the Campaign 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_id If the message was part of a newsletter split test, this is the ID of the split test variation. delivery_id The unique ID of the delivery record associated with the message. 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. newsletter_id If applicable, the ID of the Newsletter that generated the message. It can be used to retrieve full message details, including content, 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. source_message_id The unique ID of the event being sent. This can be useful for deduplicating purposes. timestamp_unixtime_ms The timestamp at which the event being reported took place. Additional Documentation mParticle Event Documentation mParticle Audience Documentation mParticle Feed Documentation --- ## Using Zapier with the Track API URL: https://docs.customer.io/integrations/data-in/connections/webhooks/zapier-legacy-api/ [Zapier](https://zapier.com/apps/customerio/integrations) helps you connect Customer.io to hundreds of other web services. You can set up automated connections called Zaps in minutes and without writing code, to automate your day-to-day tasks and build workflows between apps that otherwise wouldn't be possible. Each Zap has one app as the Trigger, where your information comes from and which causes one or more Actions in other apps, where your data gets sent automatically. What Customer.io Actions and Triggers are Supported? Supported Actions Create or Update Customer - Creates a new customer. If the customer already exists, it will be updated. Create Event - Creates an event for a specific customer. Create Anonymous Event - Creates an event for an anonymous profile. Supported Triggers We currently only support Actions with the Customer.io Zapier app, we’d love to hear your use-cases for triggering Zaps from Customer.io to help us evaluate the most valuable Triggers to build. Ingredients Customer.io Track API Credentials: Site ID and API Key; make sure you’re using credentials from the workspace you want to integrate with. Zapier account Important Notes on Integrating Customer.io People should have unique IDs Each person in Customer.io is identified by a unique ID attribute. The ID value is a required field when configuring Customer.io Zap actions and most often this is the same ID that you use in your database as the primary key for your users. IDs cannot be changed later. For more information on planning your data integration with Customer.io check out our Getting Started: Integration Planning documentation. How do I connect Customer.io to Zapier? Log in to your Zapier account or create a new account. Navigate to “Connected Accounts” from the top menu bar. Now click on “Connect new account” and search for “Customer.io” Connect your Customer.io account to Zapier using your Site ID and API Key which are found in the Integrations area of the left-hand navigation: Once that’s done you can start creating an automation! Use a pre-made Zap or create your own with the Zap Editor. Creating a Zap requires no coding knowledge and you’ll be walked step-by-step through the setup. Need inspiration? See everything that’s possible with Customer.io and Zapier. --- ## Zoho integration URL: https://docs.customer.io/integrations/data-in/connections/webhooks/integrating-with-zoho-crm/ Zoho is a clouds suite of tools for business, including [a CRM](https://www.zoho.com/crm/) and [Zoho Flow](https://www.zoho.com/flow/) for workflow automation. Using Zoho Flow, you can connect your Zoho CRM data to Customer.io and start messaging your users in minutes. Configuring Zoho CRM in Zoho Flow To start, create a new flow from the My Flows page in Zoho Flow From there you’ll be asked to name and describe your new flow, sending you to the Zoho Flow Builder. Select ‘App’ as the trigger for your flow. Select Zoho CRM as your App Trigger. Here you are given several option for what action will trigger this workflow. You can find full descriptions for each option in Zoho Flow’s documentation. For this example we’ll use New Lead.  Realtime v. Polling You can use either Realtime or Polling Zoho CRM triggers to send data to Customer.io depending on your use case. See Zoho Flow’s documentation for more information on the difference between these two kinds of triggers. You’ll then be prompted to connect Zoho Flow to your Zoho CRM account by creating a new connections. To save you time later, allow the connection to to execute all triggers and actions. When you click to authorize, you’ll be brought to a new screen where you’ll allow Zoho Flow to manage and edit data in CRM. Once you’ve accepted, you can click Done - you’ve connected Zoho CRM to Zoho Flow and set up the trigger for your workflow. --- ## Hubspot integration URL: https://docs.customer.io/integrations/data-in/connections/webhooks/integrating-with-hubspot/ You can send data from Hubspot to Customer.io by setting up webhook campaigns in your workspace or by using Zapier. This article specifically covers how to create and update contacts from Hubspot in Customer.io.  This article covers sending data from Hubspot to Customer.io. Looking to send data to Hubspot? Use our Hubspot destination! If you need a way to sync Hubspot with Customer.io, this article is for you! While we don’t have an out-of-the-box integration for sending data from Hubspot to Customer.io, you can: create a webhook campaign to retrieve this data or use Zapier to connect Hubspot to Customer.io or check out our partners to see if one of their integrations could suit your needs Some other options include exporting your Hubspot contacts and importing them to Customer.io. You can also create a custom integration and programmatically add/update people via our Track API. Webhook campaigns Prerequisites Webhook campaigns are included in every Customer.io plan. However, not all Hubspot plans support webhooks or Hubspot workflows, both of which you’ll need to successfully create this campaign. Check out Use webhooks with Hubspot workflows to see if your plan supports them. To create a webhook campaign in Customer.io, you need to be an Account Admin, Workspace Admin, or Author with Full Access. Send data from Hubspot with webhook campaigns In this recipe, we’ll show you how to create and update contacts from Hubspot in Customer.io. Make sure you’ve read the prerequisites before proceeding. Check your Hubspot plan. Does it include webhooks and workflows? If yes, proceed ahead! If not, use one of the other options above. Go to your Customer.io Account Settings to check how you identify people. These are your options when mapping contacts in Hubspot to people in Customer.io. If you don’t have account-level access, go to your Workspace Settings to check. Create a webhook campaign. In step 3, add the webhook URL to a Hubspot workflow. In step 5, drag a Create or Update Person action into your campaign workflow in Customer.io. Make sure you map your people identifier in Hubspot to a Customer.io identifier supported by your workspace (see step 2 above). Review your campaign and click Start Campaign. Keep in mind, we count people towards billing. Review our best practices for keeping a clean workspace with only the people profiles you need month-to-month. Zapier Prerequisites You can install the Zapier app, free-of-charge, on any Hubspot plan. However, you may need a paid Zapier plan depending on the volume of data you need to send to Customer.io. Review Zapier’s concept of tasks to help you understand how usage is calculated in Zapier. To integrate Zapier with Customer.io, you’ll need to be an Account Admin or Workspace Admin in Customer.io. Send data from Hubspot with Zapier This recipe shows how to create and update contacts from Hubspot in Customer.io. Make sure you’ve read the prerequisites before proceeding. Install Zapier in Hubspot, then sign in or create a Zapier account. Create a zap. Add a descriptive name at the top so you can distinguish it from other zaps. Set your trigger as “Hubspot.” Choose the event “Contact recently created or updated” or another event that meets your needs. Set your action as “Customer.io.” Add your Track API ID and Key from Workspace Settings in Customer.io. Make sure you copy the credentials from the workspace you want to integrate with. Choose the event “Create or update a person,” then map your fields to Customer.io. Click Publish when you’re ready to take the zap live! Keep in mind, Zapier has different pricing tiers for the number of tasks in your account. Note, we count people towards billing. Review our best practices for keeping a clean workspace with only the people profiles you need month-to-month. Moving forward, you will see “Zapier” in activity logs when the change came from your integration. --- ## Getting Started URL: https://docs.customer.io/integrations/data-out/getting-started/ Customer.io has *data out* integrations that let you send data from Customer.io or other sources of data to a destination service. How it works Data-out integrations let you send data to platforms outside of Customer.io, like analytics platforms, CRMs, support tools, advertising platforms, messaging platforms, and more. Most of our data-out integrations support all your sources of data. In these cases, data travels into Customer.io and then to your destination. Some data-out integrations, clearly marked with Web in our integration directory, only support data our JavaScript client. In these cases, data travels directly from your website to the destination, bypassing Customer.io. We also have data warehouse integrations. Unlike other integrations, where data travels through Customer.io to its destination in real-time, data warehouse integrations receive data in batches on a regular interval. See our data warehouse integrations for more information. flowchart LR subgraph Data sources a(Your Website) b(Server-side data) end c((Customer.io Data Pipelines)) subgraph Data destinations d(Your CRM) e(Your analytics platform) f(Customer.io Journeys) end a-->|JS integration|c b-->|Go, Python, or Node integration|c c-->|Send website and server-side data|d c-->|Send website source only|e c-->|Send website and server-side data|f linkStyle 0,3 stroke-width:2px,fill:none,stroke:#AF64FF linkStyle 1 stroke-width:2px,fill:none,stroke:#00ECBB linkStyle 2,4 stroke-width:2px,fill:none,stroke:#0597AD Set up a data-out integration The steps involved in configuring your integration change based on the service or destination you want to send data to. See an individual integrations to learn more about the individual steps, and the information you’ll need, to setup to a specific outbound integration. Go to Integrations and click Add Integration. Select the Data Out integration you want to add. (Optional) Select the incoming data sources that you want to connect to your new integration. You can always connect data sources later. Configure your integration and click Continue. We’ve provided an example of the Customer.io workspace destination, but yours may contain different fields based on the integration you selected. Example Customer.io Journeys destination settings Click Enable Integration. Now your integration is set up and will start accepting data from your sources. If you want to tailor the data that your sources send to destinations, go to your destination’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. tab. See Actions for more information. You can check the Actions tab to see data as it comes in. This helps you make sure that your integration is set up correctly. Actions Your integration’s Actions tab shows recent data sent to the integration. You can find specific actions and look at the data as it’s sent to your integration. You might use this to troubleshoot specific source events. When you set up a integration, you can check Actions to change the way we map source data to your integration—or just to better understand what data we’ll send to your integration by default.  Be careful when editing actions for your workspace The Customer.io integration is how data gets into Customer.io—the people, events, and other data that you’ll use to send messages to your audience. It’s already configured with actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to support incoming data. You can edit these actions if you want to change how we handle incoming data, but take care not to inadvertently break integrations with Customer.io. Mapping source calls to your integration Integrations that send data out of Customer.io have Actions. Actions are how we map data from sources to your integration—the “trigger” for each request, and the fields we map to payloads your integration expects. In most cases, you’ll want to stick with the defaults. But, you might disable actions that you don’t care about in your integration. Or you might click and select Edit to change the way we map data to an action—this can help you better represent your model data in your integration if our default mappings don’t quite match your use case. See our actions guide for more information. API Call tester On the API Call Tester tab, you can send calls to your destination to make sure that your integration works the way you expect. It may help to run test calls before you connect data sources to your integration—so it’s easier to find your tests. API calls are formatted to fit our Data Pipelines API. This gives you an opportunity to see how a call from your source(s) maps to your integration. Test calls are populated with test data. Use the dropdown to select the kind of call you want to test.  Need help troubleshooting errors? If you encounter errors when testing API calls, you can ask our agent for help. The agent can help you understand error messages and troubleshoot data formatting issues to ensure you’re sending data in the correct format to your destination. Overview The Overview for your integration tab shows action volume and how many actions were completed or failed over a time frame. The overview helps you understand if your integration works as you expect, and can help you spot performance issues—failed actions or high latency. Choosing a connection mode You may see data-out integrations with a Web option. These integrations: Only support our JavaScript client. Send data directly to your integration, bypassing Customer.io’s servers. Where possible, we suggest that you use integrations in their normal, non-web iterations, but there are some reasons you might use a web integration. Sometimes, the web implementation supports operations that the integration’s normal, non-web implementation doesn’t. Source Type Integration Web Integration Website (JavaScript) Server (NodeJS, Go, Python) Mobile (iOS, Android, React Native, Flutter) Data Warehouse Customer.io Standard data-out integrations Under normal circumstances, your datasource sends data to Customer.io. We then transform and forward data to your downstream integrations. This keeps the size of injected scripts in your source integrations small and load times fast. Most importantly, this mode ensures that we keep a record of data sent to your integration, which can help you audit and troubleshoot issues with your integration. Web integrations Web integrations expect to receive data directly from your website without going through Customer.io’s servers. Often times, we have this mode to support services that don’t have public APIs for certain kinds of operations, and require us to load their SDK directly on your website to send data to them. Since Web integrations don’t send data through Customer.io’s servers, you won’t see a record of actions sent to your integration. This can make it tricky to debug web integrations. If you need to debug or troubleshoot these integrations, you can try observing network calls in your browser console. If you need Customer.io’s help to troubleshoot web integrations, you may need to provide us with access to a staging environment where we can observe your integration. In general, you probably only need to use web integrations if: The integration requires it. Some integrations only support web mode. You rely on integration features that require direct access to a service. For example, if you use Adobe Target and you want to load Adobe Target’s SDK (at.js) to take advantage of their A/B testing and personalization features, you’ll need to use the web integration. You need to minimize latency between your site and your integration. For example, if you use a live chat integration, you might want to use the web version of the integration to ensure that your events pass to your live chat service as quickly as possible. Retries: how we handle outages If your integration suffers an outage, we’ll retry your data to the integration. We retry up to 14 times with an exponentially increasing delay between retries. The maximum retry window is about three hours. We don’t retry when we get a definitive error, like a 400 or 403. Rather, we retry in the following situations: 429s: we’ve exceeded the integration API’s rate limit 5xx: the integration reported internal error Network errors: we were unable to establish network connection with the integration Other errors: errors that don’t produce status codes or are otherwise unrecognized If you encounter errors and change your integration’s settings, or action settings for the integration, we’ll use the correct settings on the next retry. This makes it easy to fix problematic settings in your integration’s setup. --- ## Add a data-out integration URL: https://docs.customer.io/integrations/data-out/add-destination/ After you've added some data-in integrations, you're ready to forward your data to places where you store or act on your data. We call these *data out*, *outgoing*, or *outbound* integrations. While each data-out integration is different, there are some similar settings for each that you'll use when you set things up with Customer.io. How it works An outgoing integration is the place you want to send your data to—where you store data, use it to gather analytics, to power marketing automation (like Customer.io Journeys), and so on. You can send data from any number of incoming integrations to and outgoing integration, or you can do simple one-to-one mapping—whatever you need. However, in most cases, you’ll find that you typically want to aggregate data from multiple data sources to a single outgoing integration—so you can act on a holistic understanding of your audience. Set up a data-out integration This is a very generic process. The steps involved in configuring your integration will change based on the specific service you integrate with. See an individual integration to learn more about the individual steps, and the information you’ll need, to set up your integration. Go to Integrations and click Add Integration. Select the Data Out integration you want to add. (Optional) Select the incoming data sources that you want to connect to your new integration. You can always connect data sources later. Configure your integration and click Continue. We’ve provided an example of the Customer.io workspace destination, but yours may contain different fields based on the integration you selected. Example Customer.io Journeys destination settings Click Enable Integration. Now your integration is set up and will start accepting data from your sources. If you want to tailor the data that your sources send to destinations, go to your destination’s actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. tab. See Actions for more information. Authenticating with a data-out integration When you add a outgoing integration, you need to provide us with a credentials to authenticate with the service you want to send data to. Most integrations use an API token or credentials that you can get from their web app. If you can’t get credentials from the service you want to connect to Customer.io, you won’t be able to finish setting up your integration. The documentation for each integration includes information about what you need, and how to find it. Why can’t I connect incoming data to my outgoing integration? Some outgoing integrations, like our Facebook Lead Ads integration, only accept data from our client-side JavaScript library. We’ve labeled these integrations as . That’s because these integrations require an SDK that communicates directly with an outbound service, and we can only load that SDK with the plugin architecture supported by our JavaScript library. When someone loads a page containing the client-side JavaScript library, we load associated SDKs for these integrations as “plugins” and send data directly to these integrations—without sending the data to Customer.io first! If you need to troubleshoot traffic to one of these integrations, you’ll need to check activity in your Network tab to see traffic to the specific outbound integration. Do I need to update data-in integrations when I add a data-out integration? No. You don’t need to update anything. If you use our JavaScript client library in your website, it’ll automatically handle associated configuration changes when you connect it to an outgoing integration. Data comes into Customer.io and we’ll automatically route it to your integrations. --- ## Actions URL: https://docs.customer.io/integrations/data-out/actions/ Actions determine how we map incoming data to your outgoing integration. Here’s a quick demonstration showing how actions work and how easy it is to change data structures to fit your needs. How it works Customer.io knows how to use the data you send in identify calls and track events. But your outbound integrations don’t all work the same way Customer.io does! So we have to reshape the data you send to Customer.io to fit the kinds of requests and data your outgoing integration expects. That’s what an action does: it determines when we send data to your integration and how we map data in Customer.io to your integration. The when is what we call a trigger. You’ll see the trigger on your integration’s Actions page. This is the formula that tells us what incoming data result in a call to your integration. The how is the Data Structure. Your integration expects data in a specific shape, and the data structure lets you format your data to fit your outgoing integration.  In most cases, our default actions are all you’ll need We’ve set up actions to support the majority of use cases and expected data structures. You’ll only want to change these if you aren’t using the out-of-the-box functionality with your integration. flowchart LR a(incoming data)-->b{Does it match a trigger} b-->|yes|c(Map data to integration) b-.->|no|d(Do not forward data to integration) How many actions should I have? The Type determines the kinds of things your integration can do. In general, you won’t have more actions than you have types! By default, when you set up a new data-out integration, you’ll see an action for every available Type. Action types An action Type is the thing that you want to do in your outgoing integration. For example, you’ll notice that the top type in our Customer.io destination is the Create or Update Person action. This is a fundamental action in Customer.io Journeys and maps nicely to an Identify call. You can add/update and delete devices to, but these don’t map neatly to incoming data, so we look for events called Application Installed and Uninstalled! Because different data-out integrations serve different purposes you’ll see different actions your different integrations! Triggers and Filters A Trigger determines when we send an action of the specified Type. It’s governed by conditions in the Filter field when you edit an action. You can use our interface to set up triggers, but we also offer a Filter Query Language (FQL) that you can use to write triggers more precisely. There are two typical filter fields: type represents the method or kind of incoming data: identify, track, page, screen, group, and alias. event represents the name field from track calls. Unlike other calls, track calls simply represent custom events, and the event name tells us what kind of event a person performed. You can also filter based on other fields, but we’d recommend that you not get too granular with filters or you could inadvertently prevent data from reaching your destination! Triggers on the Actions Tab Filters within an Action Data Structure The Data Structure for each action determines the way we map incoming data to your outbound integration. Data structures use JSON notation for variables—but you don’t necessarily need to know JSON notation to change values. When you opt to change a mapping on the left, we’ll give you a drop-down of available Variables that you can select from incoming payloads. In most cases, if you want to manipulate values, you’ll want to do it as a part of your data-in integrations—in your code. But we offer a couple of functions that can make things a little easier on you. Read more about the case and coalesce functions below.  Pick variables that match your incoming data The list of available variables covers all possible incoming data. It isn’t limited to the type or event in your filter. If your action is based on an incoming identify event, and you try to map a field to $.event, that field will always be null, because identify calls don’t include an event! Mapping to traits and event properties While we know that incoming identify calls contain traits and incoming track calls likely contain properties, we don’t know what those properties are or might be. If you want to map to a specific trait or event property, you’ll need to provide it yourself—under traits.<trait-name> or properties.<property-name> respectively. Whenever you want to add these properties, you’ll click a field and add your variable (like traits.first_name) to the Variables box. Don’t just type your variable in the available field, or we’ll treat it as a static value! Make sure that your requests reliably provide the trait or event property in your incoming data or the property will be empty when we send data to your integration! Troubleshooting constant text instead of variables If you notice that your action sends literal strings like $.customer.first_name or $.properties.email instead of the actual values, this means you’ve typed the variable path directly instead of using the Variables drop-down. Click the field where you want to add a variable. Click the Variables drop-down. Type in your variable name and either select it from the list (e.g., customer.first_name) or click Use as a variable.  Get help with action errors If you encounter errors when testing actions or need help understanding why data isn’t mapping correctly, you can ask our agent for help. The agent can help you troubleshoot data mapping issues and ensure your actions are configured to send data in the correct format to your destination. The case function The case function is simple: it converts a value’s case to lower or upper. You might use this function to enforce uniform cases when you send data out of Customer.io. For example, you might want to store your users’ names in lowercase for uniformity. The case function uses the format case(dataInKey, "lower/upper"). case(traits.first_name, "lower") The coalesce function The coalesce function picks the first non-null value in a list of possible values. That’s a fancy way of saying that it uses the first non-empty value that it finds in the incoming payload. The coalesce function uses the format coalesce(firstkey, secondkey). Only two arguments are supported at the moment. For example, Customer.io lets you identify people by id or email address and we actually use coalesce($.userId, $.traits.email). So, if your identify request has a userId, we’ll map that to the person’s ID. If it doesn’t, we’ll map the email trait to the person’s ID. If you use coalesce, and none of the keys are populated, the trait and the corresponding mapping will be null (empty). flowchart LR a("coalesce(userId, traits.email)")-->b{Does the call have a userId} b-->|yes|c(Person ID = userId) b-.->|no|d{"Does the call have an email trait?"} d-->|yes|e(Person ID = traits.email) d-.->|no|f(Person ID = null) Hash function The hash function lets you hash a value using the SHA-256 algorithm. This is useful if you want to hash an email address or other personally identifiable information (PII) before sending it to an integration. hash(traits.email) Slugify function The slugify function converts a value to a slug—a value that consists only of letters, numbers, and hyphens. Slugifying a value removes all special characters, converts spaces to hyphens, and removes multiple hyphens. For example, slugify("Hello, World!") returns hello-world. This might be useful when you want to convert a value to a URL-friendly identifier. For example, if you want to convert an email address to a slug that you use to identify a person in destination. slugify(traits.email) toJSON and fromJSON functions These two functions help you handle stringified JSON data, but the naming convention can be a little confusing! fromJSON converts stringified JSON to a JSON object. toJSON converts a JSON object to a string, similar to JSON.stringify in JavaScript. You may want to convert to or from stringified JSON if you’re working with a service that expects JSON data in a specific format. var jsonTraits = { "email": "cool.person@example.com", "name": "Cool Person" } var stringifiedJSON = toJSON(jsonTraits) // returns '{"email":"cool.person@example.com","name":"Cool Person"}' fromJSON(stringifiedJSON) // returns the original JSONTraits object Empty values You can map values to an integration, but that doesn’t guarantee that those values are populated in your incoming data. Most incoming calls only require an ID of some sort or an event name, so you won’t necessarily receive errors if you send data into Customer.io that doesn’t fully populate all the data you want to capture in your outgoing integration. You’ll need to make sure sure that your data-in integrations send the right data—traits, event properties, etc—that you want to map to your outgoing integrations. Use customer attributes in actions In general, outgoing data relies on the data from incoming requests; the data you send to Customer.io is what you send out to your downstream system. But you can enrich outgoing data with customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. using the format $.customer.<attribute-name>. This lets you add attributes to outgoing data that aren’t included in the original call. For example, imagine you send a track event to Customer.io that doesn’t include a person’s email address, but you want to send the email address with the event to a downstream system. You can use the $.customer.email syntax to add the email address to the outgoing data.  Use the Variables drop-down to add attributes If you type $.customer.attribute-name directly in a field, we’ll treat it as a static string. Make sure that you type $.customer.attribute-name as a variable using the Variables drop-down that appears when you select a field to use attributes in your outgoing data. When you enrich your outgoing data with customer attributes, each action will show two entries in the Data out tab for your integration: the first is an internal request in Customer.io to look up the attributes that we add to the outgoing request, and the second is the request that we make to your external service. Testing actions with enriched data You can normally test actions with fake data, but you’ll need to use a real userId to test your action with enriched data. If you try to test with a userId of a person who doesn’t exist in your workspace, that person won’t have any attributes and therefore those parts of the action will be empty. You may want to keep a dedicated “test user” in your workspace that you can test against. This prevents you from accidentally sending test data to real members of your audience. Testing actions Go to the Tester tab for your integration to test your actions and make sure that your integration works as expected—sending data at the right time and in the right format. Select a Type of call end edit the payload to match (or not match) your action’s trigger and data structure. When you send a request, it’s a real request to your integration. You may want to use fake data that you can delete later. You might include a payload that matches your action’s trigger and data structure to make sure the request works as intended. You might also try a payload that doesn’t match your action’s trigger to make sure that your action doesn’t trigger! If the call succeeds, we’ll show you the response and tell you which action engaged. If the call fails, we’ll show you the error message and tell you which action failed. If you encounter errors when testing API calls, click Why did this error occur? to open a chat with the agent and get help with the error. --- ## Action triggers: code mode URL: https://docs.customer.io/integrations/data-out/action-trigger-syntax/ Each destination action has a *Trigger* field. This determines when we send data to your integration. By default, we have a visual editor to help you set up triggers, but you can use **Code mode** to write trigger conditions using our Filter Query Language (FQL). How it works On any action, click Code mode to see the FQL trigger for that action. You can set or edit the trigger conditions to determine when we send data to your integration. This can be especially helpful if you need to set up complex trigger logic with nested and/or conditions—which you can’t do with the simple visual editor. Standard view Code mode You’ll apply criteria that evaluates to true or false based on the contents of incoming requests. If the statement evaluates to true, we’ll send data (matching the action’s Data Structure) to your integration. The syntax supports basic boolean logic and equality operators like and and >=. But it also contains built-in functions that make it more powerful like contains( str, substr ) and match( str, pattern ). Example Imagine that you have an action called Create or Update Person, and you only want to use trigger this action when you send an identify call containing an email trait. Your filter might look like this: type = 'identify' and traits.email != null Then an identify call containing an email trait will trigger the action. If the call doesn’t contain an email trait, the call doesn’t match the trigger and we won’t send data to your integration. Would match Would not match { "type": "identify", "traits": { "email": "alex.lee@example.com", "first_name": "Alex", "last_name": "Lee", "fav_color": "blue" } } { "type": "identify", "traits": { "first_name": "Alex", "last_name": "Lee", "fav_color": "blue" } } What data can I reference in a trigger? Your trigger can reference any field in the incoming request, including nested properties like context.library.version or properties.title using JSON dot notation. { "type": "...", // type "event": "...", // event "context": { // context "library": { // context.library "name": "..." // context.library.name }, "page": { // context. page "path": "...", // context.page.path } } } Operators Comparison Use comparison operators to compare two values. You might do this to check if one value equals another or to compare string/number values. Operator Description = Equal != Not equal > Greater than >= Greater than or equal to < Less than <= Less than or equal to // Equals traits.email = 'alex.lee@example.com' // Not equals traits.email != 'alex.lee@example.com' // Greater than traits.total_visits > 30 Boolean comparison Compare two conditions with and or or. and: both conditions must be true or: at least one condition must be true // "and": both conditions must be true type = 'identify' and traits.email != null // "or": at least one condition must be true traits.phone != null or traits.email != null Not (logical negation) Use the ! operator to negate a condition. You can negate a complex condition by wrapping it in parentheses. // Negate the condition !traits.isLead // Negate a complex condition !( traits.isLead or traits.isContact ) Nesting conditions You can use parentheses to group conditions for more complex “and/or” logic. type = 'track' and ( event = 'Click' or match( 'Button *', event ) ) ( type = 'track' or type = 'identify' ) and ( properties.enabled or match( traits.email, '*@company.com' ) ) The match function The match( string, pattern ) function uses “glob” matching. It returns true if the given string fully matches a given pattern. Glob patterns are case sensitive. The examples below use a literal for the string we want to evaluate, but this is just to show how pattern matching works. In most cases you’ll evaluate an incoming variable. For example, match( event, 'purchase*' ) returns true if the event name for a track request begins with purchase. Pattern Summary * Matches zero or more characters. ? Matches one character. [abc] Matches one character in the given list. In this case, a, b, or c will match. [a-z] Matches a range of characters. In this case, any lowercase letter will match. \a Matches the character a literally. This is useful if you need to match *, ? or ] literally. For example, \*. Pattern Result Reason match( 'abcd', 'a*d' ) true * matches zero or more characters between a and d. match( '', '*' ) true * matches zero or more characters. match( 'abc', 'ab' ) false The pattern must match the full string. match( 'abcd', 'a??d' ) true ? matches any character between a and d, and the string is 4 characters long. match( 'abcd', '*d' ) true * matches one or more characters at the beginning of the string. match( 'ab*d', 'ab\*d' ) true \* matches the literal character *. match( 'abCd', 'ab[cC]d' ) true [cC] matches either c or C, and the string is 4 characters long. match( 'abcd', 'ab[a-z]d' ) true [a-z] matches any character between a and z, and the string is 4 characters long. match( 'abcd', 'ab[A-Z]d' ) false [A-Z] matches any character between A and Z but c is not in that range because it is lowercase. Error Handling Our user interface catches invalid FQL statements. We won’t let you save an action with invalid syntax. But we can’t catch things like misspellings, field names that don’t match incoming data, and so on. Make sure you use the Tester tab to test your actions to make sure they work the way you expect them to. --- ## Introduction URL: https://docs.customer.io/integrations/data-out/data-warehouses/data-warehouse-intro/ We offer two types of database and data warehouse integrations. This page explains more about how these integrations work and the differences between them. How database and data warehouse integrations work Rather than streaming data to your warehouse in real time, like with most outbound integrations, our database and data warehouse integrations send data to your storage buckets in bulk at regular intervals. Then you’ll ingest those files into the data warehouse or database of your choice. These integrations only create new files in your storage bucket; they’ll never overwrite or append an existing file, so you can delete or remove files from your storage bucket after you ingest them into their ultimate integration—your data warehouse or database. Standard integrations vs advanced integrations When you look for databases and data warehouses in our integration directory, you may see advanced entries. Standard, non-advanced integrations send data from your workspace. This is data that you use in Customer.io to send messages, trigger campaigns, and so on. Advanced integrations let you send data to a warehouse from any of your data sources even if you don’t keep that data in Customer.io. But it excludes some of the data that you keep in Customer.io. For example, if you send an identify call into Customer.io, the Advanced integration will receive data from the call more or less as you sent it. The Standard integration will receive the data as we process it in Customer.io—so you’ll see the changes made to the person in Customer.io. If your identify call doesn’t change anything, you won’t see any data/change in the Standard integration. But, if you need data about things like the campaigns people travel through, the messages they’ve received, and so on, you cannot get that data through the advanced versions of our database and data warehouse integrations. You’ll need to use the Standard integrations to get that data.  Standard and advanced integrations have different schemas Make sure you use the right documentation for your integration, because the data we output uses different schemas depending on your integration. Feature Description Standard Advanced Supports campaign data Data about the workflows and changes to campaigns. ✅ ❌ Supports actionA block in a campaign workflow—like a message, delay, or attribute change. data Data about the individual actionsA block in a campaign workflow—like a message, delay, or attribute change. people go through in each campaign journey. ✅ ❌ Supports broadcast data Information about newsletters (in Broadcasts schema) and API-triggered broadcasts (in Campaigns schema). ✅ ❌ Supports customer journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. (subjects) Data about the journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s. people take through your campaigns. ✅ ❌ Parquet output We sync files to your bucket in parquet format ✅ ✅ CSV output We sync files to your bucket in CSV ❌ ✅ JSON output We sync files to your bucket in JSON ❌ ✅ Sync interval How often we sync to your storage bucket ~15 minutes 10 minutes Supports data from outside your workspace Pass data to your warehouse without processing it in Customer.io (advanced). ❌ ✅  internal_customer_id is cio_id elsewhere in Customer.io Many of the data warehouse sync integrations include internal_customer_id and external_customer_id. These map to cio_id and id respectively. Standard integrations Our sync integrations pass information from your workspace to your storage bucket. That’s why we call them sync integrations: they sync data from your workspace to your data warehouse. For these integrations, we only send data to your integration after we’ve processed it in Customer.io. Our sync integrations let you send data about campaigns, journeys, and broadcasts do your data warehouse. These are our only integrations that support this kind of data! Advanced integrations We send incoming data to your storage bucket even if you don’t store that data in Customer.io. In general, this means that the schemas match up with our libraries. Like when you send an identify call, your data shows up in a file we send to your storage bucket called identify. But, unlike our sync integrations, non-sync integrations don’t have access to campaign and broadcast information from your workspace. --- ## Standard integrations URL: https://docs.customer.io/integrations/data-out/data-warehouses/data-warehouses-intro/ Our data warehouse integrations let you send Customer.io data about messages, people, metrics, etc to your data warehouse by way of an Amazon S3 or Google Cloud Project (GCP) storage bucket. From there, you can ingest the data to your warehouse. You can find our data warehouse integrations by going to Integrations and selecting the Databases option. From here, you can select your data warehouse or storage bucket. If your data warehouse appears twice in this list, pick the Data out integration. Check out the specific documentation for your data warehouse for help setting up your integration. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Your Data Warehouse loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. When you set up your sync, you can choose the parquet files that you want to export to your storage bucket. If you temporarily disable a file, and then turn it back on, the next parquet file will contain the changeset between when you disabled the file and when you enabled it. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. How do I get data into my data warehouse? There are many approaches to ingesting data from your storage bucket, but here’s an example moving data from a Google Cloud Storage bucket to Google BigQuery. Implement a Cloud Function to automatically import the parquet files from your GCS bucket to a BigQuery table. Set an expiration on the parquet files so they’re automatically deleted. Below is a screenshot of an example Cloud Function. Download sample code for this cloud function—based on our v5 schema—by clicking the link below: main.go Make sure you review the code and make appropriate modifications to fit your use case. Exported parquet files This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. Object-related schemas include an internal_object_id—an internal ID that we assign to all objects. You’ll use this value with the Objects export to resolve the names and IDs of your objects. Deliveries schema Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content schema The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics schema Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Subjects schema Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Outputs schema Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People schema The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Attributes schema Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns schema When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts schema The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions schema When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects schema The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types schema We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes schema Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events schema Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound (SMS) schema You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. Changelog v5 This version of our schema is backwards compatible with v4. It simply adds fields to a few schemas. Campaigns, Broadcasts, and Campaign Action schemas: Added a topic_names field. If you use our subscription center feature, this lists the topics a person must be subscribed to to receive your campaign, broadcast, or message action. Metrics Added fields that help you determine whether a metric was performed by a person or by a machine. These fields typically apply to email metrics. For example, an email service may prefectch images or scan your message to ensure that it is safe. In these cases, you may see that prefetched is true for an email opened metric. The new fields are: proxied prefetched machine user_agent email_client inbox_domain inbox_provider mx_host Upgrade to v5 If you’re upgrading from a version before v4, you should perform upgrade steps from the previous versions as well. The steps below represent an upgrade from the v4 schema. In your Customer.io workspace, go to Integrations, and click your data warehouse integration. Click Upgrade Sync. We’ll automatically disable the affected schemas so you can update your database. In your database, add a string-type topic_names column to the Campaigns, Broadcasts, and Campaign Action tables. In the Metrics table, add columns to handle the new fields: proxied, prefetched, machine, user_agent, email_client, inbox_domain, inbox_provider, and mx_host. Query the Campaigns, Broadcasts, and Campaign Action tables to populate the new topic_names column. Query the Metrics table to populate the new fields. Return to Customer.io and re-enable the Campaigns, Broadcasts, Campaign Action, and Metrics schemas. v4 If you’re on a prior schema, your Objects and Subjects tables were disabled on October 31, 2022. You’ll need to upgrade to continue receiving subjects and outputs. Version 4 includes the following changes from v3: The Subjects and Outputs tables have been disabled for previous versions. You must update to v4 to use Subjects and Outputs. Added a Delivery Content table, similar to the Deliveries table but this new table includes the actual message content for each delivery. You can associate content with a delivery by delivery_id. The Outputs schema now includes a new subject_name column. This is the same column from the Subjects table. Removed subject_id from the Outputs table. Upgrade to v4 In your Customer.io workspace, go to Integrations, and click your data warehouse integration. Click Upgrade Sync. We’ll automatically disable the Subjects and Outputs schemas, so you can update your database. In your database, add a string-type subject_name column to the Outputs table. Query the Subjects table to populate the new outputs.subject_name column. Drop the subject_id column from the Subjects and Outputs tables in your database. (Optional) Create a Delivery Content table based on the new delivery_content schema Return to Customer.io and re-enable the Subjects, Outputs, and/or Delivery Content schemas. v3 Version 3 includes the following changes from v2: A seq_num column in the Deliveries, Subjects, Outputs, and Metrics tables. This is a constantly increasing value, where a larger value indicates a more current record. People tables now contain record updates, rather than only the record as first created. For example, we produce a new record if you change a person’s email, delete them, or suppress them. The People table now has an updated_at column. Because many data warehouses don’t replace rows when adding duplicate primary keys, you can select the most recent updated_at value for each profile. The People table now contains an email_addr column. v2 Data warehouse sync v2 includes the following changes from v1: Support for transactional messages (as transactional_message_id) in the Deliveries schema. A fix for an issue that caused missing rows in Subjects and Outputs data. As a result of this bug, data warehouse v1 no longer supports Subjects or Outputs data. If you used our initial data warehouse release, we recommend updating to the v2 implementation. However, you can continue using our original data warehouse sync feature if you don’t use: Subjects and/or Outputs data Transactional messages Frequently asked questions How are exported parquet files organized? Each parquet file is named <name>_v<x>_<workspace_id>_<sequence>. <name> is either deliveries, metrics, subjects, outputs, or people. v<x> indicates the schema version. The current version is v4; v1 schemas do not have a version indicator and are deprecated. <workspace_id> refers to the Customer.io workspace whose data is included. <sequence> is an ever-increasing value over time.  Initial metrics sync file names Your initial metrics sync is broken up into files that indicate a starting and ending sequence, to help you order things appropriately, for example: <name>_v<x>_<workspace_id>_<sequence_pt1>_<sequence_pt2> How do you handle export failures? If we experience an internal failure, we monitor and repair it promptly. If there’s an external failure such as a networking problem, we retry the export; the next successful export will contain all data since the last successful export. You won’t lose data. If there’s a failure preventing us from connecting to your storage bucket, we’ll reach out to you with details. When you fix the issue, the next export will contain all data since the last successful export. If your export continues to fail after we’ve sent follow-ups, we’ll delete it automatically so that it doesn’t consume unnecessary resources. How should I import data from my bucket to my data warehouse? There are many approaches to this, but here’s one example we’ve seen work for moving data from a GCS bucket to BigQuery. Implement a Cloud Function to automatically import the parquet files from your GCS bucket to a BigQuery table. Set an expiration on the parquet files so they get automatically deleted. Below is a screenshot of an example Cloud Function. Download sample code for this cloud function—based on our v5 schema—by clicking the link below: main.go How can I get information about a campaign, workflow action, or message? We now have the Campaign Metadata schemas, which return campaign and actionA block in a campaign workflow—like a message, delay, or attribute change. names. Beyond that, you can our App API to pull any extra information you need. For example, calling https://api.customer.io/v1/campaigns/:id with the id of the campaign from a subject record will give you details about the campaign. How do I get all of the attributes for a profile? On setup, the initial Attributes export will include a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. In order to get the most recent attributes for a particular profile, you’ll need to use the timestamp value to query for the latest. An example query for Snowflake would be: select internal_customer_id, attribute_name, attribute_value from (select internal_customer_id, attribute_name, attribute_value, row_number() over (partition by internal_customer_id,attribute_name order by timestamp desc) RNO from attributes) where rno=1 and internal_customer_id='xxxxxxxxxxx'; --- ## Advanced Integrations URL: https://docs.customer.io/integrations/data-out/data-warehouses/data-warehouses-cdp/ We can stream incoming data to your data storage buckets, so you can sync your Customer.io data to a database outside of Customer.io. But these integrations work a little differently from other integrations in Customer.io. This page explains how we create files for your storage buckets which you can then import into your data warehouse or database.  Want information from your workspace, including campaign journeys? Try our standard Data Warehouse integration to get data from your workspace, including campaign journeys. How it works This integration forwards incoming data from data sources to your storage bucket independently of your workspace. This means that you can send data to your storage bucket even if you don’t store that data in Customer.io. It also means that this integration does not have access to campaign, broadcast, or journey information from your workspace. If you need that data, you should use a standard Data Warehouse integration instead. flowchart LR a(Data Source)-.->|real time|b(Your Workspace) a-->|10 minute sync|c(Storage Bucket) c-->|ingest data|d(Data Warehouse) Rather than streaming data to storage buckets in real time, like with most other data-out integrations, our data warehouse and storage integrations send data to your storage buckets in bulk at regular, 10-minute intervals. When we load data, we insert and update events, people, and groups, in JSON, CSV, or parquet files that we upload to your storage bucket. You can then ingest those files into the data warehouse or database of your choice. These integrations only create new files in your storage bucket; they’ll never overwrite or append an existing file, so you can delete or remove files from your storage bucket after you ingest them into their ultimate destination—your data warehouse or database. Exported files Our data warehouse and cloud storage integrations generate parquet, JSON, or CSV files that we load in a storage bucket you specify. The data we send (the files we generate in your storage bucket) are based on the Actions you enable. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Sync frequency Unlike other integrations where we send data in real time, these kinds of integrations attempt to send data to your storage bucket every 10 minutes—though actual sync intervals and processing times may vary. When syncing large data sets, or when you have a high volume of concurrent sync operations, it can take a little longer to process and export data. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Handling objects and arrays in CSV and Parquet files Our incoming integrations pass nested objects and arrays into calls as properties and traits, but CSVs and Parquet files don’t have a concept of objects or arrays. So we stringify or flatten properties and traits in CSVs and Parquet files to preserve your data without significantly manipulating it. { "received_at": "2019-08-24T14:15:22Z", "id": "a7280cfea0f6d", "user_id": "97980cfea0067", "anonymous_id": "d19b0cfeb606a", "sent_at": "2019-08-24T14:15:22Z", "traits": { "name": "Cool Person", "email": "cool.person@example.com", "likes_baseball": true }, "context": { ... } } received_at,id,user_id,anonymous_id,sent_at,traits,context 2019-08-24T14:15:22Z,a7280cfea0f6d,97980cfea0067,d19b0cfeb606a,2019-08-24T14:15:22Z,"{\"name\": \"Cool Person\", \"email\": \"cool.person@example.com\", \"likes_baseball\": true}", "{...}" Schemas When we load data into your storage buckets, we create and update files to match the shape of your incoming data. Note that we flatten or stringify nested objects and arrays according to the rules above. Identifies schema Identifies files contain identify calls sent into Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. Groups schema Groups files contain group calls made from your data-in integrations. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. Page schema Pages contains entries for the page calls your integrations send into Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type Screen schema Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. Track Schema Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type Alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. Timestamps We associate four timestamps with every incoming call to Customer.io: timestamp, original_timestamp, sent_at and received_at. All four timestamps pass through to your warehouse, and it may help to understand the purpose of each. In general, you should use timestamp when you query for historical events and received_at for all other queries based on time. timestamp is the UTC-converted timestamp set by the Customer.io library. If you import historical events using a server-side library, this is the timestamp you’ll want to reference in your queries. original_timestamp is the original timestamp set on data that comes into Customer.io. This timestamp can be affected by device clock skew. You can override this value by manually passing a timestamp in your incoming calls, which we map to the original_timestamp. Generally, this timestamp should be ignored in favor of the timestamp column. sent_at is a UTC timestamp set when you send calls to Customer.io. This timestamp can also be affected by device clock skew. received_at is a UTC timestamp set by Customer.io when we receive a payload. All tables use received_at as the sort key.  Use received_at for queries based on times The sent_at timestamp relies on a client’s device clock being accurate, which can be unreliable. id Each row in your database has an id which is equivalent to the messageId that our libraries pass in incoming calls. This is a unique identifier associated with the row. Sort Key All tables use received_at as the sort key. Amazon Redshift stores your data on disk in sorted order according to the sort key. The Redshift query optimizer uses sort order when it determines optimal query plans. --- ## Filtering and mapping actions URL: https://docs.customer.io/integrations/data-out/action-mapping/ In most cases, you'll want to use the default actions as provided for any integration. But, if your use case differs from the out-of-the-box experience, you can change the values that we map to your outgoing integration to fit your specific use case. How it works Each data-out integration has a list of available actions. An action is how we map incoming data to your outgoing integration. In most cases, the defaults are all you’ll ever need. But you might want to add additional action filters or change the values we map to various events to change when we send data out or to fine-tune the data that we send with each action. You can toggle actions on and off and change the data structure for actions in your integration’s Actions tab. Here’s a quick demonstration showing how actions work and how easy it is to change actions to fit your needs. Incoming data maps to Actions If incoming traffic matches an action’s Trigger and Filter, we’ll perform the associated action that sends data to your integration. flowchart LR a(Data-in event)-->b{Does it match a integration trigger} b-->|yes|c(Map data to integration) b-.->|no|d(Event is not forwarded to integration) Actions have a few properties. Action/Type: the type of action we’ll perform. Filter/Trigger: the criteria that incoming data must meet to trigger the action Mappings: the way we manipulate incoming data for your outgoing integration. Changing action mappings Again, you’ll generally want to use our default actions and mappings. But, when you set up an integration, you can go to the Actions tab and enable or disable actions. You can also click to change the way we map values to actions.  Be careful when editing actions for your workspace The Customer.io integration is how data gets into Customer.io—the people, events, and other data that you’ll use to send messages to your audience. It’s already configured with actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to support incoming data. You can edit these actions if you want to change how we handle incoming data, but take care not to inadvertently break integrations with Customer.io. When should I change triggers? Changing a trigger determines when we send data to your integration. You might change a trigger if you need to limit the data you send out of Customer.io. For example, many services have an some type of “Identify User” action that occurs whenever you pass an identify call to Customer.io. If your outgoing integration relies on an email trait, you might change the filter so that you only send identify calls when the call contain an email trait. When should I change data structure mapping? You’ll want to change the data structure of an action when your incoming data doesn’t naturally map to an outbound service. For example, you may want to change the traitsInformation that you know about a person, captured from identify events in Data Pipelines. Traits are analogous to attributes in Customer.io Journeys. from incoming identify calls that you send to your outbound integration. Imagine that your outbound integration expects a phone number, but your incoming data captures an imei trait. In this case, you could add a new mapping that converts traits.imei to a phone trait that your outbound integration expects. --- ## Resend past data URL: https://docs.customer.io/integrations/data-out/data-replay/ When you want to migrate from one platform for another, but you don't want to start from scratch, you can send your past your source data to a new destination! How it works Imagine that you’ve sent source data to an analytics platform for six months. Now you want to move to a new platform—it might have features you need or just aligns better with your business needs. Do you really have to start fresh in your new analytics platform? What happens to that six months of data? That’s what our Resend past data feature does: it lets you send data that you previously sent into Customer.io to a new outbound integration, so that you don’t have to start from scratch when you add a new outbound integration—like if you were to switch from one analytics platform to another. We store incoming data forever (or for as long as you want us to) so that you can access it and replay it to outbound integrations on demand. flowchart LR a(website)-->|real time data|c b(server-side integration)-->|real time data|c subgraph c [Customer.io] direction LR z(real time data) y(historical data) end z-.-x|disconnect old integration|d(old integration) z-->|connect real time integration|e(new integration) y-->|replay old data|e Data replay limitations Our data replay feature supports most, but not all, of our integrations. We can’t replay data from the Track API To replay data to a destination, it must have been sent to Customer.io through our Pipelines API. We can’t replay data that was originally sent to Customer.io through our Track API. This includes integrations based on our Track API like: The Legacy JavaScript snippet Data sent to Customer.io through a Customer Data Platform (CDP) like Segment or Rudderstack because their integrations use our Track API We can’t replay data from Reverse ETL or other syncs We can’t reply data from Reverse ETL, Salesforce, or Hubspot integrations through Customer.io. But you already have the ability to pull data from these sources at will; you can effectively ‘replay data’ from these sources without Customer.io’s help! Amazon Redshift Google BigQuery HubSpot Microsoft SQL server MySQL PostgreSQL Salesforce Snowflake We can’t replay data to these destinations You cannot replay data to data warehouses. These integrations sync data on regular intervals rather than receiving replayed historical data. However, they capture historical data during their first sync, so you can effectively “replay” data to them by creating a new integration. Amazon Redshift Amazon S3 Azure Blob Storage Google BigQuery Google Cloud Storage Snowflake We also can’t replay data that doesn’t go through our Pipelines API. This includes any integration relying on metrics from Customer.io: Reporting Webhooks Segment (Message Metrics) Mixpanel (Legacy) Amplitude (Legacy) Rudderstack (Legacy) How to resend data Contact us! For now, you’ll have to let us know when you want to resend past data, and how far back you want to go. Set up your new data-out integration. Double-check the actions for the new integration. Make sure that your past data works with the triggers and mappings for your integration. Contact Customer.io and let us know that you want to resend data. In the future, you’ll be able to resend data on your own! What outbound integrations can I send past data to? You can resend data to any new outgoing integration, but you’ll need to make sure that actions for the new integration support the source events you care about. For example, if you capture group events, but your new integration doesn’t have a concept of groups, then those events won’t apply or get mapped to your new integration. How long does it take to resend my data? The time that it takes to resend data depends on: The volume of data: how many events you want to send to your new integration. Rate limits for the integration. If your integration has a rate limit of 1000 events per second and you have a million events, then it’ll take at least 16 minutes and 40 seconds to resend your data after we start the operation. The total load on Customer.io during the resend operation. We process data with some elasticity. If it’s a busy day at Customer.io, it may take a little longer to resend your past data than it would on a slow weekend. --- ## Actable Predictive URL: https://docs.customer.io/integrations/data-out/connections/actable-predictive/ Getting started Go to Data & Integrations > Integrations and select the Actable Predictive entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Id: Your Actable-supplied Client ID. Client Secret: Your Actable-supplied Client Secret. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send Email Event Send an email event to Actable for prediction. Use this to supply clicks, opens, and unsubscribes. Send Web Activity Event Send a Web (or app) event to Actable for prediction. Use this to supply events like page views, link clicks, etc. Send Transaction Event Send a purchase event to Actable for prediction. Purchase events should be in v2 Commerce Spec. Send Custom Event Send a custom event to Actable for prediction. Use this to supply events that are not in Actable 's customer view. The Stream Key and custom events Each action includes a stream_key field that determines type of event we’ll send to Actable. In general, you shouldn’t change this field except when you set up a Custom Event Action. You might use Custom Event actions if your implementation contains transaction/purchase events that do not use the eCommerce Spec. For example, if you send purchase/transaction events that don’t conform to our specification, you might set up a custom event action and set the stream_key to transaction, and then map your transaction fields. --- ## Adobe Target URL: https://docs.customer.io/integrations/data-out/connections/adobe-target/ Getting started: Web integration The Adobe Target “Web” integration loads Adobe’s at.js script alongside our JavaScript client source so that you can upsert user profiles, trigger views, and track events. Go to Data & Integrations > Integrations and select the Adobe Target entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Code: Your Adobe Target client code. To find your client code in Adobe Target, navigate to Administration > Implementation. The client code is shown at the top under Account Details. Admin Number: Your Adobe Target admin number. To find your admin number, please follow the instructions in Adobe Docs. Version: The version of ATJS to use. Defaults to 2.8.0. Mbox Name: The name of the Adobe Target mbox to use. Defaults to target-global-mbox. Cookie Domain: The domain from which you serve the mbox. Adobe Target recommends setting this value to your company's top-level domain. Click Enable Destination. Getting started: Support all sources The standard Adobe Target integration only updates existing profiles in Adobe Target. If you want to add new profiles and send events, you’ll need to use the web version of this integration instead. Go to Data & Integrations > Integrations and select the Adobe Target entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Code: Your Adobe Target client code. To find your client code in Adobe Target, navigate to Administration > Implementation. The client code is shown at the top under Account Details. Bearer Token: If you choose to require authentication for Adobe Target's Profile API, you will need to generate an authentication token. Tokens can be generated in your Adobe Target account under the Implementation Settings tab or via the Adobe.IO Authentication Token API. Input the authentication token here. Note: Authentication tokens expire so a new token will need to be generated and updated here prior to expiration. Click Enable Destination. Supported Actions We support, and have default mappings for the following actions. Web integration: Create or update profile (upsert) Trigger View Track Event Supporting non-JS sources: Update person Create or update person (upsert) This action only works with the web version of this integration and creates or updates user profiles in Adobe Target. By default, we send this action based on identify calls from your web source(s). Field Type Description Mbox 3rd Party ID string A user’s unique visitor ID. Setting an Mbox 3rd Party ID allows for updates when you use this integration outside the web mode. Profile Attributes object Profile parameters specific to a user. If you send personally identifiable information to Adobe Target, you should hash it first. Trigger view This action only works with the web version of this integration and sends page views to Adobe Target. By default, we send this action based on page calls from your web source(s). Field Type Description View Name* string Name of the view or page. Page Parameters object Parameters specific to the view or page. Send Notifications to Adobe Target boolean By default, notifications are sent to the Adobe Target backend for incrementing impression count. If false, notifications are not sent for incrementing impression count. Mbox 3rd Party ID string A user’s unique visitor ID. Setting an Mbox 3rd Party ID allows for updates if you also use the standard version of this integration. Track Event This action only works with the web version of this integration and sends user actions—like clicks, conversions, and so on— to Adobe Target. By default, we send this action based on track calls from your web source(s). Field Type Description Event Type string The event type. The type event type must be registered and available in Adobe Target. Event Name string The event_name in Adobe Target. Event Parameters object Parameters specific to the event. Mbox 3rd Party ID string A user’s unique visitor ID for the person who performed the event. Setting an Mbox 3rd Party ID allows for updates if you also use the standard version of this integration. Update Person This action is available when you set up this integration in the normal, non-web version of this integration. It updates user profiles in Adobe Target. By default, we send this action based on identify calls from your web source(s). Field Type Description Mbox 3rd Party ID string A user’s unique visitor ID. Setting an Mbox 3rd Party ID allows for updates via the Adobe Target Integration. For more information, please see our Adobe Target Integration documentation. Profile Attributes object Profile parameters specific to a user. If you send personally identifiable information to Adobe Target, you should hash it first. You probably want to use the web integration You may need to set up both the web and non-web versions of this integration to get the most out of Adobe Target, but you must use the web integration if you want to create profiles in Adobe Target. Adobe Target only supports profile creation with their client-side library. We only load this library when you use the web integration, so you have to use the web integration if you want to create new profiles in Adobe Target. You can use this integration outside of web-only use cases to update existing profiles. When you set up your integration this way, we can’t create new profiles in Adobe Target, but it may be helpful to use this integration to augment information that you capture from your web integration with data from other sources. How does it work? Our JavaScript client library loads Adobe Target’s at.js script. While this script identifies people using a PCID we recommend that you identify people using the mbox3rdPartyId instead. In your integration, you should leave this value mapped to the default userId value (falling back to anonymousId for people you haven’t identified yet). This provides a common identifier that ties data back to the original profiles created from your JavaScript client. Depending on your users’ journeys, they could generate multiple profiles in Adobe Target. When an anonymous user arrives on your website, we’ll create an Adobe Target profile setting the mbox3rdPartyId to our anonymousId. If the same anonymous user visits your site on a different device, they’ll generate a new anonymousId and, therefore, a different mbox3rdPartyId. This results in a separate Adobe Target profile. This is Adobe’s standard PCID behavior. When you identify a user—like when they log into your service—the person is assigned a new mbox3rdPartyId corresponding to their userId. If this profile doesn’t already exist, they’ll have a another profile. If you identify people immediately upon their arrival—like in a single page app where people must login before they enter your website—then it’s likely that your users will always have the same userId and mbox3rdPartyId across devices (because you’ll always know exactly who they are). Finding data in Adobe Target Go to Adobe Target > Audiences > Create Audience > Add Rule to find and use your data in Adobe Target. Attributes appear under Visitor Profile attributes. Page Parameters show up as Custom attributes. Fields have page. prepended to the key. You can use Adobe Target audiences in activities, like A/B Testing and Experience Targeting. --- ## Algolia Insights URL: https://docs.customer.io/integrations/data-out/connections/algolia-insights/ Getting started Go to Data & Integrations > Integrations and select the Algolia Insights entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. App Id: Your Algolia Application ID. Api Key: An API key which has write permissions to the Algolia Insights API Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Product Clicked Events type = “track” and event = “Product Clicked” When a product is clicked within an Algolia Search, Recommend or Predict result Conversion Events type = “track” and event = “Order Completed” Successful product purcahses which can be tied back to an Algolia Search, Recommend or Predict result Product Viewed Events type = “track” and event = “Product Viewed” Product views which can be tied back to an Algolia Search, Recommend or Predict result --- ## Amazon Redshift URL: https://docs.customer.io/integrations/data-out/connections/amazon-redshift-data-out/ Send Customer.io data about messages, people, metrics, etc to your Amazon Redshift warehouse by way of an Amazon S3 or Google Cloud Project (GCP) storage bucket. This integration syncs up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Amazon Redshift loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up Amazon Redshift with Google Cloud Storage Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. To use a GCS storage bucket, you must set up a service account key (JSON) that grants read/write permissions to the bucket. You’ll provide the contents of this key to Customer.io when you set up this integration. Go to Integrations and select Amazon Redshift and then click Sync Bucket for Google Cloud Storage. Enter information about your GCS bucket and click Validate & select data. Enter Name of your GCS bucket. Enter the Path to your GCS bucket. Paste the JSON of your Service Account Key. Select the data that you want to export from Customer.io to your bucket. By default, we export all data, but you can disable the types that you aren’t interested in. Click Create and sync data. Set up Amazon Redshift with Amazon S3 or Yandex Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. For S3, you’ll need to set up your bucket with ListBucketVersions, ListBucket, GetObject, and PutObject before you can sync data from Customer.io. Create an Access Key and a Service Key with read/write permissions to your S3 or Yandex bucket. Go to Integrations and select Amazon Redshift and then click Sync Bucket. Enter information about your bucket and click Select data. Enter the Name of your bucket. Enter the path to your bucket. Paste your Access and Secret keys in the appropriate fields. Select the Region your bucket is in. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Each sync includes a cio-validate file If you sync data to an Amazon S3 bucket, Customer.io writes a file called cio-validate to your bucket before every sync. This is an empty file that we use to verify that we have write permissions to your bucket before each sync. You can safely delete this file. It does not affect data sync operations, and it’s not part of your exported data. If you have automated processes that import parquet files from your bucket, you may want to configure them to ignore the cio-validate file, since it’s not a parquet file and doesn’t contain any data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Amazon Redshift. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Amazon Redshift. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Amazon Redshift. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS) or Secret Key (S3, Yandex) again. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Amazon Redshift. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Amazon Redshift (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/amazon-redshift/ How it works This integration sends CSV, JSON, or parquet files containing your data to your Amazon Redshift (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Amazon Redshift (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and file names Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started To support Amazon Redshift (Advanced), you’ll set up a Google Cloud Storage, Amazon S3, or Microsoft Azure Blob Storage bucket to store your data. Then, you’ll query and import data from your storage bucket to Amazon Redshift (Advanced) either through a direct query or a product like Stitch. As a part of this integration, we’ll create parquet, JSON, or CSV files in your storage bucket. See data warehouses for a list of data schemas. Go to Data & Integrations > Integrations and select Amazon Redshift (Advanced) in the Directory tab. Connect to your storage bucket: Review your setup and click Finish to enable your integration. Google Cloud Storage (GCS) Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of the Google Cloud Storage Bucket where files will be written to. Learn more about GCS buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Service Account: The JSON string of the Google Cloud Service Account with permissions to upload files to a bucket, which can be found in your Google Cloud Console. Learn more about Google Cloud Service Accounts. Amazon S3 Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of an existing bucket. Learn more about S3 buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Access Key: The AWS Access Key ID that will be used to connect to your S3 Bucket. Your Access Key ID can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Secret Key: The AWS Secret Access Key that will be used to connect to your S3 Bucket. Your Secret Access Key can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Region: The AWS Region where your S3 Bucket resides in. Learn more about AWS Regions. Azure Blob Storage Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Blob Sas Url: The SAS URL of the Azure Blob Storage container with permissions to upload files to a container. Learn how to generate an Azure SAS URL in our documentation. Blob Path: Optional folder inside the container where files will be written to. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Amazon S3 URL: https://docs.customer.io/integrations/data-out/connections/amazon-s3-data-out/ Send Customer.io data about messages, people, metrics, etc to Amazon S3 storage. From here, you can ingest your data into the data warehouse of your choosing. This integration syncs up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Amazon S3 participant c as Data Warehouse loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up an Amazon S3 data-out integration Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. For S3, you’ll need to set up your bucket with ListBucketVersions, ListBucket, GetObject, and PutObject before you can sync data from Customer.io. Create an Access Key and a Service Key with read/write permissions to your S3 or Yandex bucket. Go to Integrations and select Amazon S3 and then click Sync Bucket. Enter information about your bucket and click Select data. Enter the Name of your bucket. Enter the path to your bucket. Paste your Access and Secret keys in the appropriate fields. Select the Region your bucket is in. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Each sync includes a cio-validate file If you sync data to an Amazon S3 bucket, Customer.io writes a file called cio-validate to your bucket before every sync. This is an empty file that we use to verify that we have write permissions to your bucket before each sync. You can safely delete this file. It does not affect data sync operations, and it’s not part of your exported data. If you have automated processes that import parquet files from your bucket, you may want to configure them to ignore the cio-validate file, since it’s not a parquet file and doesn’t contain any data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Amazon S3. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Amazon S3. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Amazon S3. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS) or Secret Key (S3, Yandex) again. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Amazon S3. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Amazon S3 (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/amazon-simple-storage-service/ How it works This integration sends CSV, JSON, or parquet files containing your data to your Amazon S3 (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Amazon S3 (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and file names Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started Go to Data & Integrations > Integrations and select Amazon S3 (Advanced) in the Directory tab. Connect to your storage bucket: Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of an existing bucket. Learn more about S3 buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Access Key: The AWS Access Key ID that will be used to connect to your S3 Bucket. Your Access Key ID can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Secret Key: The AWS Secret Access Key that will be used to connect to your S3 Bucket. Your Secret Access Key can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Region: The AWS Region where your S3 Bucket resides in. Learn more about AWS Regions. Review your setup and click Finish to enable your integration. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Amplitude URL: https://docs.customer.io/integrations/data-out/connections/amplitude/  We use Amplitude’s v2 API While we show the schemas that we map data to below, you’ll need to check out Amplitude’s documentation to learn more about their API. Getting started Go to Data & Integrations > Integrations and select the Amplitude entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Amplitude project API key. You can find this key in the "General" tab of your Amplitude project. Secret Key: Amplitude project secret key. You can find this key in the "General" tab of your Amplitude project. Endpoint: The region to send your data. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Log Event Default Trigger: type = “track” Send an event to Amplitude. Identify User Default Trigger: type = “identify” Set the user ID for a particular device ID or update user properties without sending an event to Amplitude. Map User Default Trigger: type = “alias” Merge two users together that would otherwise have different User IDs tracked in Amplitude. Group Identify User Default Trigger: type = “group” Set or update properties of particular groups. Note that these updates will only affect events going forward. Log Purchase Default Trigger: type = “track” Send an event to Amplitude. Log Event V2 Default Trigger: type = “track” Send an event to Amplitude --- ## Amplitude (Message Metrics) URL: https://docs.customer.io/integrations/data-out/connections/amplitude-metrics/ Take advantage of our native integration to export audience engagement events to Amplitude. How it works This integration sends your audience engagement data—also available as reporting webhooks—to Amplitude, where you can use it alongside your other user and analytics data to gain insight into your audience’s behaviors. Because Amplitude generally expects a unique, immutable user_id in events, we only send events to Amplitude when they represent a person with an id or a the event contains a device_id. We do not send events to Amplitude for people who do not have an id or aren’t represented by a device_id. The events we send are both based on things that happen in Customer.io and your customer activity. For example, an email sent event indicates that we’ve sent an email to a person, but an email clicked event lets you know that your audience clicked a link in your email. sequenceDiagram actor a as Your Audience participant b as Customer.io participant c as Amplitude b->>a: send email b-->>c: report email send a->>b: user clicks link b-->>c: report email clicked Enable the Amplitude integration As a part of this process, you’ll need your Amplitude API key. Go to Integrations and select the Amplitude integration card. Copy your Amplitude project’s API key into the API Key field. You’ll find your key in Amplitude Settings > Projects > General. (Optional) If your account is in Amplitude’s EU data centre, enable the Use EU Data Residency setting. Select the events that you want to send to Amplitude. We describe each event on the page. Check out our reporting documentation for more information about the data available to each event. (Optional) Enable Body Content to capture the first 1024 characters of messages in Sent events. This is an Amplitude limitation. You can still lookup deliveries in Customer.io to see the full contents of sent messages. Click Save.  Amplitude User IDs Amplitude does not allow a UserID of 0. If you have a profile with an ID of 0, we’ll use the email address instead for only that profile. Test your integration When you add your API key and set up your integration, you can click Test Connection to make sure that your key is valid and connects to the correct project in Amplitude. The test sends an email sent event to Amplitude to ensure that your API key is valid. You can check your Amplitude project for this email sent event to make sure that you set up your integration for the Amplitude project you want to send Customer.io data to. Payload reference In this integration, we map our reporting webhook payloads to fit Amplitude. Below is a list of fields from our reporting webhooks and how we map them to Amplitude. We’ve also provided examples of both kinds of events. Amplitude Field Customer.io Field Description user_id identifiers.id device_id recipients.device_id platform recipients.platform As of March 28, 2024, we capitalize/camel-case these items, like Web, Android, iOS. event_type object_type + metric We capitalize/camel-case these items, like Email Sent insert_id event_id Amplitude uses this ID to de-duplicate events time timestamp The date-time when the event occurred groups account_id and workspace_id While your account and workspace IDs aren’t in our standard reporting webhook payload, we send this to Amplitude to support their Accounts add-on add on. event_properties This covers the remaining fields in the payload, including identifiers.email Amplitude payload Amplitude payload { "$insert_id": "01GJDDNVWD723D5GNSEAQJVSEB", "$insert_key": "018a4e4d19084729ad7c4a77667a832afd#905", "$row_source": "realtime", "$schema": 13, "_time": 1669045350000, "adid": null, "amplitude_attribution_ids": null, "amplitude_event_type": null, "amplitude_id": 507275024343, "app": 419422, "city": null, "client_event_time": "2022-11-21 15:42:30", "client_upload_time": "2022-11-21 15:42:29.394000", "country": null, "data": { "group_first_event": {}, "group_ids": { "257325": [ 10753477083 ], "257326": [ 10871859635 ] } }, "data_type": "event", "device_brand": null, "device_carrier": null, "device_family": null, "device_id": "d148bf61-a152-5e08-bcc1-e8be96a9fce4", "device_manufacturer": null, "device_model": null, "device_type": null, "display_name": "Sms Sent", "dma": null, "event_id": 148912891, "event_properties": { "action_id": 1001266, "campaign_id": 1000223, "content": "Hey jenny! I got your number!", "customer_id": "313803", "delivery_id": "dgSiuAMBAMeAFMWAFAGEmtrrwMzFAHs6nMGmSG4=", "journey_id": "01GJDDNSY0X731SPYZEBKRNZZ1", "recipient": "5558675309" }, "event_time": "2022-11-21 15:42:30", "event_type": "Sms Sent", "global_user_properties": {}, "group_properties": { "account_id": { "1": {} }, "workspace": { "56354": {} } }, "groups": { "account_id": [ "1" ], "workspace": [ "56354" ] }, "idfa": null, "ip_address": null, "is_attribution_event": false, "language": null, "library": "batch/1.0", "location_lat": null, "location_lng": null, "os": "", "os_name": null, "os_version": null, "partner_id": null, "paying": null, "plan": {}, "platform": null, "processed_time": "2022-11-21 15:42:42.526145", "region": null, "sample_rate": null, "server_received_time": "2022-11-21 15:42:29.394000", "server_upload_time": "2022-11-21 15:42:39.646000", "session_id": -1, "source_id": null, "start_version": null, "timeline_hidden": false, "user_creation_time": "2022-11-21 15:42:28", "user_id": "313803", "user_properties": {}, "uuid": "22ed3a6e-69b3-11ed-898f-d5a02747ae5a", "version_name": null } Customer.io payload Customer.io payload { "event_id": "01GJBCEYDJMH4VCDKXC98JNQ2D", "object_type": "sms", "timestamp": 166904535, "metric": "sent", "data": { "trigger_id": 1, "customer_id": "313803", "delivery_id": "dgSiuAMBAMeAFMWAFAGEmtrrwMzFAHs6nMGmSG4=", "action_id": 1001266, "campaign_id": 1000223, "journey_id": "01GJDDNSY0X731SPYZEBKRNZZ1", "identifiers": { "id": "313803", "email": "cool.person@example.com", "cio_id": "d9c106000001" }, "content": "Hey jenny! I got your number!", "recipient": "+15558675309" } } --- ## Attio URL: https://docs.customer.io/integrations/data-out/connections/attio/ Our Attio destination creates or updates records for people, users, companies, and workspaces in Attio. This integration helps you keep your Attio workspace up-to-date with the latest information about your customers. Getting started Go to Data & Integrations > Integrations and select the Attio entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Click Enable Destination. Identify User Attio has two different kinds of records for people: Person and User. The Identify User action asserts both records and links them together. Make sure you enable this in Actions. A Person represents an actual human. People have names, email addresses, Twitter profiles, email and calendar interactions, etc. (In some other CRMs, you might think of this as a lead or contact.) A User represents a user login or profile in your product. Users might have feature flags, permission levels, etc. In Attio, a person can have multiple user records—like a person that has multiple logins across workspaces. When you identify someone, the Attio Identify User action creates both records—a person and a user—and links them together. Attio relies on both email and ID values to create, update, and associate people so your `identify call must capture both properties: An email property, to create or update a Person. By default, we map to traits.email in your identify call. An ID property, to create or update an associated User. By default we map to the userId in your identify call. { "type": "identify", "userId": "user-id", "traits": { "email": "user@example.com" } } If you need to assert people and users independently, you can configure an Assert Record action instead.  Make sure you’ve activated the user object You cannot use the standard user object before you activate it. Go to your Workspace Settings > Objects in Attio and click Activate next to the users object. Set people and user attributes  Make sure that your attributes exist in Attio You can send attributes or traits to Customer.io and we’ll create them if they don’t already exist. That’s not the case in Attio. If you map an attribute that doesn’t exist in Attio, your action will fail and we’ll show errors for your identify requests. See attribute types below for more information. You can assign attributes to people or users in Attio as a part of your Identify User action. To do this, you’ll need the IDs or slugs of the attributes you want to set. In general, the human-readable slugs are easier to deal with. To set attributes in your Identify User action, click Add Key/Value for Additional User attributes or Additional Person attributes. The value on the left is the attribute ID or slug from Attio, and the value on the right is the trait from your incoming identify call that you want to set on the person or user. Note that users don’t typically have many default attributes in Attio. Make sure that your attributes are declared in Attio before you add them to your Identify User action. See attribute types below for more information. Associate people with a workspace If you want to group users together in Attio, associate them with a workspace. You’ll need to update the Identify User action: Click Actions. Beside Identify User, click then Edit. Beside Additional User attributes, click Add Key/Value. Set the Key to workspace. Set the Value to the variable $.groupId. Click Save. Then your payload would look like this: { "type": "identify", "userId": "user-id", "groupId": "workspace-id", "traits": { "email": "user@example.com" } }  Make sure you’ve activated the workspace object You cannot use the standard workspace object before you activate it. Go to your Workspace Settings > Objects in Attio and click Activate next to the workspace object. Group Workspace The Group Workspace action creates or updates a company (by domain, like company.com) and an associated workspace (by name). Make sure you enable this in Actions. A Company can have names and domains, as well as enriched properties like ARR or category. A Workspace represents a group of users in your product. Workspaces might have feature flags, billing configurations, customer support representatives, etc. A company can have more than one workspace. To support both companies and workspaces, your group calls need to include both a groupId and a website trait. The groupId corresponds to Workspace ID in Attio and the website is the company’s Domain in Attio. { "type": "group", "groupId": "Workspace ID", "traits": { "website": "company.com" } } If you want to create a company and workspace in Customer.io AND Attio, include an object type ID in the payload. We recommend you first create your object type in Customer.io then use the ID in the call. You can represent an object typeAn object type is a group of objects. An object type could be Online Classes while an object within the type could be English 101. Customer.io generates a unique, immutable object_type_id. in Customer.io as a company in Attio and an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. as a workspace. { "type": "group", "groupId": "Workspace ID", "traits": { "website": "company.com", "objectTypeId": 5 } } This mapping asserts both a company and workspace and links them together. If you need to assert your companies and workspaces independently, you should set up your own Assert Record action(s).  Make sure you’ve activated the workspace object You cannot use the standard workspace object before you activate it. Go to your Workspace Settings > Objects in Attio and click Activate next to the workspace object. Set company and workspace attributes  Make sure that your attributes exist in Attio You can send attributes or traits to Customer.io, and we’ll create them if they don’t already exist. That’s not the case in Attio. If you map an attribute that doesn’t exist in Attio, your action will fail and we’ll show errors for your group requests. See attribute types below for more information. To assign attributes to a company or workspace in Attio, you’ll need the ID or slug of the attribute you want to set. In general, the human-readable slug is easier to deal with. To set attributes in your Group Workspace action, you’ll click Add Key/Value for Additional Company attributes or Additional Workspace attributes. The value on the left is the attribute ID or slug from Attio, and the value on the right is the trait from your incoming group call that you want to set on the person or user. For example, if you want to set a company name or workspace name in Attio, and you send traits.companyName and traits.workspaceName in your group call, you’d add the following keys and values. Or if you want to set a twitter attribute in Attio, but you send traits.twitter_handle in your group call, you would set up your mapping like this: Workspaces don’t typically have many default attributes in Attio. Make sure that your attributes are declared in Attio before you add them to your Group Workspace action. See attribute types below for more information. Assert Record: create your own action The Assert Record action lets you create or update an Attio object (like a person or a company) with a matching attribute and value. For example, you could assert an individual person by ID by email address without updating associated user records. When you set up an Asset Record, you’ll need to set the Attio Object property. This is the kind of record you want to assert (create or update) in Attio. Then, you’ll need to set the Matching Attribute property. This is the slug for the attribute in Attio, and must also be present in your incoming call. Using our example above, we’d select Person as our Attio Object and email as our Matching Attribute. So, imagine we send an identify request that looks like this: { "type": "identify", "userId": "person@example.com", "traits": { "twitter_handle": "@example-person" } } This action then attempts to find an existing person where the email matches your incoming userId. If it finds the email, we’ll update the associated twitter attribute. If it doesn’t find the email, we’ll create a new person with the email and twitter handle you’ve provided. Attribute types Before you add attributes to any of your Attio destination actionsA block in a campaign workflow—like a message, delay, or attribute change., you should make sure that they exist in Attio and that you have the right ID or slug. Go to your Objects page in Attio. Select the object you want to set attributes on and go to the Attributes tab. Find your attribute, click ︙ and select Copy slug. Our Attio destinationAn integration that sends data out of Customer.io—your data’s ultimate destination. integration supports all of Attio’s attribute data types. Below are examples of the formats that Attio expects for each type of attribute. A request will fail if you attempt to send a value that doesn’t match the attribute type in Attio. You can pass null to unset any attribute value in Attio. type Format Example values actor-reference An email address of a workspace member "alice@attio.com" checkbox Boolean true, false currency Number with up to 4 decimal places 99, 29.9999 date YYYY-MM-DD "2023-09-28" domain {domain}.{tld} "app.attio.com", "www.example.com" email A valid email address "person@example.com" location String with all valid address parts (street address, city, state, country, and postal code) combined “1 Infinite Loop, Cupertino, CA, US” number Number, stored as a 64-bit float 42.192, 17 personal-name Last name(s), First name(s) (note the comma in the middle) "Bloggs, Joe" phone-number E.164 format, starting with +... "+15558675309" pipeline A UUID or title representing the status "open", "closed" rating Integer from 0 to 5 0, 5 record-reference Specifies a reference to another record. See Attio’s Record reference for more information. All standard records have at least one “record reference” attribute. For example, you typically reference a person in Attio by email address or a user by ID. "person@example.com", "app.attio.com", "0677efa..." select A UUID or title representing the option "open" text String "A piece of text" timestamp ISO8601, e.g. YYYY-MM-DDTHH:MM:SS. See Attio’s Timestamp documentation for more about supported timestamp formats. "2023-09-28T04:39:17" --- ## Azure blob storage URL: https://docs.customer.io/integrations/data-out/connections/ms-azure-data-out/ Send Customer.io data about messages, people, metrics, etc to Microsoft Azure Blob Storage. From here, you can ingest your data into the data warehouse of your choosing. This integration exports files up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Azure Blob Storage participant c as Data Warehouse loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up an Azure Blob Storage integration As a part of this process, you’ll create an Access policy and a Shared Access Signature (SAS) URL. The Shared Access Signature grants Customer.io access to your Azure blob container, but typically has a limited expiration date. Before you generate a SAS URL, you’ll create the Access policy (with read, write, add, create, and list permissions) that lets you set a longer expiration date for your SAS URL and provides a way to revoke the token later, if you decide to shut off this integration for any reason.  You must generate an SAS URL for a container, not a storage account! Please follow the steps below to generate an SAS URL for a specific container in your Azure Blob Storage account. You can’t use a SAS URL for the storage account with Customer.io. Login to your Azure account, go to Storage browser, and select Blob Containers. Right click the container you want to export Customer.io data to, and select Access policy to create a policy allowing you to create a SAS URL with a long expiry date. Click Add policy and set an Identifier for the policy. This is just the name of the access policy that you’ll use in later steps. Click Permissions and select read, write, add, create, and list. Set the Start time to the current date. Set the Expiry time to a date well into the future. Click OK and then click Save. Right click the container again and select Generate SAS to generate the URL that Customer.io will use to access your Azure bucket. Select the Stored access policy you created in previous steps. (Optional) List Customer.io’s IP addresses under Allowed IP addresses to provide an extra layer of security for your SAS URL. Click Generate SAS token and URL and copy the URL. When you close the dialog, you won’t be able to access the token or URL again, so make sure that you copy the URL. You’ll need it in later steps. Go to Customer.io and select Integrations > Azure Blob Storage. Click Sync your Azure Blob Storage bucket. Enter the Blob Path: this is the directory in the blob where you want to deposit parquet files with each sync. If you don’t provide a path, we’ll deposit files in the root of the blob. If the path doesn’t already exist, clicking “Validate & Select Data” will create a new blob storage path. Paste your Blob SAS URL in the appropriate box and click Validate & select data. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Azure Blob Storage. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Azure Blob Storage. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Azure Blob Storage. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Blob SAS URL. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Azure Blob Storage. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. Troubleshooting I get a 403 error in Customer.io This means that your Shared Access Signature URL doesn’t grant Customer.io permission to access your Azure blob store. Make sure that your Access policy or Shared access signature grant read, add, create, write, and list permissions. If not, you may need to edit your Access policy or generate a new SAS URL and paste it into Customer.io to fix the issue. I can’t extend my SAS URL’s expiry date By default, you Microsoft Azure doesn’t allow a Shared Access Signature (SAS) more than 1 week or 365 days in the future, depending on the signing method you use. You must create an Access policy that allows you to create tokens with a longer expiration period. In general, we suggest that you create an Access policy as a way to both extend the life of your SAS token and as a method for revoking your SAS token later if you decide to turn off this integration. To create an Access policy, right click your blob container in Microsoft Azure and go to Access policy. Add a policy with the same permissions as your SAS token (read, add, create, write, and list permissions), and set an expiry date in the future. --- ## Bing Ads URL: https://docs.customer.io/integrations/data-out/connections/bing-ads/ Getting started Before you can track conversions or target audiences, you need to create a UET tag in Bing Ads and then add it to the destination settings. Follow the steps within the Bing Ads documentation to create a UET tag. You’ll only be able to include one Tag ID per source so make sure to associate conversion criteria to the correct Tag ID that is included in your settings. Go to Data & Integrations > Integrations and select the Bing Ads entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Tag ID: Your Bing Universal Event Tracking Tag ID. Learn more Click Enable Destination. It may take up to 45 minutes to fully activate your destination and begin asynchronously loading the Bing Ads snippet on your website. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Page View type = “page” Track the current page Track Custom Event type = “track” Track a custom conversion event Track Checkout Started Event type = “track” and event = “Checkout Started” Track the Checkout Started event which will be sent as a begin_checkout event to Bing. More information is available on the documentation. Track Order Completed Event type = “track” and event = “Order Completed” Track the Order Completed event which will be sent as a purchase event to Bing. More information is available on the documentation. Track Product Added Event type = “track” and event = “Product Added” Track the Product Added event which will be sent as a add_to_cart event to Bing. More information is available on the documentation. Track Product Added To Wishlist Event type = “track” and event = “Product Added To Wishlist” Track the Product Added To Wishlist event which will be sent as a add_to_wishlist event to Bing. More information is available on the documentation. Track Product List Viewed Event type = “track” and event = “Product List Viewed” Track the Product List Viewed event which will be sent as a view_item_list event to Bing. More information is available on the documentation. Track Products Searched Event type = “track” and event = “Products Searched” Track the Products Searched event which will be sent as a view_search_results event to Bing. More information is available on the documentation. Track Product Viewed Event type = “track” and event = “Product Viewed” Track the Product Viewed event which will be sent as a view_item event to Bing. More information is available on the documentation. Setting conversion criteria Before you can map your events to conversion goals, you need to create conversion criteria in Bing Ads and associate those events with your UET tag. In Bing Ads, go to the Campaign > Conversion Goals. Under Conversion Tracking, click Conversion Goals and select Create conversion goal. Enter a name for your goal. Use a descriptive name that makes sense to you, like Checkout started. Choose the Event type of conversion and click Next. Fill in the appropriate values. Make sure to add the source event name as the label field and to associate the goal to the correct UET Tag that you set up in your destination. Troubleshooting: script unverified or undetected by third-party tool This error is typically a limitation on a third-party tool’s detection process, where the detector is looking for a specific HTML element on your page. Our client side JavaScript library loads the Bing Ads script onto the page, so the detector doesn’t see it. To confirm that the Bing Ads script is actually loaded on your page(s), you can open your browser’s console and check the Network tab when the page loads. If the script does not load, make sure that your ad-blocker is disabled. --- ## Braze URL: https://docs.customer.io/integrations/data-out/connections/braze/ Web only mode You can set up this integration in “web” mode. When set up this way, our JavaScript client will load the Braze SDK and send data directly to Braze, bypassing Customer.io entirely. We don’t typically recommend setting things up this way, because: You won’t capture data in Customer.io. As the name suggests, you cannot use this mode with other sources of data (mobile SDKs, server-side libraries, and so on). It can be hard to debug this sort of implementation. Otherwise, there are very few functional differences besides the sources they support. Getting started Go to Data & Integrations > Integrations and select the Braze entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Created under Developer Console in the Braze Dashboard. App Id: The app identifier used to reference specific Apps in requests made to the Braze API. Created under Developer Console in the Braze Dashboard. Endpoint: Your Braze REST endpoint. See more details Click Enable Destination. Identifiers in Braze Braze calls require an external_id or a braze_id. We map userId from source events to Braze’s external_id. The braze_id is essentially Braze’s anonymous ID. So, if you haven’t identified a user by userId, we use the braze_id—which is just Braze’s anonymous Identifier. Otherwise, when you’ve identified someone, we’ll use the external_id. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Update User Profile Default Trigger: type = “identify” Update a user’s profile attributes in Braze Track Event Default Trigger: type = “track” and event != “Order Completed” Record custom events in Braze Track Purchase Default Trigger: event = “Order Completed” Record purchases in Braze Create Alias Default Trigger: event = “Create Alias” Create new user aliases for existing identified users, or to create new unidentified users. Identify User Identifies an unidentified (alias-only) user. Use alongside the Create Alias action, or with user aliases you have already defined. Getting started: web mode Go to Data & Integrations > Integrations and select the Braze entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. See Configuration Settings below for information about individual configuration parameters. Click Enable Destination. Web mode settings Setting Default Description Sdk Version 4.6 The version of the Braze SDK to use Api Key Found in the Braze Dashboard under Manage Settings → Apps → Web Endpoint sdk.iad-01.braze.com Your Braze SDK endpoint. See more details Allow Crawler Activity Allow Braze to log activity from crawlers. See more details Allow User Supplied Javascript To indicate that you trust the Braze dashboard users to write non-malicious Javascript click actions, set this property to true. If enableHtmlInAppMessages is true, this option will also be set to true. See more details Defer Until Identified If enabled, this setting delays initialization of the Braze SDK until the user has been identified. When enabled, events for anonymous users will no longer be sent to Braze. App Version Version to which user events sent to Braze will be associated with. See more details Content Security Nonce Allows Braze to add the nonce to any <script> and <style> elements created by the SDK. See more details Device Property Allowlist By default, the Braze SDK automatically detects and collects all device properties in DeviceProperties. To override this behavior, provide an array of DeviceProperties. See more details Disable Push Token Maintenance By default, users who have already granted web push permission will sync their push token with the Braze backend automatically on new session to ensure deliverability. To disable this behavior, set this option to true Do Not Load Font Awesome Braze automatically loads FontAwesome 4.7.0 from the FontAwesome CDN. To disable this behavior set this option to true. Enable Logging Set to true to enable logging by default Enable Sdk Authentication Set to true to enable the SDK Authentication feature. In App Message Z Index By default, the Braze SDK will show In-App Messages with a z-index of 1040 for the screen overlay, 1050 for the actual in-app message, and 1060 for the message's close button. Provide a value for this option to override these default z-indexes. Localization en By default, any SDK-generated user-visible messages will be displayed in the user's browser language. Provide a value for this option to override that behavior and force a specific language. The value for this option should be a ISO 639-1 Language Code. Automatically Display Messages true When this is enabled, all In-App Messages that a user is eligible for are automatically delivered to the user. If you'd like to register your own display subscribers or send soft push notifications to your users, make sure to disable this option. Manage Service Worker Externally If you have your own service worker that you register and control the lifecycle of, set this option to true and the Braze SDK will not register or unregister a service worker. See more details Minimum Interval Between Trigger Actions in Seconds 30 Provide a value to override the default interval between trigger actions with a value of your own. See more details No Cookies By default, the Braze SDK will store small amounts of data (user ids, session ids), in cookies. Pass true for this option to disable cookie storage and rely entirely on HTML 5 localStorage to identify users and sessions. See more details Open Cards in New Tab By default, links from Card objects load in the current tab or window. Set this option to true to make links from cards open in a new tab or window. Open in App Messages in New Tab By default, links from in-app message clicks load in the current tab or a new tab as specified in the dashboard on a message-by-message basis. Set this option to true to force all links from in-app message clicks open in a new tab or window. Require Explicit in App Message Dismissal By default, when an in-app message is showing, pressing the escape button or a click on the greyed-out background of the page will dismiss the message. Set this option to true to prevent this behavior and require an explicit button click to dismiss messages. Safari Website Push Id If you support Safari push, you must specify this option with the website push ID that you provided to Apple when creating your Safari push certificate (starts with "web", e.g. "web.com.example.domain"). Service Worker Location By default, when registering users for web push notifications Braze will look for the required service worker file in the root directory of your web server at /service-worker.js. If you want to host your service worker at a different path on that server, provide a value for this option that is the absolute path to the file, e.g. /mycustompath/my-worker.js. VERY IMPORTANT: setting a value here limits the scope of push notifications on your site. For instance, in the above example, because the service ,worker file is located within the /mycustompath/ directory, appboy.registerAppboyPushMessages MAY ONLY BE CALLED from web pages that start with http://yoursite.com/mycustompath/. Session Timeout in Seconds 1800 By default, sessions time out after 30 minutes of inactivity. Provide a value for this configuration option to override that default with a value of your own. Web mode actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Update User Profile Default Trigger: type = “identify” or type = “group” Updates a users profile attributes in Braze Track Event Default Trigger: type = “track” and event != “Order Completed” Reports that the current user performed a custom named event. Track Purchase Default Trigger: type = “track” and event = “Order Completed” Reports that the current user made an in-app purchase. Debounce Middleware Default Trigger: type = “identify” or type = “group” When enabled, it ensures that only events where at least one changed trait value are sent to Braze, and events with duplicate traits are not sent. Debounce functionality requires a frontend client to work. Therefore, it cannot be used with server-side libraries or with Engage. --- ## Braze Cohorts URL: https://docs.customer.io/integrations/data-out/connections/braze-cohorts/ Getting started Go to Data & Integrations > Integrations and select the Braze Cohorts entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Secret: Data Import Key for the client whose cohort this belongs to. Also known as customer key. Endpoint: Your Braze REST endpoint. See more details Click Enable Destination. The setup is complete and the Audience will start syncing to Braze Cohorts. We create a new cohort (if one does not already exist for the Audience Key) and add/remove users to/from the cohort accordingly. The audience will appear in your Braze account under Engagement > Segments. If you add multiple actions in your Braze Cohorts destination, we recommend changing the trigger for your actions Event Property audience_key = <your_audience_key>. This ensures that each action maps specifically to the cohort you want to target in Braze. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Sync Audience event = “Audience Entered” or event = “Audience Exited” Record custom events in Braze --- ## Clevertap URL: https://docs.customer.io/integrations/data-out/connections/clevertap/ Getting started Go to Data & Integrations > Integrations and select the Clevertap entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Clevertap Account Id: CleverTap Account Id. This can be found under Settings Page. Clevertap Passcode: CleverTap Passcode. This can be found under Settings Page. Clevertap Endpoint: Account regions. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. User Upload The User Upload Action enables you to create or update user profiles in CleverTap. User Delete The User Delete Action enables you to remove user profiles in CleverTap. Conversion timestamps If you want to convert ISO-8601 strings to timestamps, set Convert Timestamps when setting up actions. This converts attributes containing ISO-8601 strings (like 2023-05-18T22:58:45+00:00) to Unix timestamps (like 1684475925). We’ll only convert valid ISO-8601 date-times down to millisecond precision. If you store your timestamps in another format, or store timestamps using nanosecond precision, we won’t convert them. 2025-08-13T15:45:17.451Z ✅ (milliseconds - will convert) 2025-08-13T02:08:40.1418726Z ❌ (nanoseconds - won’t convert) --- ## Close URL: https://docs.customer.io/integrations/data-out/connections/close/ Getting started Go to Data & Integrations > Integrations and select the Close entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Your Close API key. Lead Custom Field Id For Company Id: Enter the ID of a Lead Custom Field that'll be used to store Company ID. You'll need to create this Lead Custom Field in Close first, and then the integration will use this field to store the Company ID when creating new contacts, and/or will be used as a lookup key when updating existing Lead. The Custom Field type must be a text. If this field is not filled out, it will only lookup and de-dupe based on Contact's email. Contact Custom Field Id For User Id: Enter the ID of a Contact Custom Field that'll be used to store User ID. You'll need to create this Contact Custom Field in Close first, and then the integration will use this field to store the User ID when creating new contacts, and/or will be used as a lookup key when updating existing Contacts. The Custom Field type must be a text. If this field is not filled out, it will only look up and de-dupe based on email. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Create or Update Contact and Lead type = “identify” Create or Update Contact and/or Lead. At first, Close will try to find Lead via Lead Company ID. If Lead is not found, Close will try to find a Contact either via Contact User ID or via Contact Email. If Contact is not found, Close will create a new Lead and Contact. It will also create a new Lead and Contact if Contact is found but exists under a Lead with different Lead Company ID. If the Action does not specify Lead Company ID, Close will update the Contact and also the Contact’s Lead. It might happen that Close will find multiple Contacts with the same Contact User ID or Contact Email. In such case, Close will update up to 10 Contacts, ordered by creation date. --- ## CommandBar URL: https://docs.customer.io/integrations/data-out/connections/commandbar/ Getting started Go to Data & Integrations > Integrations and select the CommandBar entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Org Id: The ID of your CommandBar organization. Deploy: If enabled, CommandBar will be deployed to your site automatically and you can remove the snippet from your source code. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Identify User type = “identify” Set attributes for the user in CommandBar. If "Deploy automatically" is enabled, then also boot CommandBar for the user, which makes CommandBar available to the user. Track Event type = “track” Submit an event's properties as CommandBar metaData. --- ## Cordial URL: https://docs.customer.io/integrations/data-out/connections/cordial/ Use cases Keep Cordial contacts in sync with your Customer.io data. When someone signs up or updates their profile, their contact record in Cordial updates automatically. Track customer activity in Cordial for use in Cordial’s automation and segmentation tools. Send events like page views, purchases, or feature usage so Cordial can trigger campaigns based on behavior. Manage Cordial marketing lists by automatically adding or removing contacts based on audience membership, subscription status, or other criteria. Sync e-commerce data including cart contents and order details, enabling abandoned cart campaigns and post-purchase follow-ups in Cordial. Getting started Go to Data & Integrations > Integrations and select the Cordial entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Your Cordial API Key Endpoint: Cordial API endpoint. Leave default, unless you've been provided with another one. See more details Segment Id Key: Cordial string unique attribute key to store Segment User ID in (e.g. segment_id) Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Create Contactactivity type = “track” or type = “page” Create a new contact activity. Upsert Contact type = “identify” Create or update a contact in Cordial. Add Contact to List type = “group” Add contact to a list. If the list does not exist in Cordial it will be created. Remove Contact from List Remove Contact from Cordial List Add Product to Cart type = “track” and event = “Product Added” Add product to Cordial contact cart Remove Product from Cart type = “track” and event = “Product Removed” Removes product from Cordial contact cart Upsert Order event = “Order Completed” or event = “Order Updated” or event = “Order Refunded” or event = “Order Cancelled” Upserts order to Cordial Merge Contacts type = “alias” Merge contacts in Cordial. Cordial’s destination supports several actions that map to different parts of the Cordial platform: Contact management Upsert Contact: Creates or updates a contact in Cordial. Maps to identify calls by default—when you identify someone, their contact record in Cordial gets created or updated with the traits you send. Cordial matches contacts by email or a configurable unique identifier. Merge Contacts: Merges two contact records in Cordial. Use this when you need to combine duplicate contacts—for example, when an anonymous visitor later identifies themselves with an email address. List management Add Contact to List: Adds a contact to a Cordial list. Useful for managing marketing segments and subscription groups. Remove Contact from List: Removes a contact from a Cordial list. Activity tracking Create Contact Activity: Records a custom activity (event) on a contact in Cordial. Maps to track calls by default. These activities can trigger automations in Cordial’s messaging platform. E-commerce Add Product to Cart: Adds a product to a contact’s cart in Cordial. Use this for abandoned cart tracking and recovery campaigns. Remove Product from Cart: Removes a product from a contact’s cart. Upsert Order: Creates or updates an order record in Cordial. Use this for post-purchase campaigns and order status tracking. Data mapping Contact identifiers Cordial identifies contacts primarily by email address. When you send identify calls, the integration uses the email trait to match existing contacts or create new ones. If you’ve configured a Segment ID Key in your destination settings, the integration also stores your Customer.io user ID as a custom attribute on the contact. This gives you a secondary identifier for matching contacts across systems. Passing custom attributes You can send any traits from your identify calls as custom attributes on Cordial contacts. Define attributes in your Cordial account first—Cordial silently ignores attributes it doesn’t recognize. Things to know Define attributes in Cordial before sending data. Cordial silently drops unrecognized attributes rather than creating them automatically. Define your custom attributes in Cordial’s platform before configuring your data mappings. The API endpoint varies by data center. Cordial operates multiple data centers. Make sure you use the endpoint matching your Cordial account’s data center. The default is https://integrations-ingest-svc.usw1.cordial.com. Activity tracking powers Cordial automations. Events you send via the Create Contact Activity action can trigger Cordial campaigns and automations—think of them like Customer.io events but inside Cordial’s ecosystem. E-commerce actions enable cart and order tracking. If you use Cordial for abandoned cart emails or post-purchase sequences, the cart and order actions keep Cordial’s e-commerce data in sync with your source. --- ## Criteo Audiences URL: https://docs.customer.io/integrations/data-out/connections/criteo-audiences/ Getting started Before you can setup your Criteo Audiences destination, you need to create a Criteo API Marketing Solutions app to generate your app credentials. Follow Criteo’s Developer Portal checklist to generate your credentials. You also need your Criteo Advertiser ID. Talk to your Criteo Account Strategist if you don’t know your Advertiser ID. Go to Data & Integrations > Integrations and select the Criteo Audiences entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Id: Your Criteo API client ID Client Secret: Your Criteo API client secret Advertiser Id: Your Criteo Advertiser ID Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Add users to Audience type = “track” and event = “Audience Entered” Add users from Criteo audience by connecting to Criteo API Remove users from Audience type = “track” and event = “Audience Exited” Remove users from Criteo audience by connecting to Criteo API --- ## Customer.io URL: https://docs.customer.io/integrations/data-out/connections/customerio/ Getting started Go to Data & Integrations > Integrations and select the Customer.io entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Site Id: Customer.io site ID. This can be found on your API Credentials page. Api Key: Customer.io API key. This can be found on your API Credentials page. Account Region: Learn about Account Regions. Endpoint: The URL of the Customer.io API Click Enable Destination. Actions  Be careful when editing actions for your workspace The Customer.io integration is how data gets into Customer.io—the people, events, and other data that you’ll use to send messages to your audience. It’s already configured with actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. to support incoming data. You can edit these actions if you want to change how we handle incoming data, but take care not to inadvertently break integrations with Customer.io. When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. Action Default Trigger Description Create or Update Device event = “Application Installed” or event = “Device Created or Updated” or event = “Application Opened” Create or update a person's device. Delete Device event = “Application Uninstalled” or event = “Device Deleted” Delete a person's device. Delete Relationship event = “Relationship Deleted” Delete a relationship between a person and an object in Customer.io Delete Person event = “User Deleted” Delete a person in Customer.io. Delete Object event = “Object Deleted” Delete an object in Customer.io. Create or Update Person type = “identify” Create a person in Customer.io or update them if they exist. Track Event type = "track" and event != "Relationship Deleted" and event != "User Deleted" and event != "User Suppressed" and event != "User Unsuppressed" and event != "Object Deleted" and event != "Report Delivery Event" and event != "Device Created or Updated" event != "Device Deleted" Track an event for a known or anonymous person. Track Page View type = “page” Track a page view for a known or anonymous person. Track Screen View type = “screen” Track a screen view for a known or anonymous person. Create or Update Object type = “group” Create an object in Customer.io or update them if they exist. Merge People type = “alias” Merge two customer profiles together. Suppress Person event = “User Suppressed” Track a "User Suppressed" event to suppress a person. Unsuppress Person event = “User Unsuppressed” Track a "User Unsuppressed" event to suppress a person. Report Delivery Event event = “Report Delivery Event” Report delivery metrics for a message sent from the Customer.io Journeys product. Report Content Event event = “Report Content Event” Report a Viewed or Clicked Content event. Create or update a person (identify) Incoming identify calls add and update people in your workspace. We map traitsInformation that you know about a person, captured from identify events in Data Pipelines. Traits are analogous to attributes in Customer.io Journeys. in source calls to attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for people in your workspace. These are things you know about a person, like their first name, their interests, etc. By default, we send this event when you identify a person—like when they submit their email address on your website requesting more information, or when they create an account with you. You’ll typically set relationships between a person and an object and information about the relationship with the group call. But you can also send this information in an identify call if you want. When you relate a person to an object through an identify call, you’ll place the group information in the context object. { "type": "identify", "traits": { "first_name": "cool", "last_name": "person", "email": "cool.person@example.com", "plan": "premium", // include if you want to set relationships/attributes "objectId": "math101-2024", "objectTypeId": "2", "relationshipAttributes": { "dues_paid": true, "class_complete": true, "grade": "A-" } }, "userId": "97980cfea0067", "created_at": "1679407797", } Add and update people by ID or email In Customer.io, you can identify people by an id or an email address, which provides a way to work with people if they provide an email address but haven’t generated a backend ID (like when someone signs up as a lead but hasn’t yet become a paying customer). When you send identify events to this destination, they’ll add or update people in Customer.io depending whether or not the corresponding email or ID exists. If an identify request contains either a userId or an email trait, and that person doesn’t already exist, we’ll create a person in Customer.io If an identify request contains either a userId or an email trait, and that person already exists, we’ll update the person in Customer.io If an identify request contains both a userId and an email trait, but one of those values wasn’t associated with the person, we’ll add the new identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. to the person and update their profile. flowchart LR a(incoming identify event)-->b{Does it have email or ID} b-->|yes|c{Does email or ID exist?} b-.->|no|d(Not sent to destination) c-->|yes|e(Update person) c-->|no|f(Add new person) Customer.io Journeys doesn’t support anonymous identify calls yet We’re working on support for anonymous profiles, but, for now, Journeys ignores anonymous identify calls. This doesn’t mean you shouldn’t send anonymous identify calls—even if Journeys is your only destination. Anonymous still has value if: You send data to other, non-Customer.io destinations, many of which support anonymous profiles. You use our JavaScript source. If you use our JavaScript source, anonymous identify calls store traits in local storage and attach them to subsequent identify calls. This means that when you formally identify a person later, you won’t need to capture all of a customer’s traits at the same time; the JavaScript source will automatically associate their anonymously gathered traits with their userId. Delete person You’ll delete people in Customer.io Journeys by sending a track call called User Deleted. You can always change the name of the event in the Actions tab of your destination settings. Deleting a person doesn’t necessarily require a userId. You can delete a person by their email address, for example. If you don’t include a userId, we can delete a profile by properties.email. If you include both, we’ll use the userId. { "type": "track", "event": "User Deleted", "userId": "97980cfea0067", "timestamp": "1679407797", "properties": { "reason": "user requested deletion" } } Create or update an object (group) In Customer.io Journeys, we refer to groups as objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.—non-people entities like accounts, online classes, or recreational leagues. The group call does two things: It creates or updates an object, like an identify call does for people. If you include a userId, it also relates a person to the object—including any relationship attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. you pass in the call. Note the objectTypeId field: objects in Customer.io Journeys have a type—an incrementing number beginning at 1. This lets you have different kinds of objects; if you run an edtech company, for example, you might have objects for online classes, departments of faculty members, clubs or study groups, and so on. If you don’t pass traits.object_type_id (or objectTypeId), we assume that the value is 1. Like people, objects and their relationships with people can also have traits—things you know about the object, like its name, industry, and so on. A relationship might have things you know about the relationship between the group and a person, like a person’s position in a company, whether they’re a primary contact for a company, and so on. Items in the traits object apply to the object itself—except for the relationshipAttributes key. Anything you put in the traits.relationshipAttributes object applies to the relationship between a person and the object. { "messageId": "4vl6zh", "timestamp": "2022-11-24T22:56:14.144Z", "type": "group", "traits": { "class_code": "cio101", "class_name": "Customer.io Basics", "start_date": 1679410730, "object_type_id": 1, "relationshipAttributes": { "type": "student", "dues_paid": true, "class_completed": false } }, "groupId": "cio101-spring2024", "userId": "97980cfea0067" } Object and relationship attributes Both objects and people have their own attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. But relationships can have attributes too! In the way that attributes on a person describe the person and attributes on an object describe the object, attributes on a relationship describe the relationship between the person and the object. In the example above, our object is an online class and we set relationship traits (attributes) for a student. We could just as easily define relationship traits for a teacher, a teaching assistant, and so on—anyone who might be related to the class. These attributes help you send campaigns that are relevant to the relationship between a person and an object. Delete relationship In Journeys, people are related to groups (called objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) You can relate people to one or more groups. In some cases, you may want to remove a relationship—like when someone finishes an online class or leaves an organization. You can do this with a track call called Relationship Deleted. Your call must include: properties.objectId: the ID of the object a person is related to. properties.objectTypeId: the type of object the person is related to (defaults to 1). either userId or properties.email: the ID or email of the person you’re removing the relationship from. If you include both values, we’ll use the userId. { "messageId": "4vl6zh", "timestamp": "2022-11-24T22:56:14.144Z", "type": "track", "event": "Relationship Deleted", "properties": { "objectId": "example-company", "objectTypeId": 2 }, "userId": "97980cfea0067" } Delete object In Journeys, you can delete a group (called an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.) with a track call called Object Deleted. Deleting an object also removes all relationships between people and the object. { "messageId": "4vl6zh", "timestamp": "2022-11-24T22:56:14.144Z", "type": "track", "event": "Object Deleted", "properties": { "objectId": "example-company", "objectTypeId": 2 }, "userId": "97980cfea0067" } Create or update a device When a person uses your mobile app or mobile website, you can capture and send device information to Customer.io. We capture this information with two specific events: Application Installed and Application Opened. These represent the times when you’ll typically register for a token and associate a device with a person. { "messageId": "i6yatl", "timestamp": "2022-11-24T22:48:17.593Z", "type": "track", "email": "cool.person@example.com", "properties": { "device_id": "cEFolG5uRDGPizsaUh4X4m:APA91bFsKwjW9zUaiuJMCGqa3fLRiJ8fv1riQ6U-1iI72aSFZx5wxiqGnqbL-S2g3kZ5RrRUUo3AAVh_LhpFbjDlSqEoC4rqHmH8Z1BGgqaD9jStahy63cGXueW-vOr6mTOekmXgWSww" }, "userId": "97980cfea0067", "event": "Application Installed" } Delete device We automatically remove a device when we register an event called Application Uninstalled. Our SDK automatically sends this event when people uninstall your app. However, device tokens are transient tokens can expire based on your push provider, and you’ll likely register for a new device token whenever someone closes and re-opens your app. We automatically prune devices from our messaging platform when tokens are invalidated, but we don’t do this as a part of event subscriptions. You may want to send additional calls to delete previous devices as we prune them from your Journeys tab. { "messageId": "i6yatl", "timestamp": "2022-11-24T22:48:17.593Z", "type": "track", "email": "cool.person@example.com", "properties": { "device_id": "long-device-token" }, "userId": "97980cfea0067", "event": "Application Uninstalled" } Track event The track call represents an activity someone performs—typically in your app or on your website. If you send this event and the person you reference in the event doesn’t exist, we’ll create them. You’ll notice that the track event excludes several event names. The Customer.io Journeys API contains methods and endpoints that don’t map naturally to other source calls—like deleting people, suppressing people, and so on. We’ve mapped these calls to track events with specific names. You can always change the name of the event in the Actions tab of your destination settings. Events that don’t match the defined names act as standard track events in Customer.io. { "messageId": "i6yatl", "timestamp": "2022-11-24T22:48:17.593Z", "type": "track", "email": "cool.person@example.com", "properties": { "class_name": "Customer.io Basics", "class_code": "cio101", "start_date": 1679410730 }, "userId": "97980cfea0067", "event": "enrolled" } Page views and screens There are two special kinds of track events: page and screen calls. These represent the pages people visit on your website and the screens people visit in your mobile app respectively. You can use these events to monitor the places people do and don’t visit in your website or app. You might also use page and screen calls to follow up with people, to see if they were still interested in a particular product, online class, and so on. page event page event { "messageId": "efxqsi", "timestamp": "2022-11-24T22:55:59.498Z", "type": "page", "email": "cool.person@example.com", "properties": { "session_started": 1679410730, "url": "https://www.example.com" }, "userId": "97980cfea0067", "name": "home" } screen event screen event { "messageId": "9zk6c", "timestamp": "2023-03-20T22:56:06.259Z", "type": "screen", "email": "cool.person@example.com", "properties": { "session_started": 1679410730 }, "userId": "97980cfea0067", "name": "home" } Merge people Customer.io lets you merge profiles. If you use your JavaScript SDK, we’ll automatically merge anonymous profiles with their identified counterparts when you identify them. But for any other case—if you’re not using our JavaScript SDK or you want to merge two non-anonymous profiles—you’ll want to set up a Merge People action. This action uses on the alias call from your sources. You’ll set the userId and previousId values in the alias call, where: userId is the ID of the profile that you want to remain in customer.io. If this profile doesn’t exist, the request won’t do anything. previousId is the ID of the profile that you want to merge into the userId profile and remove. The userId inherits attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. from the previousId that are not already set on the userId; if the userId and the previousId both have an attribute, the userId takes precedence. The userId also inherits events performed by the anonymousId. Note that events merged from the previousId person cannot trigger campaigns. { "previousId": "97980cfea0067", "id": "cool_person" } Suppress and unsuppress people In Journeys, suppressing a person deletes their profile and prevents you from reusing their ID or email—whichever identifier you pass in the event. You can suppress or unsuppress users by sending events called User Suppressed and User Unsuppressed respectively. In most cases, you’ll want to delete people without suppressing them. Suppressing people is typically a last resort—along the lines of GDPR “right to be forgotten” requests. { "messageId": "4vl6zh", "timestamp": "2022-11-24T22:56:14.144Z", "type": "track", "event": "User Suppressed", "userId": "97980cfea0067" } Report content event Premium This feature is available for Premium plans. The “Report content event” action tracks views and clicks for anonymous messages.  Do not edit this action This action automatically reports metrics to your Customer.io workspace. It’s not a standard event that you can use to trigger campaigns, etc. If you edit this action, you could potentially break the way we report metrics for your anonymous in-app messages. Only premium and enterprise customers have access to anonymous in-app messages. Go to your account settings to learn about upgrading! Conversion timestamps If you want to convert ISO-8601 strings to timestamps, set Convert Timestamps when setting up actions. This converts attributes containing ISO-8601 strings (like 2023-05-18T22:58:45+00:00) to Unix timestamps (like 1684475925). We’ll only convert valid ISO-8601 date-times down to millisecond precision. If you store your timestamps in another format, or store timestamps using nanosecond precision, we won’t convert them. 2025-08-13T15:45:17.451Z ✅ (milliseconds - will convert) 2025-08-13T02:08:40.1418726Z ❌ (nanoseconds - won’t convert) --- ## Facebook Conversions API URL: https://docs.customer.io/integrations/data-out/connections/facebook-conversions-api/ Getting started To set up the Facebook Conversions API destination, you’ll need a Facebook user access token from your business account. See the section below for help creating or getting a token. Go to Data & Integrations > Integrations and select the Facebook Conversions API entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Pixel Id: Your Facebook Pixel ID. Note: You may also use a dataset ID here if you have configured a dataset in your Facebook Events Manager. System User Access Token: The access token for the system user, obtained in the Facebook Business account. Learn more about obtaining the access token Test Event Code: Use this field to specify that events should be test events rather than actual traffic. You can find your Test Event Code in your Facebook Events Manager under the "Test events" tab. You'll want to remove your Test Event Code when sending real traffic through this integration. Click Enable Destination. Get your Facebook system user access token To send offline conversions with a system user access token, you must first create a system user in Facebook. You can create a system user in the Business Settings section of your Facebook Business Manager account. Go to your Facebook Business account. Go to Business Settings > Users > System users. Click Generate New Token, select your app from the dropdown, and select the ads_management permission. Click Generate Token Copy this token to the System User Access Token field in your destination settings. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Purchase type = “track” and event = “Order Completed” Send event when a user completes a purchase Initiate Checkout type = “track” and event = “Checkout Started” Send event when a user enters the checkout flow Add to Cart type = “track” and event = “Product Added” Send event when a user adds a product to the shopping cart View Content type = “track” and event = “Product Viewed” Send event when a user views content or a product Search type = “track” and event = “Products Searched” Send event when a user searches content or products Page View type = “page” Send a page view event when a user lands on a page Custom Event Send a custom event Data Structure You’ll need to set up the data structure you will send with each action. On an action page, locate the section titled Data Structure. The first field you need to map is called Action Source. The Action Source takes a variety of values, like app or website (Scroll down to action_source for options). Each source requires a different set of parameters for you to send data to Facebook’s Conversion API. For instance, if your action source is app, you’ll need to configure these fields. You can find required fields for all sources in Facebook’s Conversion API docs. FAQ & Troubleshooting Other Standard Events If you want to send Facebook standard events that we don’t have ready-made actions for, you can use the Custom Event action. For example, if you want to send a CompleteRegistration event, you would create a custom event action, set up your trigger criteria for completed registrations, and enter “CompleteRegistration” as the Event Name. When you set up custom actions, you can add fields you expect to send in events, like content_name and currency. Mapped Data and Automatically Hashed Fields (PII) We automatically map user data fields to their corresponding parameters as expected by the Conversions API before we send data to Facebook. Facebook uses shorthands for these fields in their API. We use the full names for clarity, but we map to the appropriate fields in Facebook. For example we use Email to represent em in Facebook. Facebook (Meta) requires that you hash personally identifiable information (PII) before sending it to the Conversions API. We automatically hash appropriate data for you before we send data to Facebook. Customer.io Data Field Facebook Conversions API Parameter Automatically Hashed External ID external_id ✅ Email em ✅ Phone ph ✅ Gender ge ✅ Date of Birth db ✅ Last Name ln ✅ First Name fn ✅ City ct ✅ State st ✅ Zip Code zp ✅ client_ip_address client_ip_address client_user_agent client_user_agent fbc fbc fbp fbp  We automatically hash the External ID Facebook recommends that you hash external IDs, so that external IDs match across Facebook Pixel and Facebook Conversions API if you use external IDs for deduplication. Server Event Parameter Requirements Facebook requires the action_source event parameter, which specifies where conversions occur, for all events sent to the Facebook Conversions API. If action_source is set to website, then the client_user_agent and the event_source_url parameters are also required. Events sent to the Conversions API that don’t meet this requirement may not be available for optimization, targeting, or measurement. Verify Events in Facebook When you start sending events, you should start seeing them within twenty minutes. To confirm that Facebook received your events: Go to the Events Manager. Click the pixel corresponding to your conversion events. In the Overview tab, look for events where the Connection Method is Server. --- ## Friendbuy URL: https://docs.customer.io/integrations/data-out/connections/friendbuy/ There are two versions of this integration You’ll see two entries for Friendbuy in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the Friendbuy entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Auth Key: Contact your Friendbuy account manager to generate your Friendbuy MAPI key and secret. Auth Secret: See Friendbuy MAPI Key. Click Enable Destination. Cloud mode actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Customer Create a new customer profile or update an existing customer profile. Track Purchase Record when a customer makes a purchase. Track Sign Up Record when a customer signs up for a service. Track Custom Event Record when a customer completes any custom event that you define. Getting started: web integration Go to Data & Integrations > Integrations and select the Friendbuy entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Merchant Id: Find your Friendbuy Merchant ID by logging in to your Friendbuy account and going to Developer Center > Friendbuy Code. Click Enable Destination. Web destination actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Customer type = “identify” Create a new customer profile or update an existing customer profile. Track Purchase event = “Order Completed” Record when a customer makes a purchase. Track Sign Up event = “Signed Up” Record when a customer signs up for a service. Track Page type = “page” Record when a customer visits a new page. Allow Friendbuy widget targeting by Page Name instead of URL. Track Custom Event Record when a customer completes any custom event that you define. --- ## FullStory URL: https://docs.customer.io/integrations/data-out/connections/fullstory/ Getting started Go to Data & Integrations > Integrations and select the FullStory entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: FullStory API key Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Track events Identify User type = “identify” Sets user identity variables Track Event V2 type = “track” Track events V2. Identify User V2 type = “identify” Sets user identity variables. Creates a new FullStory user if no user matching the given uid is found. --- ## Gainsight PX URL: https://docs.customer.io/integrations/data-out/connections/gainsight-px-cloud-action/ Getting started Go to Data & Integrations > Integrations and select the Gainsight PX entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Gainsight PX API key. You can find this key in the "Administration/Products" screen. Data Center: The PX data center where your PX subscription is hosted. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send Event Send entire event payload to Gainsight PX --- ## Google Ad Conversions URL: https://docs.customer.io/integrations/data-out/connections/google-enhanced-conversions/ Getting started Go to Data & Integrations > Integrations and select the Google Ad Conversions entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Customer Id: ID of your Google Ads Account. This should be 10-digits and in XXX-XXX-XXXX format. Required if you are using a mapping that sends data to the Google Ads API. Manager Customer Id: When using a manager account and are accessing a related customer account, this is the ID of the manager account. Click Enable Destination. Actions When you’re done setting up your destination, you can go to the Actions tab to see how we map source events to your destination. You may need to add actions for this destination While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your destination. See our actions page for help setting up actions. Action Default Trigger Description Upload Enhanced Conversion (Legacy) no default Upload a conversion enhancement to the legacy Google Enhanced Conversions API. Upload Click Conversion no default Upload an offline click conversion to the Google Ads API. Upload Call Conversion no default Upload an offline call conversion to the Google Ads API. Upload Conversion Adjustment no default Upload a conversion adjustment to the Google Ads API. Enhanced Conversions Google’s Enhanced Conversions feature can improve the accuracy of conversion measurements. It supplements your existing conversion tags by sending hashed, first-party conversion data from your website to Google in a privacy-safe way. You can use the Upload Conversion Adjustment action to send enhancements to the Google Ads API. But, to send enhanced conversions, you must record first conversions using the standard Google Ads Conversion tag (Gtag). You can use our Google Ads (Gtag) destination so you can use your existing sources to activate Gtag. Then you can send enhancements to web conversion actions that have Turn on enhanced conversions enabled. You can’t use the enhanced conversions feature with conversions that you track in other ways. For example, you can’t use enhancement features with goals that you import from Google Analytics.  To send enhancements for conversions that you initially track with Gtag, you need to implement an Order ID (Transaction ID) in Gtag and send the same Order ID with the corresponding enhancement data. Enhanced Conversions for Leads Google’s Enhanced Conversions for Leads feature lets you use hashed, user-provided data from your website’s lead forms for offline lead measurement. When you upload your leads, Google uses the hashed information to attribute activity back to the Google Ad campaign. To send enhanced conversions for leads, you can use the Upload Click Conversion action. Instead of sending GCLID, send an email address or phone number. Customer.io will hash that data before sending calls to Google Ads. Refreshing Access Tokens When you use OAuth to authenticate this destination, we store an access token and a refresh token. Access tokens for Google Ad Conversions expire after one hour. When they expire, we receive an error and then we use the refresh token to fetch a new access token. This results in two API requests to Google Ad Conversions, one failure and one success. Because of the reauthorization flow, you may see a warning in Google for unprocessed conversions due to incorrect or missing OAuth credentials. This warning does not indicate data loss. Google has confirmed that they continue processing conversions, and that this OAuth retry behavior will not cause any issues for your web conversions. Whenever possible, we cache access tokens to reduce the total number of requests made to Google Ad Conversions. Consent Management Beginning March 6, 2024, Google Ads requires your users’ consent to collect their data and personalize ads in conformance with the Digital Markets Act. When you set up the Upload Click Conversion or Upload Call Conversion actions, Google includes two different fields for consent. Users must consent to both for Google Ads to accept the conversion: User Data Ad Personalization If you don’t have customers in the European Economic Area (EEA), or you do not need to follow EEA regulations, you can simply set these fields to Granted. If you have customers in the EEA, and you gather consent by way of a terms of service (TOS) or store consent outside of conversion events, you can set these fields to hardcode a GRANTED value. Otherwise, you’ll need to gather consent from your customers and map it to a value in your source events. For example, if your source events have a consent property, you can map that property to consent fields in your destination. You must gather consent as GRANTED, UNKNOWN or DENIED. GRANTED: results in a successful upload of the conversion DENIED or UNKNOWN: prevents successful upload, respecting your audience’s right to privacy Troubleshooting EC_MODE_MISMATCH errors If you see an EC_MODE_MISMATCH error, it means your conversion action has the incorrect conversion type in Google Ads. Change the conversion type to API in Google Ads Platform under Tools > Conversions > Conversion Event. This segment contains only people who either: (a) are outside the European Economic Area, or (b) have consented to share their data with Google. --- ## Google Ads (Gtag) URL: https://docs.customer.io/integrations/data-out/connections/google-ads/  Don’t use this destination if you use Google Tag Manager You should only use this destination if your Google Ads account is using Gtag. If you’re using Google Tag Manager, don’t add the global site tag (gtag.js) in your GTM containers, otherwise you’ll end up duplicating Gtag on your site—once through your Customer.io integration and again through Google Tag Manager. Getting started This destination maps .page() and .track() calls from your website or app to Page Load Conversions and Click Conversions respectively.  Migrating from Segment? If you’re moving your integration from Segment, you won’t see as many settings when you set up this destination. We’ve pushed many of the settings you set in Segment down to the individual actions that they affect. This makes it easier to use our defaults, and gives you granular control of settings at the action level. Go to Data & Integrations > Integrations and select the Google Ads (Gtag) entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Account ID: Enter your GOOGLE-CONVERSION-ID. You can get this value from your global site tag snippet. It should look something like AW-901243031 Floodlight Account ID: Enter your DoubleClick Floodlight Advertiser ID to have it passed to Gtag's 'config' parameter. This ensures tags are loaded by first-party cookies. This value should look like DC-1234567. For more information, see Google's documentation. Disable Ad Personalization: Disable ad personalization. For more information, see Google's documentation. Click Enable Destination. After you set up your integration, you should check the Actions tab and map conversion labels if you track different kinds of conversions. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Custom Event type = “track” and event != “Order Completed” Track an event for a user Track Order Completed type = “track” and event = “Order Completed” Track an event for a user Track Page View type = “page” Track the current page Conversion Labels Actions for this destination include fields for Page Load Conversion Label and Click Event Conversion Label for page and track events respectively. If you use the same conversion for all page or all click events, you can set a static value. Otherwise, you might want to pass the label as a property in your events, so you can measure conversions against the appropriate labels in Google Ads. For example, I might pass a conversion label in an event as follows and then use it in my page conversion action as $.properties.conversion_label. cioanalytics.page({ conversion_label: "AbC-D_efG-h12_34-567" }) Page load conversions If you want to map all your unnamed .page() calls to a default Page Load Conversion, you can enter the conversion ID as a static value for your Page Load Conversion Label in the default Page action. But, if you created specific Page Load Conversions in Google Ads you can pass that value as an event property and pass it as a variable and set the variable (e.g. $.properties.conversion_id) as the Page Load Conversion Label. We forward all the properties in the page, like the path, title and url, so that they’re available in Google Ads (Gtag) for your remarketing campaigns. You can also send semantic properties, like value, currency, or order_id, as options in the integrations object of your API calls. In general, we recommend that you use the Click Conversion action for these kinds of properties instead, and map them to .track() calls. But, if you want to send them as a part of page events, you’d pass them like this: cioanalytics.page({}, { 'google-ads': { value: 25, currency: 'USD', order_id: 'order123' } }); Track calls and click conversions You can map your custom .track() events to Click Conversions that you created inside Google Ads. We pass event properties from your source calls to Google Ads so you can use them in remarketing campaigns. If you pass properties.value, properties.currency, or properties.order_id, Segment maps them to Google’s semantic value, currency, or transaction_id properties respectively. The only exception is that for Order Completed events, we map Google’s semantic value field to your properties.revenue or properties.total. If you pass both as properties, properties.revenue takes precedence. Multiple Google Ads Accounts If you have multiple Google Ads Gtag accounts (usually managed by various third party agencies) simply pass the conversion ID as a variable in your page and track events and map it to the Conversion Account ID fields in your actions. This field lets you override the default Account Id that you set when you configure your destination. Troubleshooting Conversions If you don’t see the correct events in Google Ads, you can take the following steps to trace incoming events to their destination and make sure that everything’s set up properly. Confirm that the events mapped to a Google Ads conversion are actually sent from Analytics.js: Go to Integrations in your workspace and click your integration. Go to the Data In tab. Click an event and make sure that the library name is analytics.js. Your events should include a snippet that looks like this: "library": { "name": "analytics.js", } Verify that the Google Conversion ID in your Google Ads destination settings is correct. Find your ad online and click on it. This redirects you to your website. Open the Network tab in your browser and select the setting to preserve logs. In Chrome, this is a simple checkbox called Preserve log; in Firefox, you’ll click the settings icon and select Persist Logs. Keep this Network tab and webpage open. In a new browser tab, go to the Actions tab for your Google Ads destination and open settings for the Click Conversions action. Make sure the events are mapped to the correct Conversion Label. Go back to your website and trigger the event that’s mapped to the click conversion action. Go to the Network tab in your browser and enter the Conversion Label linked to the event you triggered in the Filter field. See if the value for the ct_cookie_present changed to true. If true, it means that Google Ads counted the event as a conversion. --- ## Google Analytics URL: https://docs.customer.io/integrations/data-out/connections/google-analytics-4/ Getting started Go to the Data Pipelines tab and click Connections. Click Add New under Destinations. Select the Google Analytics destination. (Optional) Select the sources that you want to connect to this destination. You can always connect sources to your destination later. We’ll only show you sources that you can use with your destination. Configure your destination. Measurement Id: The Measurement ID associated with a stream. Found in the Google Analytics UI under: Admin > Data Streams > choose your stream > Measurement ID. Required for web streams. Firebase App Id: The Firebase App ID associated with the Firebase app. Found in the Firebase console under: Project Settings > General > Your Apps > App ID. Required for mobile app streams. Api Secret: An API SECRET generated in the Google Analytics UI, navigate to: Admin > Data Streams > choose your stream > Measurement Protocol > Create Click Enable Destination. When you’re done, you’ll need to add actions. While we have a number of out-of-the-box actions for Google Analytics, we don’t enable any actions by default. Actions When you’re done setting up your integration, you can go to the Actions tab to see how we map incoming data to your outbound integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your destination. See our actions page for help setting up actions. Action Default Trigger Description Purchase type = “track” and event = “Order Completed” Send event when a user completes a purchase Add to Cart type = “track” and event = “Product Added” Send event when a user adds items to a cart Page View type = “page” Send page view when a user views a page Custom Event type = “track” Send any custom event Select Item type = “track” and event = “Product Clicked” Send event when a user selects an item from a list Begin Checkout type = “track” and event = “Checkout Started” Send event when a user begins checkout Select Promotion type = “track” and event = “Promotion Clicked” Send event when a user selects a promotion View Item type = “track” and event = “Product Viewed” Send event when a user views an item Remove from Cart type = “track” and event = “Product Removed” Send event when a user removes items from a cart View Cart type = “track” and event = “Cart Viewed” Send event when a user views their cart Search type = “track” and event = “Products Searched” Send event when a user searches your content View Item List type = “track” and event = “Product List Viewed” Send event when a user views a list of items or offerings Sign Up type = “track” and event = “Signed Up” Send event when a user signs up to measure the popularity of each sign-up method View Promotion type = “track” and event = “Promotion Viewed” Send event when a promotion is shown to a user Add Payment Info type = “track” and event = “Payment Info Entered” Send event when a user submits their payment information Refund type = “track” and event = “Order Refunded” Send event when a refund is issued Login type = “track” and event = “Signed In” Send event when a user logs in Generate Lead type = “track” Send event when a user submits a form or request for information Add to Wishlist type = “track” and event = “Product Added to Wishlist” Send event when a user adds items to a wishlist Learn about managing consent for data collection and ad personalization below. Getting started: web integration Go to the Data Pipelines tab and click Connections. Click Add New under Destinations. Select the Google Analytics destination. (Optional) Select the sources that you want to connect to this destination. You can always connect sources to your destination later. We’ll only show you sources that you can use with your destination. Configure your destination. Measurement ID: The measurement ID associated with the web stream. Found in the Google Analytics UI under: Admin > Data Streams > Web > Measurement ID. Page View: Set to false to prevent the default snippet from sending page views. Enabled by default. Allow Google Signals: Set to false to disable all advertising features. Set to true by default. Allow Ad Personalization Signals: Set to false to disable all advertising features. Set to true by default. Cookie Domain: Specifies the domain used to store the analytics cookie. Set to “auto” by default. Cookie Expiration In Seconds: Every time a hit is sent to GA4, the analytics cookie expiration time is updated to be the current time plus the value of this field. The default value is two years (63072000 seconds). Please input the expiration value in seconds. More information in Google Documentation Cookie Flags: Appends additional flags to the analytics cookie. See write a new cookie for some examples of flags to set. Cookie Path: Specifies the subpath used to store the analytics cookie. We recommend to add a forward slash, / , in the first field as it is the Default Value for GA4. Cookie Prefix: Specifies a prefix to prepend to the analytics cookie name. Cookie Update: Set to false to not update cookies on each page load. This has the effect of cookie expiration being relative to the first time a user visited. Set to true by default so update cookies on each page load. Enable Consent Mode: Set to true to enable Google’s Consent Mode. Set to false by default. Default Ads Storage Consent State: The default value for ad cookies consent state. This is only used if Enable Consent Mode is on. Set to “granted” if it is not explicitly set. Consent state can be updated for each user in the Set Configuration Fields action. Ad User Data Consent State: Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on. Ad Personalization Consent State: Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on. Default Analytics Storage Consent State: The default value for analytics cookies consent state. This is only used if Enable Consent Mode is on. Set to “granted” if it is not explicitly set. Consent state can be updated for each user in the Set Configuration Fields action. Wait Time To Update Consent Stage: If your CMP loads asynchronously, it might not always run before the Google tag. To handle such situations, specify a millisecond value to control how long to wait before the consent state update is sent. Please input the wait_for_update in milliseconds. Click Enable Destination. Web integration actions When you’re done setting up your integration, you can go to the Actions tab to see how we map incoming data to your outbound integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your destination. See our actions page for help setting up actions. Action Default Trigger Description Add Payment Info type = “track” and event = “Payment Info Entered” Send event when a user submits their payment information Login type = “track” and event = “Signed In” Send event when a user logs in Sign Up type = “track” and event = “Signed Up” The method used for sign up. Search type = “track” and event = “Products Searched” The term that was searched for. Add to Cart type = “track” and event = “Product Added” This event signifies that an item was added to a cart for purchase. Add to Wishlist type = “track” and event = “Product Added to Wishlist” The event signifies that an item was added to a wishlist. Use this event to identify popular gift items in your app. Remove from Cart type = “track” and event = “Product Removed” This event signifies that an item was removed from a cart. Select Item type = “track” and event = “Product Clicked” This event signifies an item was selected from a list. Select Promotion type = “track” and event = “Promotion Clicked” This event signifies a promotion was selected from a list. View Item type = “track” and event = “Product Viewed” This event signifies that some content was shown to the user. Use this event to discover the most popular items viewed. View Promotion type = “track” This event signifies a promotion was viewed from a list. Begin Checkout type = “track” and event = “Checkout Started” This event signifies that a user has begun a checkout. Purchase type = “track” and event = “Order Completed” This event signifies when one or more items is purchased by a user. Refund type = “track” and event = “Order Refunded” This event signifies when one or more items is refunded to a user. View Cart type = “track” and event = “Cart Viewed” This event signifies that a user viewed their cart. View Item List type = “track” and event = “Promotion Viewed” Log this event when the user has been presented with a list of items of a certain category. Generate Lead type = “track” Log this event when a lead has been generated to understand the efficacy of your re-engagement campaigns. Custom Event type = “track” Send any custom event Set Configuration Fields type = “identify” or type = “page” Set custom values for the GA4 configuration fields. Learn about managing consent for data collection and ad personalization below. User Identification Your source calls must include a client ID or Firebase App Instance ID to send data to Google Analytics 4. The client ID is the web equivalent of a device identifier and uniquely identifies a given user instance of a web client. By default, we set the *client ID to the our userId, falling back on anonymousId. You can change this mapping in your actions if you use a different field for the client ID. The Firebase App Instance ID is the mobile equivalent of a device identifier and uniquely identifiers a given Firebase app instance. We don’t set a default for the Firebase App Instance ID, because you need to retrieve this value through the Firebase SDK. You can also send a User-ID—not to be confused with our userId—with your events. The User-ID lets you associate your own identifiers with individual users so you can connect their behavior across different sessions on various devices and platforms. For more information on user IDs, see Google’s Measure activity across platforms. Sending a User-ID can be helpful in some situations. For example, if you integrate with Google Analytics 4 client-side (either with our Google Analytics 4 integration or outside of Customer.io) and also use our Google Analytics 4 integration to send events through the API, you might want to take the following approach: Use the Gtag-generated client ID. Gtag sets a client ID automatically. It stores the client ID in a cookie and can be fetched on the web client. To tie data between client-side and server-side integrations, you’ll pass the client ID to Customer.io as a property and use that value as the client ID in actions for your Google Analytics 4 integration. Use Customer.io’s userId for User-ID. Our userId should be your canonical user identifier. By setting Google’s User-ID to the userId, client-side and server-side, you’ll benefit from cross-platform analytics. If you use Firebase to send mobile data to Google Analytics 4, using the same User-ID across web and mobile also ensures that your users are stitched together across devices. Recommended events Google Analytics 4 has recommended events and properties that power built-in reports. Our Google Analytics 4 integration provides prebuilt actions that automatically map incoming events using our ecommerce specification to the corresponding Google Analytics 4 events and properties. If your source events don’t follow the ecommerce spec exactly, you can change the way we map properties to actions. For example, we map “Order Completed” events to the Google Analytics 4 “Purchase” event by default. If your company uses a different name for purchase events, like “Order Finished,” you could change the Purchase action to trigger off incoming “Order Finished” events. While we recommend that you use our default actions and mapped properties when possible, our ecommerce spec doesn’t have an equivalent for all the recommended Google Analytics 4 events. You can use the Custom Event action to cover Google Analytics events outside our default actions. Track user sessions Engagement Time in Milliseconds In Google Analytics 4, you can only see active users. An active user is someone who engages with your site for a non-zero amount of time. Therefore, by default, we set the parameter engagement_time_msec equal to 1 for anyone you send to this integration. You can override this value in Actions by mapping the event field Engagement Time in Milliseconds to a field in your source data. Event Parameters and Client ID Another option is to capture session_id and session_number in your source events and map them as event properties in your Actions. If you set up both the normal and web versions of this integration, you can pass a session_id to ensure the user/session is preserved across both integrations, too. To do that, you’ll need to add some javascript to fetch the session_id and pass it along to your events. const [sessionId, sessionNumber, clientId] = await Promise.all([ new Promise(resolve => gtag('get', 'G-xxxxxxxxxx', 'session_id', resolve)), new Promise(resolve => gtag('get', 'G-xxxxxxxxxx', 'session_number', resolve)), new Promise(resolve => gtag('get', 'G-xxxxxxxxxx', 'client_id', resolve)) ]); cioanalytics.track('Order Completed', { sessionId, sessionNumber, clientId }); Be sure to replace G-xxxxxxxxxx with your Google Analytics 4 Measurement ID. You can then use sessionId, sessionNumber and clientId for the Event Parameters and Client ID fields under in your Action under Data Structure: Custom dimensions and metrics To generate reports based on the custom data you send to Google Analytics 4, you’ll need to create custom dimensions and metrics and link parameters in your incoming data to the corresponding dimensions or metrics. You can set up dimensions or metrics based on event parameters (or user properties) in your incoming source events. However, Google Analytics silently drops events that include nested parameters. Make sure that you flatten the key-value pairs in your event data or it won’t make it to Google Analytics. Consent management Beginning March 6, 2024, Google requires your users’ consent to collect their data and personalize ads in conformance with the Digital Markets Act. When you set up the Upload Click Conversion or Upload Call Conversion actions, Google includes two different fields for consent. Your users must consent to both for Google Ads to accept the conversion: User Data Ad Personalization Web-only mode Normal operation If you don’t have customers in the European Economic Area (EEA), or you do not need to follow EEA regulations, you can simply set these fields to Granted. If you have customers in the EEA, and you gather consent by way of a terms of service (TOS) or store consent outside of conversion events, you can set these fields to hardcode a Granted value. Otherwise, you’ll need to gather consent from your customers and map it to a value in your source events. For example, if your source events have a consent property, you can map that property to consent fields in your integration. You must gather consent as Granted, Unknown or Denied. Granted: results in a successful upload of the conversion Unknown or Denied: prevents successful upload, respecting your audience’s right to privacy Make sure you use the casing required for your integration, as indicated in the UI. FAQ & troubleshooting Why don’t I see my data in Google Analytics? If you don’t see your data in Google Analytics, ensure that you’ve configured and enabled at least one action for an event you want to send to Google Analytics. This integration won’t send data to Google until you configure at least one action. Google can take 24-48 hours to process data. As a result, your Google Analytics dashboards may not reflect the most current data. The Google Analytics Realtime report page displays activity on your site as it happens. How do I attribute events to a particular source? Due to limitations in the Google Analytics 4 Measurement Protocol API, our integration can’t pass certain reserved fields—including attribution data like UTM parameters—to Google. If you rely on attribution reporting, you can send attribution data as custom dimensions or set up a parallel web-mode integration to collect this data. Debug Mode Google Analytics 4 has a debug mode that displays your events and the user properties your sources collect in real-time. This can help you troubleshoot your implementation. Reserved event names and properties Google reserves certain event names, parameters, and user properties. Google silently drops events that include these reserved names. Google doesn’t accept events in the following conditions: event or user property names have spaces in them fields with null values events or properties that use reserved names Why do I see an error Param [PARAM] has unsupported value? Google has requirements/limitations imposed by their Measurement Protocol API. If an event contains null/object/array parameters, GA4 silently drops the event, so we don’t forward these events to Google Analytics. Rather, we return Invalid Type errors that you’ll find in the Data Out tab. To find out if an event will return this error, you can test it against GA4’s debug endpoint with a tool like Postman. Custom Event Naming Google Analytics 4 does not accept custom event names that include spaces. To prevent errors, we automatically convert custom event names that contain spaces to snake case (e.g. event_name). Event names are case sensitive. If you want event names to be lowercased, use the Lowercase Event Name setting when you set up Custom Event actions. If you disable this setting, Google will treat event names with different cases as distinct events. See Google Analytics 4 Event name rules for more information. Send events from both the browser and the server We support this integration in standard and web-only modes. In some cases, you might want to use this integration in both modes. In web mode, the Analytics.js client sends events from the browser to GA4. If you use this integration in normal mode (with our server-side libraries, etc), and you want to tie in server-side events representing your audience members, you’ll need to pass the same Client ID for both client and server-side events. To do this, you’ll need to fetch the Gtag-generated clientId and pass it as a property in events. See User Identification on this page for more information. GA4 collects some events that aren’t mapped to actions Google Analytics 4 collects events triggered by basic interactions with your site. See Google’s documentation for more information. Data can take up to 48 hours to appear in Google’s reports Google can take 24-48 hours to process data sent to Google Analytics. As a result, your Google Analytics dashboards may not reflect the most current data. The Google Analytics Realtime report page displays activity on your site as it happens. Deduplicating page views In web mode, we include a Page Views advanced setting. This setting is disabled by default, preventing Google’s gtag.js snippet from capturing its normal page_view event. Because we already send page events with cioanalytics.page(), this prevents duplicate page events from appearing in Google Analytics. If you see duplicate page_view events in your GA4 dashboard, you need to either: Disable the Page Views advanced setting (set it to False) cioanalytics.page() sends to the GA4 SDK. Or, Edit or disable the preset Set Configuration Fields mapping so only the page_view included in the gtag.js snippet sends to the GA4 SDK. --- ## Google BigQuery URL: https://docs.customer.io/integrations/data-out/connections/google-bigquery-data-out/ Send Customer.io data about messages, people, metrics, etc to your Google BigQuery warehouse by way of an Amazon S3 or Google Cloud Project (GCP) storage bucket. This integration syncs up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Google BigQuery loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up BigQuery with Google Cloud Storage Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. To use a GCS storage bucket, you must set up a service account key (JSON) that grants read/write permissions to the bucket. You’ll provide the contents of this key to Customer.io when you set up this integration. Go to Integrations and select Google BigQuery and then click Sync Bucket for Google Cloud Storage. Enter information about your GCS bucket and click Validate & select data. Enter Name of your GCS bucket. Enter the Path to your GCS bucket. Paste the JSON of your Service Account Key. Select the data that you want to export from Customer.io to your bucket. By default, we export all data, but you can disable the types that you aren’t interested in. Click Create and sync data. Set up BigQuery with Amazon S3 or Yandex Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. For S3, you’ll need to set up your bucket with ListBucketVersions, ListBucket, GetObject, and PutObject before you can sync data from Customer.io. Create an Access Key and a Service Key with read/write permissions to your S3 or Yandex bucket. Go to Integrations and select Google BigQuery and then click Sync Bucket. Enter information about your bucket and click Select data. Enter the Name of your bucket. Enter the path to your bucket. Paste your Access and Secret keys in the appropriate fields. Select the Region your bucket is in. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Each sync includes a cio-validate file If you sync data to an Amazon S3 bucket, Customer.io writes a file called cio-validate to your bucket before every sync. This is an empty file that we use to verify that we have write permissions to your bucket before each sync. You can safely delete this file. It does not affect data sync operations, and it’s not part of your exported data. If you have automated processes that import parquet files from your bucket, you may want to configure them to ignore the cio-validate file, since it’s not a parquet file and doesn’t contain any data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Google BigQuery. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Google BigQuery. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Google BigQuery. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS) or Secret Key (S3, Yandex) again. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Google BigQuery. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Google BigQuery (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/google-bigquery/ How it works This integration sends CSV, JSON, or parquet files containing your data to your Google BigQuery (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Google BigQuery (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and file names Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started To support Google BigQuery (Advanced), you’ll set up a Google Cloud Storage, Amazon S3, or Microsoft Azure Blob Storage bucket to store your data. Then, you’ll query and import data from your storage bucket to Google BigQuery (Advanced) either through a direct query or a product like Stitch. As a part of this integration, we’ll create parquet, JSON, or CSV files in your storage bucket. See data warehouses for a list of data schemas. Go to Data & Integrations > Integrations and select Google BigQuery (Advanced) in the Directory tab. Connect to your storage bucket: Review your setup and click Finish to enable your integration. Google Cloud Storage (GCS) Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of the Google Cloud Storage Bucket where files will be written to. Learn more about GCS buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Service Account: The JSON string of the Google Cloud Service Account with permissions to upload files to a bucket, which can be found in your Google Cloud Console. Learn more about Google Cloud Service Accounts. Amazon S3 Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of an existing bucket. Learn more about S3 buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Access Key: The AWS Access Key ID that will be used to connect to your S3 Bucket. Your Access Key ID can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Secret Key: The AWS Secret Access Key that will be used to connect to your S3 Bucket. Your Secret Access Key can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Region: The AWS Region where your S3 Bucket resides in. Learn more about AWS Regions. Azure Blob Storage Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Blob Sas Url: The SAS URL of the Azure Blob Storage container with permissions to upload files to a container. Learn how to generate an Azure SAS URL in our documentation. Blob Path: Optional folder inside the container where files will be written to. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Google Cloud Storage URL: https://docs.customer.io/integrations/data-out/connections/gcs-data-out/ Send Customer.io data about messages, people, metrics, etc to your Google Cloud Storage (GCS) instance. From here, you can ingest your data into the data warehouse of your choosing. This integration syncs up to every 15 minutes, helping you stay up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Google Cloud Storage participant c as Data Warehouse loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up a Google Cloud Storage (GCS) integration Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. To use a GCS storage bucket, you must set up a service account key (JSON) that grants read/write permissions to the bucket. You’ll provide the contents of this key to Customer.io when you set up this integration. Go to Integrations and select Google Cloud Storage and then click **Sync your Google Cloud Storage bucket. Enter information about your GCS bucket and click Validate & select data. Enter Name of your GCS bucket. Enter the Path to your GCS bucket. Paste the JSON of your Service Account Key. Select the data that you want to export from Customer.io to your bucket. By default, we export all data, but you can disable the types that you aren’t interested in. Click Create and sync data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Google Cloud Storage. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Google Cloud Storage. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Google Cloud Storage. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS). Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Google Cloud Storage. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Google Cloud Storage (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/google-cloud-storage/ How it works This integration sends CSV, JSON, or parquet files containing your data to your Google Cloud Storage (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Google Cloud Storage (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and filenames Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started Go to Data & Integrations > Integrations and select Google Cloud Storage (Advanced) in the Directory tab. Connect to your storage bucket: Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of the Google Cloud Storage Bucket where files will be written to. Learn more about GCS buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Service Account: The JSON string of the Google Cloud Service Account with permissions to upload files to a bucket, which can be found in your Google Cloud Console. Learn more about Google Cloud Service Accounts. Review your setup and click Finish to enable your integration. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Google Sheets URL: https://docs.customer.io/integrations/data-out/connections/google-sheets-non-etl/ Getting started Go to Data & Integrations > Integrations and select the Google Sheets entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Click Enable Destination. Connect your Google Account to Customer.io Before you can use the Google Sheets destination, you need to grant us access to your sheets. When you set up the destination, or when you go to the Settings tab after you set up the destination, you can click Connect to sign into your google account and grant Customer.io access to your sheets. Actions In this destination, each action represents a sheet that you want to populate with information from your source. Because we don’t know the ID of your sheet or the fields you want to import, you’ll have to set up actions for this destination—one per sheet you want to send data to. Action Default Trigger Description Upsert Sheet event = “updated” or event = “new” Write values to a Google Sheets spreadsheet. If a record with the given record identifier already exists, it will be updated. Otherwise, a new record will be created. What does “Upsert Sheet” mean? “Upsert” is a portmanteau of “insert” and “update”: for each incoming source event, we’ll insert a new row or update an existing row. We use the Record Identifier field in the action to check if a row exists or not. This means that whatever you set for the Record Identifier should be a unique value—like a userId, anonymousId, groupId, etc. If the field mapped to the record identifier does not exist, we’ll add a new row to your sheet; if it exists, we’ll update that row. When you send data to Customer.io, we’ll use the properties you specify as data in your sheet: each key is a column heading and each value a cell within that column in the appropriate row. For example, if you pass $.properties.username from a track call to your sheet, you’ll have a column called username populated with the username included in your events. Because each new key is a column in your sheet, you should make sure that your sources send relatively uniform data to Customer.io. You may also want to specify the exact properties (columns) you want to set in your sheet, rather than sending all event properties to ensure that you only populate data relevant to your sheet. Adding a sheet For each Google Sheet you want to send data to, you’ll need to set up an action. Before you begin, you should understand the source data that you want to populate in a sheet—the kinds of track events you send and the properties your source events include, to make sure that you populate your sheets correctly. Before you begin: make sure you’ve granted Customer.io access to your Google Sheets, or we won’t be able to send data to your destination. To set up a new Upsert Sheet action: In your Google Sheets destination, go to the Actions tab and click Add Action. Set your Trigger conditions. In general, you’ll use Track Event Name to determine the kinds of events that’ll add data to your sheets. Under Data Structure, set your Record Identifier: this is the value we’ll use to determine whether we insert a new row or update an existing one. That means that this value should be unique to the data you want to keep in your sheet—like a userId or groupId. By default, we use the userId if it exists, or the anonymousId if it doesn’t. In the next field down, add Spreadsheet ID. You’ll find this value in the URL for your sheet, like https://docs.google.com/spreadsheets/d/{SPREADSHEET_ID}/edit. In the next field, enter the exact name of the sheet you want us to add data to. In Google sheets, this is the tab at the bottom of the page, named Sheet1 by default. If you don’t enter the sheet name exactly as it appears in Google, your action will fail. Determine whether to import Raw data to your sheet or to use the User Input setting. Raw: your sheet imports data exactly as sent in your source events. This prevents your sheets from automatically formatting or modifying values it identifies as currencies, numbers, dates, etc. User Input: your sheet applies formatting to your data, allowing your sheet to modify and format values it identifies as currencies, dates, etc. In the last field, add the properties that you want to send to your google sheet. Each key will be a column in your sheet.  We stringify nested JSON Google sheets won't handle nested properties. If your actions reference a property containing a JSON object or array value, we'll flatten and stringify the values, meaning cells in your sheet will contain stringified JSON values. Click Save Action. If you’ve connected a data source, you should start seeing data flow into your sheet as soon as matching events come in. Troubleshooting Errors in Data Out If you see errors in Data Out, make sure that you’ve granted Customer.io access to your Google Sheets. If you haven’t granted Customer.io access, you’ll receive errors for every entry in the Data Out tab. Make sure that you’ve set the correct Sheet Name as well. Your action must use the sheet name exactly as it appears in Google Sheets—the same case, length, no trailing characters, etc—otherwise we’ll report 404 errors for your sheet. Why is there extra data in my sheet? When you set up the Data Structure for your sheet, you can specify the keys (columns) that you want to send to your sheet. If you send $.properties you’ll capture all event properties in your sheets, whether you want to or not! To keep a clean sheet, you may want to specify the exact event properties that you want to capture, like $.properties.product_name, so that your sheet’s data is uniform. We stringify nested objects and arrays Google Sheets doesn’t handle nested objects or arrays, so we flatten and stringify them. If you send a property with an object or array value, we populate that column with stringified JSON. --- ## Google Tag Manager (GTM) URL: https://docs.customer.io/integrations/data-out/connections/google-tag-manager/  Don’t use both the GTAG and GTM destinations Using both the Google Tag Manager and Google Ads (GTAG) destinations on the same page, with the same pixel ID, will likely result in duplicate events. Getting started Go to Data & Integrations > Integrations and select the Google Tag Manager (GTM) entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Container ID: The Container ID is available in your Tag Manager Accounts Page. Data Layer Name: Customize the name of the data layer object. Useful if you have multiple containers on the same page. Will default to dataLayer if not specified. Environment: Optional preview environment (the gtm_preview parameter). Important: make sure the string includes gtm_auth. For example, your string should look like env-xx&amp;gtm_auth=xxxxxx Click Enable Destination. Consent management Beginning March 6, 2024, Google requires your users’ consent to collect their data and personalize ads in conformance with the Digital Markets Act. With other Google destinations, you can manage consent within settings and actions. But for Google Tag Manager, you must manage constent within your GTM settings. You cannot manage consent for this integration within Customer.io. Custom data layer The Datalayer name setting is helpful if you use multiple instances of Google Tag Manager on your site. You can set separate data layer names for your different GTM destinations so that you don’t duplicate events across your GTM instances. Preview environment If you use Google Tag Manager in Preview and Debug Mode to test your events, you can enter the preview environment ID in the Preview environment setting. This ensures that we send events to the correct GTM container when you use preview and debug mode. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Track an event Track Page View type = “page” Track the current page Tracking page views By default, this destination tracks all page views. But you may want to track only certain page views—like categorized or named pages. If this is something you want to do, you’ll want to update the trigger for the page action. To track categorized pages, you’ll want to update the trigger to fire when the category property is present on the page call. To track named pages, you’ll want to update the trigger to fire when the name property is present on the page call. Migrating from standalone GTM If you previously used Google Tag Manager in your environment and are moving to Customer.io, your calls will follow a similar format. Where you likely used to call gtag('event', 'event-name', {}), you’ll now call cioanalytics.track('event-name', {}) Standalone GTM Customer.io gtag('event', 'login', { first_name: 'Alex' }); cioanalytics.track('login', { first_name: 'Alex' }); --- ## Heap URL: https://docs.customer.io/integrations/data-out/connections/heap/ There are two versions of this integration You’ll see two entries for Heap in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the Heap entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. App Id: The app_id corresponding to one of your projects. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” or type = “page” or type = “screen” Send an event to Heap. Identify User type = “identify” Set the user ID for a particular device ID or update user properties. Getting started: web destination Go to Data & Integrations > Integrations and select the Heap entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. App Id: The app ID of the environment to which you want to send data. You can find this ID on the Projects page. Disable Text Capture: Setting to true will redact all target text on your website. For more information visit the heap docs page. Secure Cookie: This option is turned off by default to accommodate websites not served over HTTPS. If your application uses HTTPS, we recommend enabling secure cookies to prevent Heap cookies from being observed by unauthorized parties. For more information visit the heap docs page. Tracking Server: This is an optional setting. This is used to set up first-party data collection. For most cased this should not be set. For more information visit the heap docs page. Hostname: This is an optional setting used to set the host that loads heap-js. This setting is used when heapJS is self-hosted. In most cased this should be left unset. The hostname should not contain https or app id it will be populated like so: https://${hostname}/js/heap-${appId}.js. For more information visit the heap docs page. Click Enable Destination. Web destination actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Track events Identify User type = “identify” Sets user identity Troubleshooting Anonymous traffic from server-side sources doesn’t appear in Heap Heap’s API rejects server-side events that don’t include a user_id. You’ll need to pass a user_id in event calls from server-side sources — even if you use the anonymous_id as the user_id — when you send server-side events to Heap. Heap does not supported nested objects/arrays Heap does not accept nested properties so we automatically flatten the incoming object and stringify nested values from your source calls before we send data to Heap. For example, if you sent this event: cioanalytics.track('Signed Up', { foo: { bar: { cheese: 'american', prop: [1, 2, 3], products: [{"A": "Jello"}, {"B": "Peanut"}] } } }); We’d transform the properties for Heap like this: foo.bar.cheese: 'american' foo.bar.prop: '[1,2,3]' foo.bar.products: "[{'A': 'Jello'},{'B': 'Peanut'}]" --- ## HubSpot URL: https://docs.customer.io/integrations/data-out/connections/hubspot/ There are two versions of this integration You’ll see two entries for HubSpot in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the HubSpot entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Portal Id: The Hub ID of your HubSpot account. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send Custom Behavioral Event type = “track” Send a custom behavioral event to HubSpot. Upsert Contact type = “identify” Create or update a contact in HubSpot. Upsert Company type = “group” Create or update a company in HubSpot. Upsert Custom Object Record Upsert records of Deals, Tickets or other Custom Objects in HubSpot. Getting started: web integration Go to Data & Integrations > Integrations and select the HubSpot entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Portal Id: The Hub ID of your HubSpot account. Enable European Data Center: Enable this option if you would like to load the HubSpot SDK for EU data residency. Flush Identify Immediately: Enable this option to fire a trackPageView HubSpot event immediately after each identify call to flush the data to HubSpot immediately. Format Custom Behavioral Event Names: Format the event names for custom behavioral event automatically to standard HubSpot format (pe&lt;HubID&gt;_event_name). Load Forms SDK: Enable this option if you would like to automatically load the HubSpot Forms SDK onto your site. Click Enable Destination. Web integration actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Custom Behavioral Event type = “track” Send a custom behavioral event to HubSpot. Track Page View type = “page” Track the page view for the current page in HubSpot. Upsert Contact type = “identify” Create or update a contact in HubSpot. FAQ and Troubleshooting Why don’t my custom events show up in Hubspot? HubSpot has limits for custom behavioral events that don’t necessarily apply to source data, like a limit on the number of event properties per event. Each event can contain data up to 50 properties. If your source data goes over this limit, HubSpot truncates incoming events to the first 50 properties in each event. See HubSpot for other limits. My objects aren’t companies. How do I send other objects to HubSpot? Our default group action references companies. But if you want to create records for other types of objects in Hubspot, you can use the Create Custom Object Record action. For example, to create a deal in HubSpot, you’d add a new Create Custom Object Record action, set up your Trigger criteria, and input a literal string of “deals” as the Object Type. You can use the Properties object to add fields that are in the deals object, like dealname and dealstage. You can follow this same process for other types of objects (tickets, quotes, etc). Note that you can only send custom fields inside the Properties object, including associations. We do not support record updates for custom, non-company objects; you can only create new records this way. How do I send Page events to HubSpot? The Page View action is only available when you use this integration in web mode. If you use this integration in its normal mode, you can set up a Custom Behavioral Event to send page data to Hubspot. You’ll also need to follow Hubspot’s instructions to create a custom behavioral event for Page Viewed in HubSpot. --- ## Intercom URL: https://docs.customer.io/integrations/data-out/connections/intercom/ There are two versions of this integration You’ll see two entries for Intercom in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the Intercom entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Identify Contact type = “identify” Create or update a contact in Intercom Identify Company type = “group” Create or update a company in Intercom and attach a contact. Track Event type = “track” Submit an event to Intercom. Getting started: web integration Go to Data & Integrations > Integrations and select the Intercom entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. App Id: The app_id of your Intercom app which will indicate where to store any data. Activator: By default, Intercom will inject their own inbox button onto the page, but you can choose to use your own custom button instead by providing a CSS selector, e.g. #my-button. You must have the "Show the Intercom Inbox" setting enabled for this to work. The default value is #IntercomDefaultWidget. Rich Link Properties: A list of rich link property keys. Api Base: The regional API to use for processing the data Click Enable Destination. Web integration actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Submit an event to Intercom. Identify User type = “identify” or type = “page” Create or update a user in Intercom. Identify Company type = “group” Create or update a company in Intercom. FAQ and Troubleshooting Why is my company not in my Intercom dashboard? If you create a company without an assigning user (using the group method), the company will not appear on Intercom’s dashboard. This is expected functionality in Intercom. When you associate a person with the company, it will appear in your list of companies in Intercom. Why isn’t a user getting attached to a company? When you use the Identify Company action (group calls in your incoming data), we create or update a company’s information. In the same action, We also attach the user in your group call to that company. If the user doesn’t exist in Intercom yet, we’ll create or update the company but can’t attach the user. You should identify a user before you try to attach them to a company with a group call. --- ## Koala URL: https://docs.customer.io/integrations/data-out/connections/koala/ Getting started Go to Data & Integrations > Integrations and select the Koala entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Project Slug: Please enter your Public API Key found in your Koala workspace settings. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Send visitor events to Koala. Identify Visitor type = “identify” Update visitor traits in Koala. --- ## LaunchDarkly URL: https://docs.customer.io/integrations/data-out/connections/launchdarkly/ Getting started Go to Data & Integrations > Integrations and select the LaunchDarkly entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Id: Find and copy the client-side ID in the LaunchDarkly account settings page. Click Enable Destination. Actions To get the most out of this destination, you need to create metrics in LaunchDarkly corresponding to your track events. See Creating metrics for help representing your source events in LaunchDarkly. When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Alias User type = “identify” or type = “alias” Alias an anonymous user with an identified user key. Track Event type = “track” Track custom events for use in A/B tests and experimentation. --- ## LiveLike URL: https://docs.customer.io/integrations/data-out/connections/livelike-cloud/ Getting started Go to Data & Integrations > Integrations and select the LiveLike entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Client Id: Your LiveLike Application Client ID. Producer Token: Your LiveLike Producer token. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Send an event to LiveLike. --- ## LogRocket URL: https://docs.customer.io/integrations/data-out/connections/logrocket/ Getting started Go to Data & Integrations > Integrations and select the LogRocket entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. App ID: The LogRocket app ID. Network Sanitization: Sanitize all network request and response bodies from session recordings. Input Sanitization: Obfuscate all user-input elements (like <input> and <select>) from session recordings. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track type = “track” Send track events to logrocket for filtering and tagging. Identify type = “identify” Send identification information to logrocket. --- ## Meta (Facebook) Pixel URL: https://docs.customer.io/integrations/data-out/connections/meta-pixel/ Getting started Go to Data & Integrations > Integrations and select the Meta (Facebook) Pixel entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Pixel ID: Your Pixel ID from the snippet created on the Facebook Pixel creation page. Limited Data Use: The Limited Data Use (LDU) setting controls whether or not Limited Data Use mode is set in the Pixel SDK. When enabling LDU, default user geography options will be sent which will use geolocation. Send Default Events Automatically: The Facebook Pixel will, by default, send button click and page metadata from your website to improve your ads delivery and measurement and automate your pixel setup. You can learn more about this here. You can disable this functionality by unchecking this setting. Click Enable Destination. Migrating from Segment If you’re coming to us from Segment, you’ll notice that our Meta Pixel integration contains fewer settings than Segment’s Facebook Pixel destination. We’ve moved many of the settings that were globally set in Segment, like custom properties and personally identifiable information settings down to the component actions for this integration—where we think they make more sense. Limited Data Use Meta Pixel has a setting called Limited Data Use, pertaining to the ways in which they store data based on US state laws—particularly, California, Colorado, and Connecticut. The corresponding setting in our Meta Pixel integration is enabled by default. When you enable this setting, we’ll automatically apply the appropriate limited data use settings for members of your audience by geolocating their IP addresses or the $.traits.address properties that we automatically capture in the Identify User action. Actions Most of the actions for this integration are based on the names of the events you send. While these events are based on our ecommerce specification, you can update the track-based actions to use whatever event names you use to represent ecommerce actions. For example, the Track “Order Completed” Event is based on a track call with the event name “Order Completed” by default. If you use “Payment Accepted” instead, you could edit the action to change the trigger. When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Identify User type = “identify” Send user data to Meta Track Custom Event type = “track” and event != “Order Completed” and event != “Product Added” and event != “Product List Viewed” and event != “Product Viewed” and event != “Products Searched” and event != “Checkout Started” Track an event for a user Track Page View type = “page” Track the current page Track Checkout Started Event type = “track” and event = “Checkout Started” Track the Checkout Started event which will be sent as an InitiateCheckout event to Facebook. Track Order Completed Event type = “track” and event = “Order Completed” Track the Order Completed event which will be sent as a Purchase event to Facebook. Track Product Added Event type = “track” and event = “Product Added” Track the Product Added event which will be sent as a AddToCart event to Facebook. Track Product List Viewed Event type = “track” and event = “Product List Viewed” Track the Product List Viewed event which will be sent as a ViewContent event to Facebook. Track Products Searched Event type = “track” and event = “Products Searched” Track the Products Searched event which will be sent as a Search event to Facebook. Track Product Viewed Event type = “track” and event = “Product Viewed” Track the Product Viewed event which will be sent as a ViewContent event to Facebook. Facebook standard events You’ll see that as a part of this integration, we’ve mapped six of our standard ecommerce events to Meta Pixel. We forward track events based on our ecommerce specification to Meta using their standard event format. “Order Completed” is sent as “Purchase” “Product Added” is sent as “AddToCart” “Product List Viewed” is sent as “ViewContent” “Product Viewed” is sent as “ViewContent” “Products Searched” is sent as “Search” “Checkout Started” is sent as “InitiateCheckout” But Meta has more than these six standard events! For events beyond the six we’ve predefined, you can use the Custom Event action, and map incoming data to Meta’s other standard events.  Purchase events require a currency Facebook’s Purchase event requires a currency property representing the type of currency used to make the purchase. If you don’t pass this property, we assume that the currency is the US Dollar—USD. Standard and custom properties We’ve mapped ecommerce events to Meta using only the properties that Meta expects, but you can send additional properties in track or page calls and map them to Meta’s events. On the Actions tab, find the action you want to change, click and select Edit. At the bottom of the page, click Edit Object. Enter the names of the properties from your incoming events that you want to preserve in Meta. Timestamps Meta Pixel expects timestamps in ISO 8601 format without time zone information—for example, 2023-05-27T15:09:41. We automatically send the $.timestamp from events in this format, but you should format timestamps to fit this format in other fields you want to pass to Meta. Map event categories to meta content types Meta events contain a content_type that’s similar to the event category we use in track and page view events from our analytics.js library. By default, we assume that Meta events are centered around products and set content_type to product accordingly. But if you use real estate, travel, or automotive Dynamic Ads you can map the associated track event category values to content_type values. For example, you might map the category “cars” to the “vehicle” content type so Meta promotes relevant vehicles from your catalog. See Facebook Dynamic Ads to learn more about available content types. Handling Personally Identifiable Information (PII) Meta enforces strict guidelines for Personally Identifiable Information (PII). To adhere to Meta’s rules, we automatically filter out the following data representing PII: email firstName lastName gender city country phone state zip birthday We’ll still forward events containing PII to Meta, but we strip these properties from the events before we do. If you want to redact additional PII, you can use the hash function to make sure that you don’t send properties in plain text. The hash applies a sha256 hash to your data, ensuring that you properly store data in Meta. On the Actions tab, find the action you want to change, click and select Edit. At the bottom of the page, click Edit Object. Enter the name of the property you want to send in events to Facebook. In the second box, after the =, pick the incoming key and then click Functions. Select the hash function. Click Add Key/Value to set additional properties. Send additional data with events By default, actions that forward data to Meta contain default properties expected by those events. If you capture additional information in each event, you can pass that data along to Meta at the bottom of the data structure for any action. On the Actions tab, find the action you want to change, click and select Edit. At the bottom of the page, click Edit Object. Add the keys and properties you want to send. For Custom events, this might be as simple as sending $.properties. Click Add Key/Value to add additional properties. Troubleshooting Inconsistent or missing conversions Meta conversion pixels will fire inconsistently if your page redirects or reloads before the pixel has time to load on the page. Make sure your pages don’t redirect or reload for at least 300ms after the conversion event happens. We recommend using our trackLink or trackForm helpers to delay the page redirect. You can extend the delay by setting the timeout to 500ms. Extra or duplicate conversions You might see duplicate conversions if you use the same data sources in your development, staging, or testing environments. We recommend that you set up separate data-in integrations for each environment so that you can either point events to test conversion pixels in Meta Conversion Tracking or turn off Meta Conversion Tracking in your non-production environments. If you have separate environments and you still see duplicate conversions, check that your conversion events don’t appear in other places on your site. If your audience reloads the conversion page or otherwise re-triggers conversion events, you’ll count those conversions multiple times. Meta’s conversion reports count view-through conversions and click-through conversions by default. You can change that setting in Meta under Attribution Settings. Meta conversions don’t match Google Analytics Meta counts conversions per person, but Google Analytics counts conversions per browser cookie session (unless you’re using Google Analytics User-ID). If someone saw or clicked your ad on a mobile phone, and then returned to your site on a different device and completed their purchase, Google Analytics may not know that this was the same person, but Meta would. In that scenario Google Analytics counts 2 unique visits with a conversion attributed to the last device that completed the purchase. Facebook counts one conversion with the conversion properly attributed to the last ad click/view on the original device. --- ## Metronome URL: https://docs.customer.io/integrations/data-out/connections/metronome/ Getting started Go to Data & Integrations > Integrations and select the Metronome entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Token: Your Metronome API Token Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send Event Send an event to Metronome Mapping events to Metronome Metronome uses a specific event format that requires the following fields. When you map the Send Event action, you’ll need to make sure that you also map the following five fields. Field Type Description transaction_id string The unique identifier for each event, analgous to our messageId. customer_id string Represents which customer in Metronome the event applies to, analagous to our userId or anonymousId timestamp string When the event happened in RFC 3339 format. event_type string Typically our event name property, like, page_view or cpu_used. properties (object) The key/value pairs with details of the event—typically $.properties from the source event. --- ## Mixpanel URL: https://docs.customer.io/integrations/data-out/connections/mixpanel/ Getting started Go to Data & Integrations > Integrations and select the Mixpanel entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Project Token: Mixpanel project token. Api Secret: Mixpanel project secret. Api Region: Learn about EU data residency Source Name: This value, if it's not blank, will be sent as source_name to Mixpanel for every event/page/screen call. Strict Mode: This value, if it's 1 (recommended), Mixpanel will validate the events you are trying to send and return errors per event that failed. Learn more about the Mixpanel Import Events API Click Enable Destination. Actions Before you get started with Mixpanel, you should implement the alias method in your sources. This ensures that you attribute activity to the right users. When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Track Event Default Trigger: type = “track” Send an event to Mixpanel. Learn more about Events in Mixpanel Identify User Default Trigger: type = “identify” Set the user ID for a particular device ID or update user properties. Learn more about User Profiles and Identity Management. Group Identify User Default Trigger: type = “group” Updates or adds properties to a group profile. The profile is created if it does not exist. Learn more about Group Analytics. Alias Create an alias to a distinct id. This action is primarily supported for the sake of customers using the legacy identity management in their Mixpanel project. For new customers or those who have migrated to the new identity management in Mixpanel should use identify. Track Purchase Default Trigger: type = “track” Send an ‘Order Completed’ Event to Mixpanel. Use simplified identity merging in Mixpanel Before March 2023, Mixpanel required you to use the alias method to merge identities. Now Mixpanel has a Simplified API for identity merging. This API supports identity merging the way most platforms do—without needing to send alias calls. Learn more about Mixpanel’s simplified ID merge feature. If you created your account before April 2024, you need to enable Mixpanel’s Simplified API to support easy identity merges. Otherwise, you’ll have to use the alias method to merge identities. Using the alias method (original API) Mixpanel’s original API doesn’t gracefully handle identity changes. For example, when you identify a person who was previously anonymous, Mixpanel’s original API doesn’t automatically associate anonymous activity with the userId in your identify call. Instead, Mixpanel’s original API treats the anonymousId and userId as two separate people. If you don’t want to enable the Simplified API, you need to send an alias call to associate anonymous activity with an identified person (a userId). You should send the alias method before you send an identify call for a person. For example, using our JavaScript snippet, your flow might look something like this: // the anonymous user does actions under an anonymous ID cioanalytics.track('92734232-2342423423-973945', 'Anonymous Event') // the anonymous user signs up and is aliased to their new user ID cioanalytics.alias('92734232-2342423423-973945', '1234') // the user is identified cioanalytics.identify('1234', { 'plan': 'Free' }) // the identified user does actions cioanalytics.track('1234', 'Identified Action') Attributing track events to groups You can attribute events to groups in Mixpanel. To do this, make sure that your track calls either include a groupId and that it’s mapped to a valid value in your action. By default, we map this field to your source data’s context.groupId field. --- ## Mixpanel (Message Metrics) URL: https://docs.customer.io/integrations/data-out/connections/mixpanel-legacy/ Take advantage of our native integration to export audience engagement events to Mixpanel. How it works This integration sends audience engagement data—also available as reporting webhooks—to Mixpanel, where you can use it alongside your other user and analytics data to gain insight into your audience’s behaviors. The events we send are both based on things that happen in Customer.io and your customer activity. For example, an email sent event indicates that we’ve sent an email to a person, but an email clicked event lets you know that your audience clicked a link in your email. sequenceDiagram actor a as Your Audience participant b as Customer.io participant c as Mixpanel b->>a: send email b-->>c: report email send a->>b: user clicks link b-->>c: report email clicked Enable the Mixpanel integration As a part of this process, you’ll need your Mixpanel project token. Go to Integrations and select the Mixpanel integration card. Copy and paste your Mixpanel project token into the Project Token field. You’ll find your key in Mixpanel Settings > Project Settings. (Optional) If your account is in Mixpanel’s EU data centre, enable the Use EU Data Residency setting. Select the events that you want to send to Mixpanel. We describe each event on the page. Check out our reporting webhooks documentation for more information about the data available to each event. (Optional) Enable Body Content to capture the first 255 characters of messages in Sent events. This is a Mixpanel limitation. You can still lookup deliveries in Customer.io to see the full contents of sent messages. Click Save. Test your integration When you add your project token and set up your integration, you can click Test Connection to make sure that your key is valid and connects to the correct project in Mixpanel. The test sends an email sent event to Mixpanel to ensure that your API key is valid. You can check your Mixpanel project for this email sent event to make sure that you set up your integration for the Mixpanel project you want to send Customer.io data to. Payload reference In this integration, we map our reporting webhook payloads to fit Mixpanel’s track format. Below is a list of fields from our reporting webhooks and how we map them to Mixpanel. We’ve also provided examples of both kinds of events.  Check Message to find the campaign_id in Mixpanel In Mixpanel, campaign_id is a reserved property. If you look at an event for a message sent as a part of campaign in Mixpanel, the Message field under All Properties represents the campaign_id. If you switch to JSON mode, you’ll also see the campaign_id. Mixpanel Field Customer.io Field Description event object_type + metric Represents the channel and metric. We capitalize/camel-case these items, e.g. Email Sent properties.distinct_id identifiers.id If a person doesn’t have an id, this property is blank. properties.profile_email identifiers.email If a person doesn’t have an email, this property is blank properties.devices "id": recipient.device_id "platform": recipient.device_platform An array of objects, where each object represents a device. properties.$insert_id event_id The unique identifier for the event, used to deduplicate events in Mixpanel. properties.time timestamp The date-time when the event occurred, not necessarily when the event is sent to Mixpanel. properties.account_id [account_id] While your account_id isn’t in our standard reporting webhook payload, we send this to Mixpanel to support their Group Analytics add on. properties.workspace_id [workspace_id] While your workspace_id isn’t in our standard reporting webhook payload, we send this to Mixpanel to support their Group Analytics add on. properties The remaining fields in the webhook payload Mixpanel payload Mixpanel payload { "event": "Sms Sent", "properties": { "time": 1668976965, "distinct_id": "346300", "$insert_id": "01GJBCEYDJMH4VCDKXC98JNQ2D", "account_id": [1], "action_id": 1000495, "campaign_id": 1000024, "content": "Hey jenny! I got your number!", "customer_id": "346300", "delivery_id": "dgSiuAMAAK2_Fqu_FgGElsdx39aGHalAkXrIOwc=", "headers": {}, "journey_id": "01GJ3N8GM0AK1FJB1WFNCN2N7C", "recipient": "+15558675309", "workspace_id": [56354] } } Customer.io payload Customer.io payload { "event_id": "01GJBCEYDJMH4VCDKXC98JNQ2D", "object_type": "sms", "timestamp": 1668976965, "metric": "sent", "data": { "trigger_id": 1, "customer_id": "346300", "delivery_id": "dgSiuAMAAK2_Fqu_FgGElsdx39aGHalAkXrIOwc=", "action_id": 1000495, "campaign_id": 1000024, "journey_id": "01GJ3N8GM0AK1FJB1WFNCN2N7C", "identifiers": { "id": "346300", "email": "cool.person@example.com", "cio_id": "d9c106000001" }, "content": "Hey Jenny! I got your number!", "recipient": "+15558675309" } } --- ## MoEngage URL: https://docs.customer.io/integrations/data-out/connections/moengage/ Use cases Sync customer profiles to MoEngage so you can use MoEngage’s segmentation and campaign tools with up-to-date customer data. When someone signs up or updates their profile, their user record in MoEngage updates automatically. Track customer behavior in MoEngage by sending events like purchases, page views, or feature usage. MoEngage uses these events to power push notifications, in-app messages, and other engagement campaigns. Unify your engagement data across Customer.io and MoEngage. If you use MoEngage for mobile engagement (push, in-app) and Customer.io for other channels, this integration keeps both systems working with the same customer data. Trigger MoEngage campaigns based on events from your data pipeline. For example, trigger a push notification campaign in MoEngage when a user completes a purchase or abandons a cart. Getting started MoEngage operates in multiple regions. Select the region matching your MoEngage account—using the wrong region causes authentication failures. Region code Location DC_01 US (default) DC_02 EU DC_03 India DC_04 Singapore Go to Data & Integrations > Integrations and select the MoEngage entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Id: Your Moengage API Id Api Key: Your Moengage API Key Region: The region to send your data. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Send an event to Moengage. Identify User type = “identify” Set the user ID for a particular device ID or update user properties. Identify user The Identify User action creates or updates a user profile in MoEngage. It maps to identify calls by default. Use it to sync customer attributes like name, email, phone number, and custom traits to MoEngage. MoEngage uses these profiles for segmentation, campaign targeting, and personalization. Include a consistent identifier (like userId) so MoEngage can match incoming data to existing user profiles. Track event The Track Event action sends an event to MoEngage. It maps to track calls by default. These events appear in the user’s activity timeline in MoEngage and can trigger campaigns, flows, and segments. For example, if you send a track call with the event name Purchase Completed, you can configure a MoEngage campaign that sends a follow-up push notification to that user. Things to know Select the correct data center region. MoEngage operates in multiple regions (US, EU, India, Singapore). API calls fail with authentication errors if you select the wrong region. User identity is key. MoEngage needs a consistent user identifier to match incoming data to user profiles. Make sure your identify and track calls include a userId that MoEngage can use to look up or create the user. Events power MoEngage’s engagement tools. Events you send via the Track Event action drive segmentation, campaign triggers, analytics, and Smart Triggers across MoEngage. The more behavioral data you send, the more you can do in MoEngage. MoEngage enforces its own rate limits. High-volume event streams may hit MoEngage’s API throttles. If you send large volumes of events, check your MoEngage plan’s API limits. --- ## MS Azure Blob Storage (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/azure-blob-storage/ How it works This integration sends CSV, JSON, or parquet files containing your data to your MS Azure Blob Storage (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as MS Azure Blob Storage (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and file names Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started Go to Data & Integrations > Integrations and select MS Azure Blob Storage (Advanced) in the Directory tab. Connect to your storage bucket: Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Blob Sas Url: The SAS URL of the Azure Blob Storage container with permissions to upload files to a container. Learn how to generate an Azure SAS URL in our documentation. Blob Path: Optional folder inside the container where files will be written to. Review your setup and click Finish to enable your integration. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Pinterest Conversions URL: https://docs.customer.io/integrations/data-out/connections/pinterest-conversions/ Getting started Go to Data & Integrations > Integrations and select the Pinterest Conversions entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Ad Account Id: Unique identifier of an ad account. This can be found in the Pinterest UI by following the steps mentioned here. Conversion Token: The conversion token for your Pinterest account. This can be found in the Pinterest UI by following the steps mentioned here. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Report Conversion Event Report events directly to Pinterest. Data shared can power Pinterest solutions that will help evaluate ads effectiveness and improve content, targeting, and placement of future ads. Deduplication with Pinterest Tag Pinterest doesn’t know if conversions reported by the Pinterest Tag and conversions reported by the API are the same. Because Pinterest recommends using both the API for Conversions and the Pinterest Tag, they automatically deduplicate events to avoid double-counting events sent through multiple sources. For example, imagine that a user triggers an add to cart event and the tag reports the data using 123 as the event ID, and your web server later reports the conversion to the API, also using 123 as the event ID. They’ll look at the event IDs to confirm they correspond to the same event. If they do, Pinterest won’t count the conversion twice, which helps you report conversions using both the tag and the API without having to worry about over-counting conversions. You should use deduplication for any events they expect to be reported by multiple sources across the API and the Pinterest Tag. Conversion Events must meet the following requirements to be deduped: Events have non-empty and non-null values for event_id and event_name The action_source for events is not offline (for example, events that occurred in the physical world, like in a local store) where the action_source parameter is one of – app_android, app_ios, web, or offline. The duplicate events arrive within 24 hours of initial, unique events. Limited Data Processing Starting from Jan 1, 2023, Pinterest introduced the Limited Data Processing setting per the California Consumer Privacy Act (CCPA). This flag helps you comply with CCPA. You’re responsible for complying with user opt-outs, as well as identifying the user’s state of residency when you implement the Limited Data Processing setting. This setting can impact campaign performance and targeting use cases. Pinterest recommends that you use the Limited Data Processing setting on a per-user basis for best results. LDP relies on 3 fields and is enabled only when all 3 combinations are met, if one of them is not met then LDP is disabled / ignored. Field Name Field Description Required Value for LDP opt_out_type Opt Out Type based on User’s privacy preference “LDP” st State of Residence “CA” country Country of Residence “US” Hashing Personally Identifiable Information (PII) We create a SHA-256 hash of the following fields before we send them to Pinterest. This ensures that your user data is stored safely in Pinterest. External ID Mobile Ad Identifier Email Phone Gender Date of Birth Last Name First Name City State Zip Code Country User Data Parameters We automatically map user data fields to their corresponding Pinterest parameters as expected by the Conversions API. User Data Field Conversions API User Data Parameter External ID external_id Mobile Ad Id hashed_maids Client IP Address client_ip_address Client User Agent client_user_agent Email em Phone ph Gender ge Date of Birth db Last Name ln First Name fn City ct State st Zip Code zp Country country Custom Data Parameters We automatically map custom data fields to their corresponding parameters as expected by the Pinterest’s Conversions API. User Data Field Conversions API Custom Data Parameter Currency currency Value value Content IDs content_ids Contents contents Number of Items num_items Order ID order_id Search String search_string Opt Out Type opt_out_type Server Event Parameter Requirements Pinterest requires the action_source server event parameter for all events sent to the Pinterest Conversions API. This parameter specifies where the conversions occur and is one of app_android, app_ios, web, or offline. --- ## Pipedrive URL: https://docs.customer.io/integrations/data-out/connections/pipedrive/ Getting started Go to Data & Integrations > Integrations and select the Pipedrive entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Domain: Pipedrive domain. This is found in Pipedrive in Settings > Company settings > Company domain. Api Token: Pipedrive API token. This is found in Pipedrive in Settings > Personal preferences > API > Your personal API token. Person Field: This is a key by which a Person in Pipedrive will be searched. It can be either Person id or a custom field containing external id. Default value is person_id. Organization Field: This is a key by which an Organization in Pipedrive will be searched. It can be either Organization id or a custom field containing external id. Default value is org_id. Deal Field: This is a key by which a Deal in Pipedrive will be searched. It can be either Deal id or a custom field containing external id. Default value is deal_id. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Create or Update Organization type = “group” Update an organization in Pipedrive or create it if it doesn't exist yet. Create or Update Person type = “identify” Update a person in Pipedrive or create them if they don't exist yet. Create or update an Activity type = “track” and event = “Activity Upserted” Update an Activity in Pipedrive or create one if it doesn't exist. Create or update a Deal type = “track” and event = “Deal Upserted” Update a Deal in Pipedrive or create it if it doesn't exist yet. Create or update Lead type = “identify” Update a Lead in Pipedrive or create it if it doesn't exist yet. Create or update a Note type = “track” and event = “Note Upserted” Update a Note in Pipedrive or create it if it doesn't exist yet. --- ## PlayerZero URL: https://docs.customer.io/integrations/data-out/connections/playerzero/ Getting started Go to Data & Integrations > Integrations and select the PlayerZero entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Project Id: The Project ID where you want to send data. You can find this ID on the Project Data Collection page. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Track events Identify User type = “identify” Sets the user identity --- ## Qualtrics URL: https://docs.customer.io/integrations/data-out/connections/qualtrics/ Getting started Go to Data & Integrations > Integrations and select the Qualtrics entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Token: Qualtrics API token found in your Qualtrics account under "Account settings" -> "Qualtrics IDs." Datacenter: Qualtrics datacenter id that identifies where your qualtrics instance is located. Found under "Account settings" -> "Qualtrics IDs". Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Create and/or update contact in XM Directory type = “identify” Create and/or update contact in XM Directory. Updating is handled by contact deduplication in your directory settings. If deduplication is setup correctly this action will perform UPSERT operations on contacts Upsert contact transaction type = “track”, event = “Transaction Created” Add a transaction to a contact in Qualtrics directory. If the contact already exists, add the transaction. If the contact does not exist, create the contact first, then add the transaction record. Start a workflow in Qualtrics This action is used to kick off a workflow in Qualtrics --- ## Reporting Webhooks URL: https://docs.customer.io/integrations/data-out/connections/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 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. (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 RegionEU 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 136.111.237.157 34.10.127.179 Disabling webhooks If you want to stop sending webhook events, you can disable the webhook. Go to 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.  If you re-enable a webhook, you’ll only receive events from the moment you re-enable it. We don’t backfill or replay webhook events that occurred while the webhook was disabled. 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: 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. machine Only on email “clicked” events, indicates if the click was generated by a machine (boolean). 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. prefetched Only on email “opened” events, indicates if the email was opened by Gmail, Apple Privacy Protection, or a user agent identified as a machine (boolean). proxied Only on email “opened” events, indicates if the email was fetched by a proxy to hide the user identity, IP address, etc. (boolean). 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. tracked_response Only available for in-app message “clicked” events, the tracked response value if Track Clicks is enabled for the in-app message action/component that a person clicked/tapped. trigger_event_id The id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) 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 reporting 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 } Send rate, timeouts, and failures Our send rate is dynamic with no hard max; however, we roughly process in batches of 40 requests per workspace. The send rate for a single endpoint depends on: How fast you can accept our webhooks The total volume of reporting webhooks across our customer base at the time of your request We have a 4-second timeout for calls to your webhook endpoint. If we don’t get a successful (2xx) response during those 4 seconds, we retry the webhook over a period of 7 days with exponential backoff. Each time we check your workspace for reporting webhooks, we start by queuing the retries then check for new requests if there’s still room in the batch of requests. If your webhook server responds with any of the following status codes, we’ll add the failed call to a batch that we’ll retry after an hour. When we process the batch, we’ll move individual failures in the batch to a new batch/queue. Error codes: 400, 401, 402, 403, 404, 405, 410, 422,429, 500, 502, 521. Error responses: EOF, server misbehaving, connect: connection refused, read: connection reset by peer, tls: failed to verify certificate: x509: flowchart TD A(Send webhooks up to 40 at a time) A --> C{Did all webhooks return 2xx?} C ----->|yes|G(Send the next batch of webhooks) C -. no .-> D{Is the error 429 or client timeout?} D -. no .-> n2{Did all webhooks fail with an invalid integration error?} D -- yes --> n3(Retry failed webhooks individually) n3 --> n4{Do they still fail?} n4 -- yes --> n5(Move failed webhooks to the retry queue) n2 -- yes --> n6(Your integration is marked invalid. Retry webhooks after an hour.) n2 -. no .-> n5 n5 --> A If you have issues with your webhook server and you want 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 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: 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-Timestamp header 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 always v0. 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-Signature value 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 } 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 the email_drafted events for that campaign_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. Send third-party delivery metrics to Customer.io If you use outgoing webhooks to trigger messages from a third-party provider that is not natively integrated with Customer.io, you can send incoming calls to our Track metrics API to capture metrics in Customer.io. This allows you to see delivery metrics for all of your messages in one place. The following steps assume you already have at least one campaign that uses a Send and Receive Data webhook action to send messages via a third party’s API. To send delivery metrics back to Customer.io: Create a reporting webhook in Integrations where you pass webhook events. Send the reporting webhook to a URL from your third-party service provider. The third-party provider can reference the X-CIO-Delivery-ID in the header of the message to uniquely identify that message. From the third-party provider, you’ll send updates back via our Track API including delivery metrics (“delivered”, “bounced”, etc.). You can view the metrics anywhere you can see them within your Customer.io account, such as Campaign Metrics, as well as data-out integrations. Note that the metrics will show as Webhook messages, since that was the type of delivery from your campaign. 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 customer_subscribedcustomer_unsubscribedcio_subscription_preferences_changed customer_subscribed { "data": { "customer_id": "0200102", "identifiers": { "id": "0200102", }, "email_address": "test@example.com" }, "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "customer", "metric": "subscribed", "timestamp": 1585250199 } customer_unsubscribed { "data": { "customer_id": "0200102", "identifiers": { "id": "0200102", }, "email_address": "test@example.com" }, "event_id": "01E4C4C6P79C12J5A6KPE6XNFD", //the id of the reporting webhook instance "object_type": "customer", "metric": "unsubscribed", "timestamp": 1585250179 } cio_subscription_preferences_changed { "metric": "cio_subscription_preferences_changed", "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "delivery_type": "email", "timestamp": 1613063089, "data": { "content": "{\"topics\":{\"topic_1\":true}}", "customer_id": "42", "email_address": "test@example.com", "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "trigger_id": 1, "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "action_id": 96, "broadcast_id": 2, "journey_id": "01GW20GXAAXBKZD8J96M8FNV3R", "parent_action_id": 1 } } Email event examples email_draftedemail_attemptedemail_sentemail_deliveredemail_openedemail_clickedemail_convertedemail_unsubscribedemail_bouncedemail_droppedemail_spammedemail_failedemail_undeliverable email_drafted { "data": { "action_id": 36, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4G1S0AMNG0XVF2M7RPH5S", //the id of the reporting webhook instance "object_type": "email", "metric": "drafted", "timestamp": 1585250305 } email_attempted { "data": { "action_id": 36, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh", "failure_message": "from address can't be blank", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4HDQ7GH7M19ZKS39BDB73", //the id of the reporting webhook instance "object_type": "email", "metric": "attempted", "timestamp": 1585250350 } email_sent { "data": { "action_id": 36, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh", "recipient": "test@example.com", "subject": "hello", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C8AY5K21N2QNRBD9YXJ13Z", //the id of the reporting webhook instance "object_type": "email", "metric": "sent", "timestamp": 1585254331 } email_delivered { "data": { "action_id": 12042, "campaign_id": 1424, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "SA13dk35ja7s8d9kja3s2dASdasd==", "recipient": "test@example.com", "subject": "Thanks for joining!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01ASDG7S9P6MAZPTJ78JND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "delivered", "timestamp": 1234567890 } email_opened { "data": { "action_id": 36, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh", "recipient": "test@example.com", "subject": "hello", "proxied": false, "prefetched": true, "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C8BES586H0A5PFK1ARB9JW", //the id of the reporting webhook instance "object_type": "email", "metric": "opened", "timestamp": 1585254348 } email_clicked { "data": { "action_id": 36, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgABcRhIBqSp7kiPekGBIeVh", "href": "http://google.com", "link_id": 1, "machine": false, "recipient": "test@example.com", "subject": "hello", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C8BES5XT87ZWRJFTB35YJ3", //the id of the reporting webhook instance "object_type": "email", "metric": "clicked", "timestamp": 1585254348 } email_converted { "data": { "action_id": 12042, "campaign_id": 1424, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "SA13dk35ja7s8d9kja3s2dASdasd==", "recipient": "test@example.com", "subject": "Thanks for joining!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01ASDG7S9P6MYWPTJ78JND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "converted", "timestamp": 1234567890 } email_unsubscribed { "data": { "action_id": 37, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECAAFxEqBRJ6UXtUdDSeFe_L8=", "recipient": "test@example.com", "subject": "hello", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4S9V7WSQK80RJZC6ATRQX8B", //the id of the reporting webhook instance "object_type": "email", "metric": "unsubscribed", "timestamp": 1585692122 } email_bounced { "data": { "content_id": 1146, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RMehBAAAAXE7r_ONUGXly9DBGkpq1JS31=", "failure_message": "550 5.5.0 Requested action not taken: mailbox unavailable", "newsletter_id": 736, "recipient": "test@example.com", "subject": "Thanks for joining!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "12ASDG7S9P6MAZPTJ78DAND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "bounced", "timestamp": 1234567890 } email_dropped { "data": { "content_id": 1146, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RMehBAAasdAAXE7r_ONUGXly9DBGkpq1JS31=", "failure_message": "Not delivering to previously bounced address", "newsletter_id": 736, "recipient": "test@example.com", "subject": "Thanks for joining!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "31ASDG7S9P6MAZPTJ78DAND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "dropped", "timestamp": 1234567890 } email_spammed { "data": { "action_id": 73, "campaign_id": 52, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RObuBAXE4xnjV0vtJso_6xdRAas135", "recipient": "test@example.com", "subject": "Book now!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "31ASDG7SU6HSJ78DAND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "spammed", "timestamp": 1234567890 } email_failed { "data": { "action_id": 73, "campaign_id": 52, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "EFBAXE4xnjV0vtJso_6xdRAas135", "failure_message": "Customer is unsubscribed", "recipient": "test@example.com", "subject": "Book for vacation now!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "31ASDG7SU6JN78DAND9GDC", //the id of the reporting webhook instance "object_type": "email", "metric": "failed", "timestamp": 1234567890 } email_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "email", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "action_id": 96, "campaign_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "subject": "string", "recipient": "test@example.com", "failure_message": "Something went wrong!", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) } } Push notification event examples push_draftedpush_attemptedpush_sentpush_deliveredpush_openedpush_clickedpush_convertedpush_bouncedpush_droppedpush_failedpush_undeliverable push_drafted { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4G1S0HZ7C4220T6QNY8JX", //the id of the reporting webhook instance "object_type": "push", "metric": "drafted", "timestamp": 1585250305 } push_attempted { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAEABQFxN56fWzydfV4_EGvfobI=", "failure_message": "NoDevicesSynced", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4VSX8SZ0T9AQMH4Q16NRB89", //the id of the reporting webhook instance "object_type": "push", "metric": "attempted", "timestamp": 1585776075 } push_sent A person can have multiple devices. When we send a push, we can try to send it to more than one device—but that doesn’t mean that each device will receive the message. If we can’t send a message to a device in the recipients array, we’ll include a failure_message for it. { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "eeC2XC_NVPo:APA91bEYRSgmu-dAZcOWi7RzKBbT9gdY3WJACOpLQEMAmAOsChJMAZWirvSlSF3EuHxb7qdwlYeOyCWtbsnR14Vyx5nwBmg5J3SyPxfNn-ey1tNgXIj5UOq8IBk2VwzMApk-xzD4JJof", "device_platform": "android", "failure_message": "FCM_INVALID_TOKEN" } ] }, "event_id": "01E4C4HDQ7P1X9KTKF0ZX7PWHE", //the id of the reporting webhook instance "object_type": "push", "metric": "sent", "timestamp": 1585250350 } push_delivered { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "eeC2XC_NVPo:APA91bEYRSgmu-dAZcOWi7RzKBbT9gdY3WJACOpLQEMAmAOsChJMAZWirvSlSF3EuHxb7qdwlYeOyCWtbsnR14Vyx5nwBmg5J3SyPxfNn-ey1tNgXIj5UOq8IBk2VwzMApk-xzD4JJof", "device_platform": "android" } ] }, "event_id": "01E4C4HDQ7P1X9KTKF0ZX7PWHE", //the id of the reporting webhook instance "object_type": "push", "metric": "delivered", "timestamp": 1585250350 } push_opened Customer.io cannot track opened events for a push notification unless: You are using our mobile SDKs in your app. You report opened metrics to us using the API { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "eeC2XC_NVPo:APA91bEYRSgmu-dAZcOWi7RzKBbT9gdY3WJACOpLQEMAmAOsChJMAZWirvSlSF3EuHxb7qdwlYeOyCWtbsnR14Vyx5nwBmg5J3SyPxfNn-ey1tNgXIj5UOq8IBk2VwzMApk-xzD4JJof", "device_platform": "android" } ] }, "event_id": "01E4C4V2B3FW52RKEKP4QF2P74", //the id of the reporting webhook instance "object_type": "push", "metric": "opened", "timestamp": 1585250665 } push_clicked { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRhIBqSfeiIwdIYJKxTY", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "href": "ciosas://product/2", "link_id": 1, "recipients": [ { "device_id": "eeC2XC_NVPo:APA91bEYRSgmu-dAZcOWi7RzKBbT9gdY3WJACOpLQEMAmAOsChJMAZWirvSlSF3EuHxb7qdwlYeOyCWtbsnR14Vyx5nwBmg5J3SyPxfNn-ey1tNgXIj5UOq8IBk2VwzMApk-xzD4JJof", "device_platform": "android" } ] }, "event_id": "01E4V2SBHYK4TNTG8WKMP39G9R", //the id of the reporting webhook instance "object_type": "push", "metric": "clicked", "timestamp": 1585751829 } push_converted { "data": { "action_id": 37, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgUBcRiSO2rkbaiQ-5luSWXK", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "eeC2XC_NVPo:APA91bEYRSgmu-dAZcOWi7RzKBbT9gdY3WJACOpLQEMAmAOsChJMAZWirvSlSF3EuHxb7qdwlYeOyCWtbsnR14Vyx5nwBmg5J3SyPxfNn-ey1tNgXIj5UOq8IBk2VwzMApk-xzD4JJof", "device_platform": "android" } ] }, "event_id": "01E4XWX0NB4DH73NWDRTT71NMT", //the id of the reporting webhook instance "object_type": "push", "metric": "converted", "timestamp": 1585846320 } push_bounced { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAEABQFxN55vrab8yVNNVNI2Hxc=", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "my_android_device_id", "device_platform": "android", "failure_message": "FCM_INVALID_TOKEN" } ] }, "event_id": "01E4VSWX38K3R96QJ3B9N37KJR", //the id of the reporting webhook instance "object_type": "push", "metric": "bounced", "timestamp": 1585776063 } push_dropped { "data": { "action_id": 40, "campaign_id": 7, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECBQFxN6HHbPKYTzCT4XAS20Y=", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipients": [ { "device_id": "my_android_device_id", "device_platform": "android", "failure_message": "FCM_INVALID_TOKEN" } ] }, "event_id": "01E4VT612DR9BX6J1HXCBAYA1N", //the id of the reporting webhook instance "object_type": "push", "metric": "dropped", "timestamp": 1585776361 } push_failed { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECAQFxNeUBx6LqfjqrN1j-BJc=", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "failure_message": "Variable 'customer.test' is missing" }, "event_id": "01E4VSWX38K3R96QJ3B9N37KJR", //the id of the reporting webhook instance "object_type": "push", "metric": "failed", "timestamp": 1585747134 } push_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "push", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "action_id": 96, "broadcast_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "failure_message": "Something went wrong!" } } In-App message event examples in_app_draftedin_app_attemptedin_app_sentin_app_openedin_app_clickedin_app_convertedin_app_failedin_app_undeliverable in_app_drafted { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "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" } } } in_app_attempted { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "attempted", "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" }, "failure_message": "Something went wrong!" } } in_app_sent { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "sent", "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" }, "content": "string", "recipient": "test@example.com" } } in_app_opened { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "opened", "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" } } } in_app_clicked { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "clicked", "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" }, "recipient": "test@example.com", "href": "https://www.customer.io", "link_id": 1, "tracked_response": "Response One" } } in_app_converted { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "converted", "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" }, "recipient": "test@example.com" } } in_app_failed { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "failed", "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" }, "failure_message": "Something went wrong!" } } in_app_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "in-app", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "action_id": 96, "broadcast_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "failure_message": "Something went wrong!" } } SMS event examples sms_draftedsms_attemptedsms_sentsms_deliveredsms_clickedsms_convertedsms_bouncedsms_failedsms_undeliverablesms_replied sms_drafted { "data": { "action_id": 38, "broadcast_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRhIBqSZNqzgZVFoivwW" }, "event_id": "01E4C4G1S02P8D0G2JMY88KAFN", //the id of the reporting webhook instance "object_type": "sms", "metric": "drafted", "timestamp": 1585250305 } sms_attempted { "data": { "action_id": 41, "campaign_id": 7, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "ROk1AAIBcR4iT6mueuxiDtzO8HXv", "failure_message": "Twilio Error 21408: Permission to send an SMS has not been enabled for the region indicated by the 'To' number: +18008675309.", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4F3DCS83P8HT7R3E6DWQN1X", //the id of the reporting webhook instance "object_type": "sms", "metric": "attempted", "timestamp": 1234567890 } sms_sent { "data": { "action_id": 38, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRhIBqSZNqzgZVFoivwW", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipient": "+18008675309" }, "event_id": "01E4C4HDQ7KZF7AFPG6N2YQDJ0", //the id of the reporting webhook instance "object_type": "sms", "metric": "sent", "timestamp": 1585250350 } sms_delivered { "data": { "action_id": 38, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRh6qzHz-8gKvscP2UZa", "recipient": "+18008675309", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4XXG43MDMG47Z43V3090AW5", //the id of the reporting webhook instance "object_type": "sms", "metric": "delivered", "timestamp": 1585846946 } sms_clicked { "data": { "action_id": 38, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRh6qzHz-8gKvscP2UZa", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "href": "https://app.com/verify", "link_id": 1, "recipient": "+18008675309" }, "event_id": "01E4XXPN42JDF4B1ATQKTZ8WHV", //the id of the reporting webhook instance "object_type": "sms", "metric": "clicked", "timestamp": 1585847161 } sms_converted { "data": { "action_id": 38, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRh6qzHz-8gKvscP2UZa", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipient": "+18008675309" }, "event_id": "01E4XXPN42JDF4B1ATQKTZ8WHV", //the id of the reporting webhook instance "object_type": "sms", "metric": "converted", "timestamp": 1585847161 } sms_bounced { "data": { "action_id": 38, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgIBcRhIBqSZNqzgZVFoivwW", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "failure_message": "Twilio error code: 21704", "recipient": "+18008675309" }, "event_id": "01E4C4HENVDANXW94RQHHQYYDM", //the id of the reporting webhook instance "object_type": "sms", "metric": "bounced", "timestamp": 1585250351 } sms_failed { "data": { "action_id": 41, "campaign_id": 7, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "ROk1AAIBcR4iT6mueuxiDtzO8HXv", "failure_message": "Twilio Error 21408: Permission to send an SMS has not been enabled for the region indicated by the 'To' number: +18008675309.", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4F3DCS83P8HT7R3E6DWQN1X", //the id of the reporting webhook instance "object_type": "sms", "metric": "failed", "timestamp": 1234567890 } sms_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "sms", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "action_id": 96, "campaign_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "failure_message": "Something went wrong!" } } sms_replied This event occurs when someone replies to a message you sent them. We attribute inbound messages to the original delivery if the inbound message occurs within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can’t otherwise attribute the reply to a delivery, we won’t send this event. { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "sms", "timestamp": 1613063089, "metric": "replied", "data": { "inbound_event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", "to": "+15555555555", "recipient": "+15551234567", "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "action_id": 96, "campaign_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, } } Slack event examples slack_draftedslack_attemptedslack_sentslack_clickedslack_failedslack_undeliverable slack_drafted { "data": { "action_id": 39, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgQBcRhIBqRiZAc0fyQiLvkC", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4G1S0T3Y4V8W7F6MNFA8S", //the id of the reporting webhook instance "object_type": "slack", "metric": "drafted", "timestamp": 1585250305 } slack_attempted { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECAQFxNeUBx6LqfjqrN1j-BJc=", "failure_message": "Variable 'customer.test' is missing", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4TYA2KA9T0XGHCRJ784B774", //the id of the reporting webhook instance "object_type": "slack", "metric": "attempted", "timestamp": 1585747134 } slack_sent { "data": { "action_id": 39, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgQBcRhNAufb0s30bmz5HD7Y", "recipient": "#signups", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4TQKD6KJ274870J5DE2HB", //the id of the reporting webhook instance "object_type": "slack", "metric": "sent", "timestamp": 1585250655 } slack_clicked { "data": { "action_id": 39, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgQBcRhocpCJE3mFfwvRzNe6", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "href": "http://bing.com", "link_id": 1, "recipient": "#signups" }, "event_id": "01E4C6HJTBNDX18XC4B88M3Y2G", //the id of the reporting webhook instance "object_type": "slack", "metric": "clicked", "timestamp": 1585252451 } slack_failed { "data": { "action_id": 39, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgQBcRhIBqRiZAc0fyQiLvkC", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "failure_message": "value passed for channel was invalid", }, "event_id": "01E4C4HDQ77BCN0X23Z3WBE764", //the id of the reporting webhook instance "object_type": "slack", "metric": "failed", "timestamp": 1585250350 } slack_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "slack", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "action_id": 96, "campaign_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "failure_message": "Something went wrong!" } } Webhook example events webhook_draftedwebhook_attemptedwebhook_sentwebhook_clickedwebhook_failedwebhook_undeliverable webhook_drafted { "data": { "action_id": 40, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgEBcRhIBqSrYcXDr2ks6Pj9", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC" //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) }, "event_id": "01E4C4G1S04QCV1NASF4NWMQNR", //the id of the reporting webhook instance "object_type": "webhook", "metric": "drafted", "timestamp": 1585250305 } webhook_attempted { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECAQFxNeUBx6LqgjqrN1j-BJc=", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "failure_message": "Variable 'customer.test' is missing" }, "event_id": "01E4TYA2KA9T0XGHCRJ784B774", //the id of the reporting webhook instance "object_type": "webhook", "metric": "attempted", "timestamp": 1585747134 } webhook_sent { "data": { "action_id": 40, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgEBcRhNAufr2aU82jtDZEh6", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "recipient": "https://test.example.com/process" }, "event_id": "01E4C6EP0HCKRHKFARMZ5XEH7A", //the id of the reporting webhook instance "object_type": "webhook", "metric": "sent", "timestamp": 1585252357 } webhook_clicked { "data": { "action_id": 40, "campaign_id": 9, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RPILAgEBcRhNAufr2aU82jtDZEh6", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "href": "http://bing.com", "link_id": 1, "recipient": "https://test.example.com/process" }, "event_id": "01E4C6F5N1Y54TVGJTN64Y1ZS9", //the id of the reporting webhook instance "object_type": "webhook", "metric": "clicked", "timestamp": 1585252373 } webhook_failed { "data": { "action_id": 38, "campaign_id": 6, "customer_id": "0200102", "identifiers": { "id": "0200102", }, "delivery_id": "RAECAQFxNeK3bC4SYqhQqFGBQrQ=", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "failure_message": "HTTP 404 Not Found []" }, "event_id": "01E4TY5FVB0ZQ4KVDKRME0XSYZ", //the id of the reporting webhook instance "object_type": "webhook", "metric": "failed", "timestamp": 1585746984 } webhook_undeliverable { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", //the id of the reporting webhook instance "object_type": "webhook", "timestamp": 1613063089, "metric": "undeliverable", "data": { "customer_id": "42", "delivery_id": "ZAIAAVTJVG0QcCok0-0ZKj6yiQ==", "trigger_event_id": "21E4C3CT6YDC7Y4N7FE1GWWABC", //the id of the event that triggered an event-triggered campaign (not an API-triggered broadcast) "action_id": 96, "campaign_id": 2, "identifiers": { "id": "42", "email": "test@example.com", "cio_id": "d9c106000001" }, "failure_message": "Something went wrong!" } } 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: 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: email address of a user that does not exist inside Customer.io reply_to: 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 --- ## Ripe URL: https://docs.customer.io/integrations/data-out/connections/ripe/ There are two versions of this integration You’ll see two entries for Ripe in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the Ripe entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: The Ripe API key found in the Ripe App Endpoint: The Ripe API endpoint (do not change this unless you know what you're doing) Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Group type = “group” Group user in Ripe Identify type = “identify” Identify user in Ripe Page type = “page” Register page view in Ripe Track type = “track” Send user events to Ripe Getting started: web integration Go to Data & Integrations > Integrations and select the Ripe entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Sdk Version: The version of the Ripe Widget SDK to use Api Key: The Ripe API key found in the Ripe App Endpoint: The Ripe API endpoint (do not change this unless you know what you're doing) Click Enable Destination. Web integration actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Group type = “group” Group user in Ripe Identify type = “identify” Identify user in Ripe Page type = “page” Register page view in Ripe Track type = “track” Send user events to Ripe --- ## Rudderstack (legacy) URL: https://docs.customer.io/integrations/data-out/connections/rudderstack-legacy/ Set up Customer.io as a Rudderstack source, and use Reporting Webhooks to send email events from your workspace to any of Rudderstack's destinations. How it works Rudderstack takes advantage of reporting webhooks when using Customer.io as a source. Rudderstack converts our webhooks to their event format. A person’s customer_id (a legacy identifier for id) in Customer.io becomes their userId in Rudderstack. If a person doesn’t have an id, Rudderstack sets a person’s email address as an anonymousId. { "event_id": "01E4C4CT6YDC7Y5M7FE1GWWPQJ", "object_type": "email", "timestamp": 1613063089, "metric": "opened", "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" }, "content": "string", "subject": "string", "recipient": "test@example.com" } } Set up Go to Rudderstack and click Add Source. Select CustomerIO from the list of Event Stream sources. Assign a Name for your source and click Next. To connect this source to a RudderStack warehouse destination, the source name should match the name of your warehouse schema. Note the source Write key: you’ll need this key for your reporting webhook URL. In Customer.io, go to Integrations and find the Reporting Webhooks integration. Click Add Reporting Webhook and add the webhook URL from the Source Settings page in your RudderStack dashboard. Make sure you add your source’s write key as query parameter to the URL. See Rudderstack’s documentation for more information about the data plane URL. Select the events that you want to send to Rudderstack. Rudderstack only supports the following Email Events: Delivered The delivery provider reported that the email was delivered to the recipient’s inbox. Opened A recipient opened an email. Clicked A recipient clicked a tracked link in an email. Bounced The delivery provider reported that it was unable to deliver an email to a recipient. Spammed A recipient marked an email as spam. Unsubscribed A recipient unsubscribed via a particular email. Click Save and Enable Webhook. Supported events RudderStack only supports the following email events. Email Event Description Delivered The delivery provider reported that the email was delivered to the recipient’s inbox. Opened A recipient opened an email. Clicked A recipient clicked a tracked link in an email. Bounced The delivery provider reported that it was unable to deliver an email to a recipient. Spammed A recipient marked an email as spam. Unsubscribed A recipient unsubscribed via a particular email. --- ## About this integration URL: https://docs.customer.io/integrations/data-out/connections/salesforce/salesforce-intro/  Do you want to capture data from Salesforce? Check out our Salesforce Source. With the Salesforce data-in and data-out integrations, you can set up a complete, end-to-end integration! Getting started Go to Data & Integrations > Integrations and select the About this integration entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Mapping actions to Salesforce Operations When you set up an action, you’ll select the Operation that you want to perform on a record. Our outgoing Salesforce integration supports the following operations: Create: Pushes new records to Salesforce. This operation works when you want to push new records but don’t need data within each row to remain up to date or when you’re working with static data that you won’t update later, like events. Upsert: Pushes new records to Salesforce and updates fields on existing records in Salesforce. This operation helps you push records into Salesforce and keep data up to date. Update: Updates fields on existing records in Salesforce. This operation works when you know that you already have records in Salesforce that you update or add new information to Delete: Deletes existing records in Salesforce. Matching records with delete, update, and upsert When you use the delete, update and upsert operations, you need to specify one or more Record Matchers. A record matcher is a value we’ll query Salesforce for to find and match the records you want to upsert, delete, or update. You can use any field in your source data as a record matcher, including: External IDs. To map an External ID, the Salesforce API name should have __c appended to it. Record IDs. To map a Record ID, the Salesforce API name is Id. Standard fields. To map a standard field, the Salesforce API name should match what is in Salesforce for the given field, for example Email. Custom fields. To map a custom field, the field needs to be predefined in Salesforce and the Salesforce API name should have __c appended to it. You can also set the Record Matchers Operator if you have multiple record matchers. This determines whether Salesforce matches on any record matcher (OR), or all record matchers (AND). If you have multiple Record Matchers, you should use fields that result in unique records. If the operator is set to Or and Salesforce finds multiple matching records, it won’t perform the operation. We’ll record a 300 status for the request, and will not retry it. Note that you should only set Record Matchers that that have the “Filter” property in Salesforce; these are fields Salesforce can query. For example, we can’t perform a Salesforce query on the Description field because it is not a filterable property. You can look up the standard field properties in Salesforce’s API documentation to determine if a field is is a valid Record Matcher. --- ## Sent messages as tasks URL: https://docs.customer.io/integrations/data-out/connections/salesforce/tasks-in-sf/ You can send message information to Salesforce—the emails you send, whether or not people click them, and so on. In Salesforce, you'll typically record these events as **Tasks**. When you send [message events](#message-event-triggers) to Salesforce, you'll need to map a few fields to make sure that Salesforce processes your message activity *tasks* properly. How it works The Task for Message Delivery action records message events from Customer.io as completed tasks in Salesforce. This makes it easy for you to see the messages you’ve sent, whether people have interacted with them, and so on—all in Salesforce. This helps you make your Salesforce environment a reliable source of truth. To take advantage of this action, you need to make sure that you sync people from Salesforce to Customer.io with their Contact ID or Lead ID. That’s how we associate tasks with people in Salesforce. Set up the Task for Message Delivery action By default, we only record tasks for emails, but you could set up different actions to record tasks for other message types. See Message event triggers for more information. Go to Integrations and select your Salesforce Data out integration. Go to the Actions tab and click Add Action. Select Task for Message Delivery. Scroll down to The Contact or Lead ID to associate the task with (string), and select the attribute that contains your audience’s Contact or Lead ID. If you store both contact and lead IDs, you can use the coalesce function to use the contact ID if it exists, and fall back to the lead ID if the person hasn’t graduated to a contact yet: coalesce($.customer.contact_id, $.customer.lead_id). Click Save action. Now Salesforce will record completed tasks for email events. What value is the Contact or Lead ID in Customer.io? We need the identifier for a person in Salesforce so that we can associate tasks with them—typically their Contact or Lead ID; you’ll set it in the The Contact or Lead ID to associate the task with (string) when you set up the Task for Message Delivery action. You typically map this value to an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. in Customer.io when you set up your Salesforce data-in integration, but if you don’t know what attribute stores your contact or lead IDs, you can find or update this value in your Salesforce data-in integration. Learn more about mapping lead and contact IDs in Customer.io. Go to Integrations and select your Salesforce Data in integration. Go to Syncs. Find your contact or lead sync and click Settings > Edit. Click Edit next to Fields to Sync and find the attribute that contains the Contact or Lead ID. If you’ve renamed the attribute, you’ll see the name of the attribute you’ve set. If you haven’t already enabled the contact or lead ID, toggle it on and click Rename to set the name of the attribute you want to use to store the Contact or Lead ID, like contact_id or lead_id. This value is the ID you’ll use to associate the task with a person in Salesforce in the format $.customer.<attributeName>, e.g. $.customer.id. What if I send a message to someone who isn’t in Salesforce? Because we look for a value representing a person’s Contact or Lead ID in Customer.io—and that’s a condition for the action itself—Customer.io will ignore any message events for people who don’t have a Contact or Lead ID. If you send a message to a person who isn’t in Salesforce, you won’t see anything in the Data out tab for your Salesforce integration. Capturing a subset of events By default, the Task for Message Delivery action captures all email events. That may include events that you don’t want to record as tasks. For example, you may only care about sends, clicks, and opens; you may only want to record tasks for specific campaigns or emails by their subject. If you only want to capture specific email events, you can edit the trigger to capture only the events you want. But, because triggers don’t support nested and/or conditions, you may need to create different actionsA block in a campaign workflow—like a message, delay, or attribute change. for each set of criteria you use. Go to Integrations and select your Salesforce Data out integration. Go to the Actions tab and click Edit next to the Task for Message Delivery action. Update the Trigger to capture the events you want. You may want to use the subject field to capture a specific email. Or you could set the Track Event Name to Email Opened to capture all email opens but not other event types. We’ve provided examples below.  You might need to create multiple actions Because triggers don’t support nested and/or conditions, you may need to create different actions for each set of criteria you use. For example, you may want to create an action for each email event you want to capture—e.g. one trigger for Email Sent and another for Email Opened if you only want to capture those two events. Email subject trigger Specific email event trigger Click Save action. Capture non-email message events By default, the Task for Message Delivery action captures email-based message events. You can edit the trigger to capture different message events, but different message events contain different data—this may change the values you want to map to Salesforce tasks. We recommend that you create different actions for different message types. We default to the email task type, but you can edit the task type to match the message type. Go to Integrations and select your Salesforce Data out integration. Go to the Actions tab and click Add Action. Select Task for Message Delivery. In the Trigger step change the Track Event Name to the kind of message events you want to capture: Push, SMS, or In-App. Click Save action. Now Salesforce will record completed tasks for each non-email message event. Set up an action to create tasks in Salesforce Attribute enrichment requests Message events from Customer.io don’t necessarily contain a person’s Contact or Lead ID, so we need to look up the person’s Contact or Lead ID in Customer.io. Then we send that enriched event to Salesforce, where it creates a task. This means that for each message event, you’ll see two requests in the Data out tab for your Salesforce integration: one internal request to look up the Contact ID in Customer.io and the actual request to create the task in Salesforce. Since this second request is the only one that goes to Salesforce, this model doesn’t increase the number of Salesforce API calls you make as a part of your Customer.io integration. In general, you don’t need to worry about the internal attribute enrichment requests, but it can help you see what data we’re fetching from Customer.io to map to users in Salesforce. Message event triggers By default, the Task for Message Delivery action captures all email events listed below. You can edit the trigger to capture different events for different message channels (push, sms, in-app), but different message events contain different data—this may change the values you want to map to Salesforce tasks—like the subject line. We recommend that you create different actions for different message types. Name Description Subscribed A person’s unsubscribed attribute was set to false. Unsubscribed A person’s unsubscribed attribute was set to true. Subscription preferences changed A person’s subscription preferences were changed—they subscribed to, or unsubscribed from, a topic. Email Drafted Customer.io successfully rendered a message and populated 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}}. for an individual recipient. The message is ready to send. 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 Link Clicked A tracked link in an email was clicked. Email Converted A person matched conversion criteria 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 Marked as Spam An email was marked as spam by the recipient. Email Failed An email couldn’t be sent to the delivery provider. In-App Drafted Customer.io successfully rendered an in-app message and is ready to send it. In-App Attempted An in-app that couldn’t be sent will be retried. In-App Sent An in-app was sent. In-App Opened An in-app was opened. In-App Clicked A tracked response in an in-app was clicked. In-App Converted A person matched conversion criteria attributed to an in-app. In-App Failed An in-app couldn’t be sent. Push Drafted Customer.io successfully rendered a push notification and populated 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}}. for an individual recipient. The message is ready to send. 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 Link 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. If you want to track links specifically, you’d need to send this metric back to us through the metric reporting API. Push Converted A person matched conversion criteria 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 Customer.io successfully rendered an SMS message and populated 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}}. for an individual recipient. The message is ready to send. 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 Link Clicked A tracked link in an SMS has been clicked. SMS Converted A person matched conversion criteria 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 Customer.io successfully rendered a slack message and populated 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}}. for an individual recipient. The message is ready to send. 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 Link 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 Customer.io successfully populated a webhook with 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}}. and is ready to send it. 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 Link Clicked A tracked link in a webhook payload has been opened. Webhook Failed A webhook couldn’t be sent to the specified Webhook URL. --- ## Frequently Asked Questions URL: https://docs.customer.io/integrations/data-out/connections/salesforce/salesforce-faq/ Get answers to common questions about our Salesforce data-out integration. Salesforce OAuth connection limits You should not create more than five Salesforce connections (between data-in and data-out) per user. Salesforce enforces a limit of five connections per OAuth-enabled Connected Application, per user. When you hit this limit, Salesforce revokes the oldest connection. This limit is not configurable and Customer.io is not notified when Salesforce revokes a connection. Because this limit is per user, you can circumvent the limit by creating multiple Salesforce integration users and carefully managing their connections—no more than five connections per user.  What counts towards the limit? Salesforce counts an “authorized connection” when you click Connect and set up your Salesforce integration. Even if you don’t complete the connection flow, Salesforce counts it towards the limit! How do I enable a sandbox instance? To send data to a Salesforce sandbox instance, go to your integration’s Settings, enable the Sandbox Instance setting and click Connect. If you already connected to a different Salesforce instance, you’ll need to disconnect and reconnect with your sandbox username. Your Salesforce sandbox username is your Salesforce production username with your sandbox name at the end. For example, if your username for a production instance is user@example.com and your sandbox is named test, your sandbox username is user@example.com.test. How do I add custom fields? You can add additional, custom fields to any action under Additional Fields. You must define fields in Salesforce before you send them from your source data. When you set up custom fields in Salesforce, the names of your custom fields should end with __c (for example, My_Custom_Field__c). You should include the __c in your mapping. You can see Salesforce API names in Salesforce under Setup > Objects and Fields > Object Manager > Select your object > Fields & Relationships > FIELD NAME. See Salesforce’s Create Custom Fields documentation for more information. How do I associate a Contact with an Account? To associate a Contact with an Account, you must include the AccountId on the Contact record. The AccountId is a Salesforce-generated ID assigned to the account when you create it (i.e. 0018c00002CDThnAAH). Salesforce only accepts a single AccountId as the Contact’s primary account; you can’t pass an array of IDs. How do I send data for Person Accounts? A Person Account is a special type of account that represents an individual rather than a business. Our standard Account action isn’t designed for Person Accounts, so you’ll need to use our Custom Object action if you want to map data to Person Accounts. Person Accounts have specific field requirements. For example, Name is required for Accounts, but LastName is required for Person Accounts. Hard code the Salesforce Object to Account and define other standard and custom fields, such as LastName and FirstName, in the Other Fields mapping. How many API calls do you make to Salesforce? The update and upsert operations take two API calls per action—one as a query to determine whether a record already exists in Salesforce or not and a second API call to update or create that record. All other operations are a single call. To check how many API calls you have left in Salesforce, go to Setup > Company Settings > Company Information, and you’ll find a field labeled: API Requests, Last 24 Hours. How does Salesforce Bulk API work? When you enable Use Salesforce Bulk API is enabled for an action, we send data to Salesforce’s Bulk API 2.0 rather than their streaming REST API. We’ll collect source data in batches of 1000 calls before we perform actions in Salesforce. You can only use the bulk API for upsert or update operations. For bulk update, if a record in a batch is missing a Bulk Update Record ID, we still send it to Salesforce. Salesforce will reject the individual record because it won’t find a record to update. But Salesforce will still process other records in the batch. When records fail this way, the Data Out tab won’t report it. We’ll show the entire batch as successful because we only know if the bulk request is successful or not; we don’t know the status of individual records in the batch. --- ## SalesWings URL: https://docs.customer.io/integrations/data-out/connections/saleswings/ Getting started Go to Data & Integrations > Integrations and select the SalesWings entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: API key for your SalesWings project. Environment: SalesWings environment this destination is connected with. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Submit Track Event type = “track” Send your Track events to SalesWings to use them for tagging, scoring and prioritising your leads. Submit Identify Event type = “identify” Send your Identify events to SalesWings to use them for tagging, scoring and prioritising your leads. Submit Page Event type = “page” Send your Page events to SalesWings to use them for tagging, scoring and prioritising your leads. Submit Screen Event type = “screen” Send your Screen events to SalesWings to use them for tagging, scoring and prioritising your leads. How we map users to SalesWings leads When you send events to SalesWings, SalesWings creates a lead-profiles based on the userId/anonymousId and the email trait. SalesWings displays leads that are identified with an email, but it doesn’t display profiles represented by userId/anonymousId. SalesWings will log all source data that comes in before you identify someone with an email trait, but it won’t show that data on a lead-profile until you identify someone with an email trait. Once you identify someone with an email trait, SalesWings will merge the data from the anonymous lead-profile into the identified lead-profile. How we map events to SalesWings lead activities Page events are registered as Page-Visit activities in a SalesWings lead. To make use of these activities for tags and scores in the Falcon engine, use the “Page Visit” condition. Track, Identify, and Screen events are registered as Custom-Event activities of a SalesWings lead. To make use of these activities for tags and scores in the Falcon engine, use the “Custom Event” condition. When you add a Track, Identify, or Screen action, you control how a corresponding Custom-Event activity appears in SalesWings. When you see a Custom-Event activity in the SalesWings cockpit or the SalesWings Lead Intent View in Salesforce, the activity is visualized as [Kind] Data. When adding an action for Track, Identify, or Screen calls, you can configure how Kind and Data fields are formed in SalesWings. The action configuration has the following defaults: Source Event Kind Data Custom Event Activity Track Track The name of the Track event (e.g. User Registered) [Track] User Registered Identify Identify The email address as identified by your source [Identify] peter@example.com Screen Screen The name of the screen [Screen] Home View You can override these defaults when you configure an action and map Kind and Data to static values, or map them to other properties that are parts of your source events. If you have the Custom Attributes feature enabled in SalesWings, you can configure SalesWings Custom Attributes based on source events properties (for Track and Screen events) and traits (for Identify events). When you add a Custom Attribute with an ID that matches a property or a trait name in Customer.io, you will see the Custom Attribute values on the lead profiles created in SalesWings. Configuring multiple actions for the same event type You can add multiple actions actions for the same event type—like track events, for example. But the default trigger every SalesWings action is simply the event type (for example, Event Type = Track). If you add multiple actions for the same event type, make sure to configure mutually exclusive triggers based on event names or other properties in your events. If your action triggers are not mutually excelusive, you’ll register multiple SalesWings lead activities for the same source event. --- ## Segment URL: https://docs.customer.io/integrations/data-out/connections/segment/ Getting started Go to Data & Integrations > Integrations and select the Segment entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Source Write Key: The Write Key of a Segment source. Endpoint: The region to send your data. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send Identify type = “identify” Send an identify call to Segment’s tracking API. This is used to tie your users to their actions and record traits about them. Send Group type = “group” Send a group call to Segment’s tracking API. This is used to associate an individual user with a group Send Screen type = “screen” Send a screen call to Segment’s tracking API. This is used to track mobile app screen views. Send Page type = “page” Send a page call to Segment’s tracking API. This is used to track website page views. Send Track type = “track” Send a track call to Segment’s tracking API. This is used to record actions your users perform. --- ## Segment (Message Metrics) URL: https://docs.customer.io/integrations/data-out/connections/segment-legacy/ Set up Customer.io as a Segment Source to pipe data out of Customer.io and into any one of Segment’s hundreds of Destination [integrations](https://segment.com/catalog/). This integration sends data to Segment in `track()` calls. You can send any event supported by our [reporting webhooks](/integrations/api/webhooks/) to Segment.  Disable and re-enable this integration to gain access to new features! We updated this integration in May of 2022. If you enabled your Segment Source integration before then, disable the integration and re-enable it to gain access to new events and settings. Enable Customer.io as a Segment source To enable Customer.io as a Segment source, you need both a Customer.io and a Segment.com account. If you are creating a new Segment account, enter “Customer.io” in the How Did You Hear About Us? field.  We send anonymous events for people who do not have an ID If a person has an ID, we send events with the ID as the Segment userId. If a person has an email address but does not have an ID, we send anonymous events with the email address as the anonymousId. In the Segment interface, Go to Connections, click Add Source, and select Customer.io. Give your source a Name and a Label, and click Add Source. The Name helps you identify your source in Segment, and the label helps you organize sources in your Segment workspace. On the Overview page, copy your Write Key. In your Customer.io workspace, go to Integrations and select the Segment Source integration. You can find it in the Data Management category. Paste your Write Key in the field and click Connect Segment. Enable the events you want to send from Customer.io to Segment. Set your integration options: Send Frequency: Determine whether to send every instance of every event or only the first time an event occurs. Body Content: Enable this to include your message’s body content in all of the “Sent” events we send to you. Send segment events back to Customer.io destinations: This setting lets you send events from Customer.io, as a Segment source, back to Customer.io as a Segment destination. In general, we suggest that you leave this feature disabled. The data you send to Segment is already available in Customer.io. Learn more. Segment Data Ingestion Region: Set the region you want to ingest data into. In Segment, turn on the destinations you want to send your Customer.io data to. How we map people to Segment Our Segment Source integration maps a person’s id in Customer.io to userId in Segment. If your Customer.io workspace supports both email and id as identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., people in your workspace can have an email but not an id. In this case, we use a person’s email as their anonymousId in Segment. For more information about identifying people, see Segment’s Identify API. Person without ID Person without ID cioanalytics.track("New Lead", { anonymousId: "peter@example.com" name: "Peter Gibbons", email: "peter@example.com" }); Person with ID Person with ID cioanalytics.track("New Customer", { userId: "97980cfea0067", name: "Peter Gibbons", email: "peter@example.com", plan: "premium" }); Send segment events back to Customer.io destinations We give you the option to feed events from your source back to Customer.io destinations. In general, you should not enable this option. The data that you send to Segment is already available in your workspace. Sending it back to your workspace simply makes this data available as events. Rather than looping events back into your workspace, you can use our data-driven segment builderA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. to trigger campaigns based on your Message Data. You can then trigger campaigns based on the ways in which people respond to your messages without sending duplicate data back into your workspace. Track Payloads: confirm that your Segment Source works You can confirm that your Customer.io Source is working by sending yourself a test campaign and checking the debugger. Customer.io can send any of our reporting webhook events to Segment, reshaped specifically for Segment’s track calls. We’ve provided some examples below to help you understand the information that you’ll see in Segment. If you enable the Body Content setting, the events below will also include a data.content key representing the body of your message.  If a person doesn’t have an ID, we send anonymousId The payloads below all show users with an ID, which we pass to Segment as a userId. If a person doesn’t have an ID, userId will be an empty string and the payload will include an anonymousId populated with the person’s email address. Email Opened  Only sent events contain content If you enable the Include body content in all Sent events setting, Email Sent events will contain body content. For all other email events, content is null. { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "cool.person@example.com" } }, "event": "Email Opened", "integrations": { "Customer.io": false }, "messageId": "api-1u21CJeZPJX3TdLhPNrzstyAYZa", "originalTimestamp": "2021-06-16T13:04:06Z", "properties": { "action_id": 16, "campaign_id": 1, "delivery_id": "dgP6xQb6xQYDAAF6FOl-vmR0E7zoPqTRiAo=", "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "subject": "Test Subject", "content": null, //null unless Body Content is enabled and event is Email Sent "recipient": "cool.person@customer.io", "userId": "cool-person" }, "receivedAt": "2021-06-16T13:05:39.605Z", "sentAt": "2021-06-16T13:05:39.000Z", "timestamp": "2021-06-16T13:04:06.605Z", "type": "track", "userId": "cool-person", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } SMS Sent { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "cool.person@example.com" } }, "event": "SMS Sent", "integrations": { "Customer.io": false }, "messageId": "api-28wdXq0yQP8jesjRYdLmj2dxiaM", "originalTimestamp": "2022-05-09T21:27:02.000Z", "properties": { "action_id": 41, "anonymousId": "cool.person@example.com", "campaign_id": 7, "content": "Hey, just testing sms segment stuff.", "delivery_id": "RKGZAQACAYCqt-kVejTLzrd-t9D_sQ==", "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "recipient": "15555555555", "userId": "cool.person@example.com" }, "receivedAt": "2022-05-09T21:27:03.384Z", "sentAt": "2022-05-09T21:27:03.151Z", "timestamp": "2022-05-09T21:27:02.233Z", "type": "track", "userId": "karn@customer.io", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } SMS Link Clicked { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "cool.person@example.com" } }, "event": "SMS Link Clicked", "integrations": { "Customer.io": false }, "messageId": "api-1u242Y8g1XCtEl9rEp7NwBx3K9o", "originalTimestamp": "2021-06-16T13:29:01Z", "properties": { "action_id": "n18", "campaign_id": 3, "delivery_id": "dgP6xQb6xQYDAAF6FQJvh4yaVoIOMGeCVp4=", "content": null, //null if Body Content is disabled "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "recipient": "15555555555", "userId": "cool.person@example.com", "link": { "id": 1, "url": "https://customer.io/features" } }, "receivedAt": "2021-06-16T13:29:03.021Z", "sentAt": "2021-06-16T13:29:02.000Z", "timestamp": "2021-06-16T13:29:02.021Z", "type": "track", "userId": "cool-person", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } Push Sent { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "cool.person@example.com" } }, "event": "Push Sent", "integrations": { "Customer.io": false }, "messageId": "api-1u21CJeZPJX3TdLhPNrzstyAYZa", "originalTimestamp": "2021-06-16T13:04:06Z", "properties": { "action_id": "n16", "campaign_id": 1, "delivery_id": "dgP6xQb6xQYDAAF6FOl-vmR0E7zoPqTRiAo=", "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "recipients": [ { "device_id": "15ad6c7ddc29f1d6a28580b040ddddfb831fd009bafac1dff718be7ed4233c15", "device_platform": "ios" } ], "userId": "cool.person@example.com" }, "receivedAt": "2021-06-16T13:05:39.605Z", "sentAt": "2021-06-16T13:05:39.000Z", "timestamp": "2021-06-16T13:04:06.605Z", "type": "track", "userId": "cool-person", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } Slack Sent { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "sales@example.com" } }, "event": "Slack Sent", "integrations": { "Customer.io": false }, "messageId": "api-1u21CJeZPJX3TdLhPNrzstyAYZa", "originalTimestamp": "2021-06-16T13:04:06Z", "properties": { "action_id": "n16", "campaign_id": 1, "delivery_id": "dgP6xQb6xQYDAAF6FOl-vmR0E7zoPqTRiAo=", "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "recipient": "#cool-channel", "userId": "cool.person@example.com" }, "receivedAt": "2021-06-16T13:05:39.605Z", "sentAt": "2021-06-16T13:05:39.000Z", "timestamp": "2021-06-16T13:04:06.605Z", "type": "track", "userId": "sales-team", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } Webhook Sent { "context": { "integration": { "name": "customer.io", "version": "2.0.0" }, "library": { "name": "unknown", "version": "unknown" }, "traits": { "email": "cool.person@example.com" } }, "event": "Webhook Sent", "integrations": { "Customer.io": false }, "messageId": "api-1u21MrfL8kIJwhuNanhhBbuc9gq", "originalTimestamp": "2021-06-16T13:05:55Z", "properties": { "action_id": "n17", "campaign_id": 2, "delivery_id": "dgP6xQb6xQYDAAF6FOsF0yqZiIo1NgezYcI=", "journey_id": "01G2NBFT8N2MYDTT3MHWK7Z6C6", "recipient": "https://example.com/webhook-url", "userId": "cool.person@example.com" }, "receivedAt": "2021-06-16T13:07:04.404Z", "sentAt": "2021-06-16T13:07:04.000Z", "timestamp": "2021-06-16T13:05:55.404Z", "type": "track", "userId": "cool-person", "writeKey": "aSOzeRCalHEDr3H5BslyxT4G35vDJBna" } If events flow into your Segment debugger, you’ll know that the Source is properly enabled. Upgrade your integration If you had the previous iteration of this integration enabled, you were only able to send email events to Segment. To update your integration to version 2.0, disable the integration and then re-enable it. When you re-enable the integration, you’ll have access to the new event types and other settings made available in May of 2022. Differences between versions 1.0 and 2.0 When updating from version 1.0 to 2.0, you’ll notice the following changes: version is incremented from 1.0.0 to 2.0.0. email_id is no longer included in Email events. This field duplicated the delivery_id. When you enable Body Content for email events, email_subject has changed to simply subject. Events no longer include action_name, campaign_name, newsletter_name, or transactional_message_name. We’ve removed these items to improve performance. Instead, you can retrieve these values using the corresponding object id (ie campaign_id) and our App API. Need help? If you run into issues enabling the Customer.io Source, finding your Write Key, setting up a Segment workspace, or find that your data isn’t syncing, contact Segment’s support team. Not seeing Customer.io events in the Segment debugger? Get in touch with support! --- ## SendGrid Marketing Campaigns URL: https://docs.customer.io/integrations/data-out/connections/sendgrid/ Use cases Keep your SendGrid contacts up to date by syncing customer profiles from your source. When someone signs up or updates their profile, their contact information in SendGrid updates automatically. Build targeted email lists in SendGrid based on traits you send from Customer.io. Sync attributes like plan type, company size, or engagement score so you can segment your SendGrid lists. Centralize your contact data across platforms. If you use SendGrid for marketing emails and Customer.io for other channels, this integration keeps both systems in sync without manual imports. Getting started Go to Data & Integrations > Integrations and select the SendGrid Marketing Campaigns entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Send Grid Api Key: The Api key for your SendGrid account. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Upsert Contact Add or update a Contact in SendGrid. Data mapping The Update User Profile action maps incoming data to SendGrid contact fields. Here’s how the default mappings work: SendGrid field Default mapping Description Email (primary) $.traits.email or $.properties.email Required. The contact’s email address. SendGrid uses this to find and update existing contacts. First name $.traits.first_name or $.properties.first_name The contact’s first name. Last name $.traits.last_name or $.properties.last_name The contact’s last name. Country $.traits.address.country or $.properties.address.country Two-letter country code. Postal code $.traits.address.postal_code or $.properties.address.postal_code Postal or zip code. City $.traits.address.city or $.properties.address.city City name. State $.traits.address.state or $.properties.address.state State or province. Address line 1 $.traits.address.line1 or $.properties.address.line1 Street address. Address line 2 $.traits.address.line2 or $.properties.address.line2 Apartment, suite, or unit. Phone number $.traits.phone or $.properties.phone The contact’s phone number. Custom fields To send additional traits beyond the default fields, create Custom Fields in SendGrid Marketing Campaigns before sending data. SendGrid won’t automatically create fields for unrecognized traits. In SendGrid, go to Marketing > Custom Fields and create a field for each trait you want to sync. In your action configuration, map the trait to a custom field key. The key must match the field name in SendGrid Marketing Campaigns exactly. For example, if you have a company trait and you’ve created a company Custom Field in SendGrid, you can map $.traits.company to the company key in your action.  If you map a trait to a key that doesn’t exist as a Custom Field in SendGrid, the entire request fails—not just the unrecognized field. Make sure all mapped keys have corresponding Custom Fields in SendGrid. Recording a userId SendGrid Marketing Campaigns doesn’t have a built-in userId field. To record a userId in SendGrid, pass it as a trait in your identify calls and create a matching Custom Field in SendGrid. { "type": "identify", "userId": "user-123", "traits": { "email": "person@example.com", "userId": "user-123", "first_name": "Jane", "last_name": "Doe" } } Things to know Email is the primary identifier. SendGrid uses the email address to find and update existing contacts. Every request must include an email address—without one, the action fails. Create Custom Fields in advance. Unlike Customer.io, SendGrid doesn’t auto-create fields. If you send a trait that doesn’t match a Custom Field, the request fails. Contact updates aren’t instant. SendGrid processes contact updates asynchronously. Changes may take a few minutes to appear in your SendGrid contact list. This integration syncs Marketing Campaigns contacts only. It doesn’t send emails or manage email content. It syncs contact data so you can use SendGrid’s built-in tools for list management and email campaigns. --- ## Slack URL: https://docs.customer.io/integrations/data-out/connections/slack/ Use cases Alert your sales team when a high-value lead signs up or reaches a usage milestone. Post to your #sales-alerts channel with the person’s name, plan, and key traits. Notify support when a customer submits a negative NPS score or cancels their subscription, so your team can reach out proactively. Keep your team in the loop on product activity—post to a channel when someone completes onboarding, upgrades their plan, or hits an error threshold. Monitor data pipeline health by posting to an internal channel when specific track events fire, confirming that your integration is working as expected. Getting started Go to Data & Integrations > Integrations and select the Slack entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Post Message Post a message to the specified Slack workspace and channel when the associated trigger criteria are met. Post message Set up at least one Post Message action. This action posts a message to a channel in your Slack workspace when an incoming data trigger fires. Go to Actions for your Slack integration and click Add Action to set up the Post Message action. Set up the Trigger for the action. This determines when Customer.io posts to Slack. Under Data Structure, enter the Webhook URL for your Slack workspace and channel. See Slack’s documentation for help creating an incoming webhook. Enter the Message you want to send. You can include variables in your message! Enter the Channel you want to send your message to. Set the User. This controls who the message appears from. (Optional) Enter the Icon URL for the user. This sets the profile picture for your integration. Use variables in your messages You can use variables in the Message field to include data from the incoming event. For example, if someone triggers a track call with the event name Order Completed, you might send a message like: 🎉 {{$.properties.name}} just completed an order for {{$.properties.total}}! This pulls the name and total properties from the incoming track event and includes them in your Slack message. Send to different channels Each Post Message action targets a single Slack webhook URL, which maps to a specific channel. If you want to post to multiple channels based on different events, create separate Post Message actions—one per channel. For example, you might have: A #sales-alerts action triggered by identify calls where $.traits.plan equals enterprise A #support-alerts action triggered by track calls where $.event equals NPS Submitted Things to know Webhook URLs are channel-specific. Each Slack incoming webhook is tied to a single channel. You’ll need a separate webhook (and action) for each channel you want to post to. Messages are plain text. This integration posts text messages. It doesn’t support Slack’s Block Kit for rich formatting. The default username is “Customer.io Data Pipelines.” Change this per action by updating the User field. Rate limits apply. Slack limits incoming webhooks to roughly 1 message per second per webhook URL. If you send high-volume events to Slack, some messages may be dropped. Use this integration for important alerts, not high-frequency event streams. --- ## Snowflake URL: https://docs.customer.io/integrations/data-out/connections/snowflake-data-out/ Send Customer.io data about messages, people, metrics, etc to your Snowflake warehouse by way of an Amazon S3 or Google Cloud Project (GCP) storage bucket. This integration syncs up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Snowflake loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up Snowflake with Google Cloud Storage Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. To use a GCS storage bucket, you must set up a service account key (JSON) that grants read/write permissions to the bucket. You’ll provide the contents of this key to Customer.io when you set up this integration. Go to Integrations and select Snowflake and then click Sync Bucket for Google Cloud Storage. Enter information about your GCS bucket and click Validate & select data. Enter Name of your GCS bucket. Enter the Path to your GCS bucket. Paste the JSON of your Service Account Key. Select the data that you want to export from Customer.io to your bucket. By default, we export all data, but you can disable the types that you aren’t interested in. Click Create and sync data. Set up Snowflake with Amazon S3 or Yandex Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. For S3, you’ll need to set up your bucket with ListBucketVersions, ListBucket, GetObject, and PutObject before you can sync data from Customer.io. Create an Access Key and a Service Key with read/write permissions to your S3 or Yandex bucket. Go to Integrations and select Snowflake and then click Sync Bucket. Enter information about your bucket and click Select data. Enter the Name of your bucket. Enter the path to your bucket. Paste your Access and Secret keys in the appropriate fields. Select the Region your bucket is in. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Each sync includes a cio-validate file If you sync data to an Amazon S3 bucket, Customer.io writes a file called cio-validate to your bucket before every sync. This is an empty file that we use to verify that we have write permissions to your bucket before each sync. You can safely delete this file. It does not affect data sync operations, and it’s not part of your exported data. If you have automated processes that import parquet files from your bucket, you may want to configure them to ignore the cio-validate file, since it’s not a parquet file and doesn’t contain any data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Snowflake. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Snowflake. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Snowflake. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS) or Secret Key (S3, Yandex) again. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Snowflake. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Snowflake (Advanced) URL: https://docs.customer.io/integrations/data-out/connections/snowflake/ How it works This integration sends CSV, JSON, or parquet files containing your data to your Snowflake (Advanced) bucket. Then you can ingest the files in your storage bucket to your data warehouse of choice. We write files for each type of incoming call to your storage bucket every 10 minutes. So you’ll have files for identify calls, track calls, and so on. Files are named with an incrementing number, so it’s easy to determine the sequence of files, and the order of incoming calls. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Snowflake (Advanced) loop every 10 minutes a->>b: export CSV, JSON, or parquet files b->>c: ingest c->>b: expire/delete files before next sync end Sync frequency and file names Syncs occur every 10 minutes. Each sync file contains data from the previous sync interval. For example, if the last sync occurred at 12:00 PM, the next sync will only send data from 12:00 PM to 12:09:59 PM. Each sync generates new files for each data type in your storage bucket. Files are named in the format <integration id>.<integration action id>.<current position>.<type>. The integration ID and action ID are unique identifiers generated by Customer.io. You’ll see them with the first sync. current position is an incrementing number beginning at 1 that indicates the order of syncs. So your first sync is 1, the next one is 2, etc. type is the type of incoming call—identify, track, page, screen, alias, or group. So, if your file is called 2184.13699.1.track.json, it’s the first sync file for the track call type. Getting started To support Snowflake (Advanced), you’ll set up a Google Cloud Storage, Amazon S3, or Microsoft Azure Blob Storage bucket to store your data. Then, you’ll query and import data from your storage bucket to Snowflake (Advanced) either through a direct query or a product like Stitch. As a part of this integration, we’ll create parquet, JSON, or CSV files in your storage bucket. See data warehouses for a list of data schemas. Go to Data & Integrations > Integrations and select Snowflake (Advanced) in the Directory tab. Connect to your storage bucket: Review your setup and click Finish to enable your integration. Google Cloud Storage (GCS) Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of the Google Cloud Storage Bucket where files will be written to. Learn more about GCS buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Service Account: The JSON string of the Google Cloud Service Account with permissions to upload files to a bucket, which can be found in your Google Cloud Console. Learn more about Google Cloud Service Accounts. Amazon S3 Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Bucket Name: Name of an existing bucket. Learn more about S3 buckets and bucket naming rules. Bucket Path: Optional folder inside the bucket where files will be written to. Access Key: The AWS Access Key ID that will be used to connect to your S3 Bucket. Your Access Key ID can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Secret Key: The AWS Secret Access Key that will be used to connect to your S3 Bucket. Your Secret Access Key can be found in the My Security Credentials section of your AWS Console. Learn more about AWS credentials. Region: The AWS Region where your S3 Bucket resides in. Learn more about AWS Regions. Azure Blob Storage Endpoint: Endpoint for the internal ETL API. Token: Authentication token for the internal ETL API. Format: Format of the data files that will be created. Blob Sas Url: The SAS URL of the Azure Blob Storage container with permissions to upload files to a container. Learn how to generate an Azure SAS URL in our documentation. Blob Path: Optional folder inside the container where files will be written to. Schemas The following schemas represent JSON for the different types of files we export to your storage bucket (identify, track, and so on). For CSV and Parquet files, we stringify objects and arrays. For example, if identify calls contain the traits object with a first_name and last_name, CSV files output to your storage bucket will contain a traits column with data that looks like this for each row: "{ "\first_name\": \"Bugs\", \"last_name\": \"Bunny\" }". identify identify Identifies files contain identify calls sent to Customer.io. The context and traits in the schema below are objects in JSON. In CSV and parquet files, these columns contain stringified objects. traits object Additional properties that you know about a person. We’ve listed some common/reserved traits below, but you can add any traits that you might use in another system. createdAt string  (date-time) We recommend that you pass date-time values as ISO 8601 date-time strings. We convert this value to fit destinations where appropriate. email string A person’s email address. In some cases, you can pass an empty userId and we’ll use this value to identify a person. Additional Traits* any type Traits that you want to set on a person. These can take any JSON shape. group group Groups files contain group calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and traits columns contain stringified objects. traits object Additional data points that the call assigns to the group. Additional Traits* any type Traits can have any name, like `account_name` or `total_employees`. These can take any JSON shape. track track Tracks contains entries for the track calls you send to Customer.io. It shows information about the events your users perform. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. event string The slug of the event name, mapping to an event-specific table. event_text string The name of the event. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. Event Properties* any type page page Pages contains entries for the page calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties sent with the page call. We’ve listed some common/reserved traits captured by our Analytics.js library, but you can add any properties that you might use in another system. category string The category of the page. This might be useful if you have a single page routes or have a flattened URL structure. path string The path of the page. This defaults to location.pathname, but can be overridden. referrer string The referrer of the page, if applicable. This defaults to document.referrer, but can be overridden. search string The search query in the URL, if present. This defaults to location.search, but can be overridden. title string The title of the page. This defaults to document.title, but can be overridden. url string The URL of the page. This defaults to a canonical url if available, and falls back to document.location.href. Page Properties* any type screen screen Screens files contain entries for the screen calls sent to Customer.io. If your integration outputs CSV or parquet files, the context and properties columns contain stringified objects. If your integration outputs JSON files, the context and properties columns contain objects. properties object Additional properties that you sent in your screen event Additional event properties* any type Properties that you sent in the event. These can take any JSON shape. alias alias The Alias schema contains entries for the alias calls you send to Customer.io. It shows information about the users you merge, with each entry showing a user’s new user_id and their previous_id. --- ## Sprig URL: https://docs.customer.io/integrations/data-out/connections/sprig/ Getting started Go to Data & Integrations > Integrations and select the Sprig entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Env Id: Your environment ID (production or development). Debug Mode: Enable debug mode for testing purposes. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Identify User type = “identify” Set user ID and/or attributes. Sign Out User type = “track” and event = “Signed Out” Clear stored user ID so that future events and traits are not associated with this user. Track Event type = “track” and event != “Signed Out” Track event to potentially filter user studies (microsurveys) later, or trigger a study now. Update User ID type = “alias” Set updated user ID. --- ## Talon.One URL: https://docs.customer.io/integrations/data-out/connections/talon-one/ Getting started Go to Data & Integrations > Integrations and select the Talon.One entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Api Key: Created under Developer Settings in the Talon.One Campaign Manager. Deployment: The base URL of your Talon.One deployment. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Update Customer Profile This updates attributes and audiences for a single customer profile. Track Event This records a custom event in Talon.One. Update Customer Profile V2 You do not have to create attributes or audiences before using this endpoint. Update Customer Sessions This updates a customer session. --- ## TikTok Conversions URL: https://docs.customer.io/integrations/data-out/connections/tiktok-conversions/ Before you get started The TikTok Conversions destination uses the TikTok Events API. To use the Events API, you’ll need to generate a TikTok Pixel Code and Access Token: Create a TikTok For Business account. Create a TikTok Pixel in Developer Mode to obtain a Pixel Code. For more information about Developer Mode, see TikTok’s developer documentation. Follow instructions for Authorization and generate a long term Access Token. Getting started Go to Data & Integrations > Integrations and select the TikTok Conversions entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Access Token: Your TikTok Access Token. Please see TikTok’s Events API documentation for information on how to generate an access token via the TikTok Ads Manager or API. Pixel Code: Your TikTok Pixel ID. Please see TikTok’s Events API documentation for information on how to find this value. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Report Web Event Report events directly to TikTok. Data shared can power TikTok solutions like dynamic product ads, custom targeting, campaign optimization and attribution. FAQ & Troubleshooting Deduplication with the TikTok Pixel If you independently placed the TikTok Pixel on your website, reach out to your TikTok representative to see if you need to change your Pixel to properly deduplicate events sent through both the Pixel and Customer.io. Match Keys To increase the probability of matching website visitor events with TikTok ads, send one or more of the following keys and identifiers when possible. TikTok Click ID External ID Phone Number Email IP Address User Agent Other Standard Events If you want to send a TikTok standard event that we don’t have a prebuilt action for, you can use the Report Web Event action. For example, if you wanted to send CompleteRegistration events, you would: Create a mapping for Report Web Event. Set up your Event Trigger criteria for completed registrations. Input a literal string of “CompleteRegistration” as the Event Name. Be aware that TikTok only supports select event names. Hashing Personally Identifiable Information (PII) We create a sha256 hash of the following fields before sending to TikTok: External ID Email Phone Number Web Diagnostics In addition to our own Data Out tab, You can check if your integration is working is working, test events in real-time, and troubleshoot common issues in TikTok’s Web Diagnostics Suite. See the TikTok Pixel Web Diagnostics documentation for more information. --- ## Twilio URL: https://docs.customer.io/integrations/data-out/connections/twilio/  This destination sends SMS through the Twilio Programmable Messaging API. If you want to trigger multi-step communication workflows in Twilio, use the Twilio Studio destination instead. Use cases Send transactional SMS notifications—like order confirmations, appointment reminders, or shipping updates—triggered by track events from your source. Alert customers about account activity, such as password resets, login attempts, or billing changes. Re-engage inactive users by sending a personalized text message when someone hasn’t visited your app in a set period. Deliver time-sensitive information like one-time passwords, verification codes, or flash sale announcements. Getting started Go to Data & Integrations > Integrations and select the Twilio entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Account Id: Your Twilio Account Id Token: Your Twilio Token. Phone Number: Your Twilio Phone Number with Country Code. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send SMS type = “track” Sends an SMS message Send SMS The Send SMS action sends a text message to a phone number through Twilio’s API. Here’s what you need to know about the key fields: To: The recipient’s phone number. Use E.164 format—for example, +15551234567. Map this to a trait or property containing your customer’s phone number, like $.traits.phone or $.properties.phone. Body: The message content. You can use variables to personalize the message, like Hi {{$.traits.first_name}}, your order has shipped! Media URL: (Optional) A URL pointing to media you want to attach as an MMS message. The recipient’s carrier must support MMS. Example: order confirmation If your source sends a track call when an order is placed, you could set up the trigger for the event Order Completed and configure the action like this: Field Value To $.properties.phone Body Your order #{{$.properties.order_id}} has been confirmed! Estimated delivery: {{$.properties.delivery_date}} Things to know Phone numbers must use E.164 format. They start with + followed by the country code and number—for example, +15551234567. If your phone numbers aren’t in this format, transform them before they reach Twilio. You need a Twilio phone number. You can’t send SMS from an arbitrary number. Purchase a phone number in your Twilio Console or use a Twilio Messaging Service. Standard Twilio rates apply. Each SMS sent through this integration counts toward your Twilio usage and billing. Check Twilio’s pricing for details. MMS support depends on the carrier. If you include a Media URL, the message sends as MMS. Not all carriers or regions support MMS—in those cases, the media attachment may not arrive. --- ## Twilio Engage Messaging URL: https://docs.customer.io/integrations/data-out/connections/engage-messaging-twilio/  This destination is specifically for Engage audience workflows. If you want to send SMS based on individual track or identify events, use the standard Twilio destination. For multi-step communication workflows, use Twilio Studio. Use cases Send targeted SMS campaigns to audience segments. When someone enters an audience—for example, “high-value customers”—automatically send them a promotional text message. Deliver WhatsApp notifications to audience members using Twilio’s WhatsApp Business API. Reach customers on their preferred messaging channel. Trigger re-engagement messages when users enter a “lapsed” or “at-risk” audience segment, nudging them to return with a personalized SMS or WhatsApp message. Send subscription-aware messages that automatically respect opt-in status. This destination checks subscription status before sending, so you only message people who’ve opted in. Getting started Go to Data & Integrations > Integrations and select the Twilio Engage Messaging entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Twilio Account SID: Twilio Account SID Twilio Api Key SID: Twilio API Key SID Twilio Api Key Secret: Twilio API Key Secret Profile Api Environment: Profile API Environment Profile Api Access Token: Profile API Access Token Space Id: Space ID Source Id: Source ID Webhook Url: Webhook URL that will receive all events for the sent message Twilio Hostname: Overrides the default Twilio host name used mainly for testing without having to send real messages. Connection Overrides: Connection overrides are configuration supported by twilio webhook services. Must be passed as fragments on the callback url Region: The region where the message is originating from Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Send SMS type = “track” and event = “Audience Entered” Send SMS using Twilio Send WhatsApp type = “track” and event = “Audience Entered” Send WhatsApp using Twilio Send SMS The Send SMS action sends a text message through Twilio when the trigger conditions are met. By default, this action triggers on track events where the event name is Audience Entered. Key fields to configure: From (required): The Twilio Phone Number, Short Code, or Messaging Service SID to send from. Body: The message content. You can use variables to personalize the message. Media URLs: (Optional) URLs for media attachments, sent as MMS. Content SID: (Optional) A Twilio Content API template SID if you want to use pre-approved message templates. Send WhatsApp The Send WhatsApp action sends a WhatsApp message using Twilio’s WhatsApp Business API. Like the SMS action, it triggers by default on Audience Entered track events. Key fields to configure: From (required): Your Twilio WhatsApp-enabled phone number, formatted as whatsapp:+15551234567. Body: The message content. Content SID: (Optional) A Twilio Content API template SID for pre-approved WhatsApp templates. WhatsApp requires pre-approved templates for business-initiated conversations. Things to know Subscription status is enforced. This destination checks each recipient’s subscription status before sending. It only sends messages to people with a subscribed status, so you don’t need to build separate opt-in logic. API keys are required (not Auth Tokens). Unlike the standard Twilio destination, this integration uses API Key SID and Secret pairs. Create these in your Twilio Console. WhatsApp templates may be required. WhatsApp Business API requires pre-approved message templates for business-initiated conversations. If you send WhatsApp messages outside of a 24-hour customer-initiated window, you need a Content SID pointing to an approved template. Default trigger is “Audience Entered.” Both SMS and WhatsApp actions come pre-configured to trigger when someone enters an audience. You can change this, but the destination works best with audience-based workflows. Standard Twilio rates apply. Twilio bills SMS, MMS, and WhatsApp messages according to your plan. --- ## Twilio Studio URL: https://docs.customer.io/integrations/data-out/connections/twilio-studio/  This destination triggers Studio Flows, which are multi-step communication workflows you build in Twilio Studio. If you just need to send a single SMS, use the Twilio destination instead. Use cases Trigger multi-step onboarding flows when a new user signs up. Your Studio Flow could send a welcome SMS, wait for a response, then route the conversation based on what the user says. Start interactive support workflows when a customer submits a help request. Studio can send an initial message, collect information, and route the customer to the right team. Run automated phone surveys after a purchase or support interaction by triggering a Studio Flow that places an outbound call with an IVR menu. Initiate appointment reminders with confirmation handling. Your Studio Flow can send a reminder SMS, listen for “confirm” or “reschedule” replies, and take action accordingly. Getting started Go to Data & Integrations > Integrations and select the Twilio Studio entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Account Sid: Your Twilio Account SID, starting with AC. You can find this in the Account Info section of your dashboard in the Twilio Console. Auth Token: Your Twilio Auth Token. You can find this in the Account Info section of your dashboard in the Twilio Console. Space Id: Your Segment Space ID. Profile Api Access Token: Your Segment Profile API Access Token. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Trigger Studio Flow Trigger a Flow in Twilio Studio to initiate an outbound call or message. The Flow will execute via the REST API trigger. Trigger studio flow The Trigger Studio Flow action starts a Twilio Studio Flow execution when an event matches your trigger conditions. Configure the following fields: Flow SID (required): The identifier for the Studio Flow you want to trigger. These identifiers start with FW—find them in your Twilio Studio dashboard. From Phone Number (required): The Twilio phone number that initiates calls or messages during the flow execution. Use E.164 format—for example, +15551234567. Cooling-off Period: The time (in seconds) during which the flow can only be triggered once per Flow SID and user combination. Defaults to 60 seconds. This prevents duplicate triggers from rapid successive events. Example: post-purchase follow-up Suppose you have a Studio Flow that sends a follow-up SMS after a purchase, asks the customer to rate their experience, and routes negative responses to your support team. You’d configure the action like this: Field Value Trigger track calls where event = Order Completed Flow SID FWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx From Phone Number +15551234567 Cooling-off Period 300 (5 minutes—prevents duplicate triggers if multiple order events fire) Things to know You need a Studio Flow first. This destination triggers existing flows—it doesn’t create them. Build your flow in Twilio Studio before setting up the action. The cooling-off period prevents duplicates. If the same user triggers the same flow multiple times within the cooling-off window, only the first trigger runs. Increase this value if you see duplicate flow executions. User identity passes automatically. The userId and anonymousId from your incoming event go to the Studio Flow, so you can reference them in your flow logic. Standard Twilio usage charges apply. Each flow execution and the messages or calls it generates follow your Twilio plan’s billing. --- ## Twitter Pixel URL: https://docs.customer.io/integrations/data-out/connections/twitter-pixel/ Getting started Go to Data & Integrations > Integrations and select the Twitter Pixel entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Pixel ID: The Pixel ID is a unique identifier for your Twitter Pixel installation. It can be found in the "Tools" -> "Events Manager" section of your Twitter Ads account. Restricted Data Use: The Restricted Data Use (RDU) parameter enables an advertiser to limit Twitter's use of certain data for specific business purposes only on that advertiser's behalf. More information is available in the documentation. Click Enable Destination. When you’re done, you’ll need to go to the Actions tab and add actionsThe source event and data that triggers an API call to your destination. For example, an incoming identify event from your sources adds or updates a person in our Customer.io Journeys destination. for each event you want to report to Twitter Pixel. If you’re migrating from Segment You’ll notice that our destination has fewer settings than Segment’s. We’ve moved some settings into our Actions, where we think they make more sense. We use Twitter’s current Pixel paradigm. We do not support the legacy Universal Website Tag and Single Event Website Tags web tags. This means that you must define the events you want to send to Twitter in your Twitter Ads account before you map source data to those events in Customer.io. Twitter generates an event ID for each type of event you want to track, and uses that ID to group and categorize events rather than the simple event name that you send with track calls. You’ll need to provide this ID with each action you set up, so that we can properly map source data to events in your Twitter Ads account. If you previously used the Universal Website Tag, Twitter has effectively migrated you to Single Event Website Tags. You’ll find the Website Tag for each of your events in your Twitter Pixel Events Manager. You’ll use this tag when you set up actions in Customer.io. Actions By default, Identify User is the only enabled action. You’ll need to add actions for each event you want to report to Twitter Pixel. We support a number of default Twitter Pixel events, but you can also add custom actions for any events you’ve set up in Twitter Pixel. When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Identify User type = “identify” Send user data to Twitter Pixel Track Page View type = “page” Track the current page Track Custom Event type = “track” and event != “Products Searched” and event != “Product Viewed” and event != “Product Added” and event != “Order Completed” and event != “Product Added To Wishlist” and event != “Checkout Started” and event != “Payment Info Entered” Track a custom conversion event Track Checkout Started Event type = “track” and event = “Checkout Started” Track the Checkout Started event which maps to the InitiateCheckout event to Twitter. Track Order Completed Event type = “track” and event = “Order Completed” Track the Order Completed event which maps to the Purchase event to Twitter. Track Payment Info Entered Event type = “track” and event = “Payment Info Entered” Track the Payment Info Entered event which maps to the AddPaymentInfo event to Twitter. Track Product Added Event type = “track” and event = “Product Added” Track the Product Added event which maps to the AddToCart event to Twitter. Track Product Added To Wishlist Event type = “track” and event = “Product Added To Wishlist” Track the Product Added To Wishlist event which maps to the AddToWishlist event to Twitter. Track Products Searched Event type = “track” and event = “Products Searched” Track the Products Searched event which maps to the Search event to Twitter. Track Product Viewed Event type = “track” and event = “Product Viewed” Track the Product Viewed event which maps to the ViewContent event to Twitter. Predefined Track Events We’ve defined a number of ecommerce events for Twitter Pixel based on our own ecommerce specification. If you use our ecommerce specification for other things, you’ll likely want to use these events as well to track conversions in Twitter. These events are all triggered by common event names defined in our ecommerce specification. For all other events, you’ll want to use the Custom Event Action and set your own trigger based on the Track Event Name. By default, our Custom Event action excludes all of our predefined events, but you may want to set the trigger to simply match on the event name that you send from your sources. Remember: for each event you want to send to Twitter, you need to define it in your Twitter Ads account first. Set up a Twitter Pixel action Before you set up a new action, make sure that you’ve defined your event in Twitter Pixel. See Twitter’s Documentation for help setting up new events. When you set up events, you’ll need to get the Website Tag for this event. We don’t expect you to store this pixel in your sources. Rather, you set it up in Customer.io and we’ll automatically append it to data that we send to your destination. In your Twitter Pixel destination, go to the Actions tab and click Add Action. Set your Trigger conditions. In general, you’ll use Track Event Name to determine when we’ll send event data to Twitter.  For custom events, you should set an is condition When you set up a custom event, we assume that you’ll look for any event that is not one of our presets. However, with Twitter, you may simply want to set conditions to Track Event Name is event-you-want-to-track>. With twitter, it’s unlikely that you’ll have a “catch-all” event, because you need to define your events in Twitter first. Under Data Structure, set your Event ID: this is the Website Tag for the event you defined in Twitter Pixel. In the next field down, you can set the Conversion ID. In general, we use the messageId from incoming source events, because it deduplicates source data and ensures that we report unique conversions to Twitter. Set the Click ID if you want to force conversions to a particular click—like the Add to cart button for added_to_cart events, or something similar. If you don’t set a click target, Twitter will provide the Click ID itself. Determine the fields that you want to pass to Twitter. You can send all $.properties, or you can select individual event properties. You can also map specific fields for Currency and Revenue. Click Save Action. Sending product information to Twitter In general, when you send events containing product information, Twitter expects you to identify products by SKU. But they’ll also support an ID if you don’t have the product SKU. So, when you send an event that contains properties.products, we’ll automatically identify products by SKU if it exists or ID if the SKU does not exist. You can change this mapping by editing the array of products, but, if you do this, you’ll also want to define the other product keys that you want to pass along to Twitter. --- ## Visual Website Optimizer (VWO) URL: https://docs.customer.io/integrations/data-out/connections/vwo/ There are two versions of this integration You’ll see two entries for Visual Website Optimizer (VWO) in our integration catalog, with one labeled Web. We typically recommend that you use the standard integration, the one not labeled “Web” when possible. The web version of this integration only works with our JavaScript client and does not pass data through Customer.io’s servers, which can make it hard to debug your integration, capture a history of events sent to the integration, and so on. Learn more about Web integrations. Getting started Go to Data & Integrations > Integrations and select the Visual Website Optimizer (VWO) entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Vwo Account Id: Enter your VWO Account ID Apikey: VWO Fullstack SDK Key. It is mandatory when using the VWO Fullstack suite. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Sends a track event to VWO Identify User type = “identify” Maps visitor traits to the visitor attributes in VWO Page Visit type = “page” Sends a page event to VWO Getting started: web integration Go to Data & Integrations > Integrations and select the Visual Website Optimizer (VWO) entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Vwo Account Id: Your VWO account ID, used for fetching your VWO async smart code. Settings Tolerance: The maximum amount of time (in milliseconds) to wait for test settings before VWO will simply display your original page. Library Tolerance: The maximum amount of time (in milliseconds) to wait for VWO’s full library to be downloaded before simply displaying your original page. Use Existing Jquery: If your page already includes JQuery, you can set this to “true”. Otherwise, VWO will include JQuery onto the page for you. VWO needs JQuery on the page to function correctly. Click Enable Destination. Web integration actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Track Event type = “track” Sends Segment's track event to VWO Identify User type = “identify” Sends Segment's page event to VWO --- ## Webhooks URL: https://docs.customer.io/integrations/data-out/connections/webhook/ Getting started Our webhook destination lets you transform and send data to any URL you maintain. It’s basically a custom destination, letting you handle source data however you want. Go to Data & Integrations > Integrations and select the Webhooks entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Shared Secret: If set, request will be signed with an HMAC in the "X-Signature" request header. The HMAC is a hex-encoded SHA1 hash generated using this shared secret and the request body. Click Enable Destination. Securing webhook requests We recommend that you use a Shared Secret to secure your webhook URL and ensure that you only ingest requests that come from Customer.io. When you use a shared secret, we’ll sign requests with an X-Signature header containing a hex-encoded SHA1 hash from the shared secret and request body. You can use this signature to verify that the request came from Customer.io. Actions A webhook request has one action: Send. This action lets you map incoming data to a URL you maintain. When you set up a webhook Send action, you’ll set: The Trigger for the action: typically this is the type of incoming data (identify, track, etc). But you may also want to send specific events like email_opened or product_viewed. The URL to send the data to. The HTTP method to use: POST, PUT, PATCH, or DELETE. Whether or not to batch events: We batch up to 1000 events matching the trigger in a single request. See Batching for more information. Set your HTTPS Headers and the data you want to send in the payload. Like most actions, we typically recommend that you send the userId, event name, and properties or traits from the incoming call, but you may want to set up specific mappings. Batching If you want to send fewer calls to your webhook endpoint, you can batch your webhook requests in groups of up to 1000 events. We send each batch as an array of objects where each object is the body of an incoming request that matches your trigger criteria. For example, imagine that you’ve mapped track events as shown in the image below. The code sample below the image is an example of what we’d send to your webhook endpoint. [ { "type": "track", "userId": "123", "event": "Signed Up", "properties": { "plan": "Startup", "referred_by": "Friend" } }, { "type": "track", "userId": "456", "event": "Signed Up", "properties": { "plan": "Free", "referred_by": "Ad" } } ] --- ## Wisepops URL: https://docs.customer.io/integrations/data-out/connections/wisepops/ Getting started Go to Data & Integrations > Integrations and select the Wisepops entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Website Id: The identifier of your Wisepops' website. You can find it in your setup code on Wisepops. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Set Custom Properties type = “identify” Define custom properties to let Wisepops target them in your scenarios. Track Event type = “track” Send a custom event to Wisepops. Keep in mind that events are counted as page views in your Wisepops' monthly quota. Track Goal type = “track” and event = “Order Completed” Track goals and revenue to know which campaigns are generating the most value. Track Page type = “page” Let Wisepops know when the visitor goes to a new page. This allows Wisepops to display campaigns at page change. --- ## Yandex URL: https://docs.customer.io/integrations/data-out/connections/yandex-data-out/ Send Customer.io data about messages, people, metrics, etc to Yandex. From here, you can ingest your data into the data warehouse of your choosing. This integration syncs up to every 15 minutes, helping you keep up to date on your audience's message activities.  We have two integrations! This integration uses Customer.io as a source, and syncs data from your Customer.io workspace to your data warehouse, including campaign information. Our other integration sends data from multiple sources to your data warehouse. While the other integration captures data from multiple sources, even if those sources don’t send data to your workspace, it cannot capture some data from within Customer.io like campaign information. How it works This integration exports individual parquet files for Deliveries, Metrics, Subjects, Outputs, Content, People, and Attributes to your storage bucket. Each parquet file contains data that changed since the last export. Once the parquet files are in your storage bucket, you can import them into data platforms like Fivetran or data warehouses like Redshift, BigQuery, and Snowflake. Note that this integration only publishes parquet files to your storage bucket. You must set your data warehouse to ingest this data. There are many approaches to ingesting data, but it typically requires a COPY command to load the parquet files from your bucket. After you load parquet files, you should set them to expire to delete them automatically. We attempt to export parquet files every 15 minutes, though actual sync intervals and processing times may vary. When syncing large data sets, or Customer.io experiences a high volume of concurrent sync operations, it can take up to several hours to process and export data. This feature is not intended to sync data in real time. sequenceDiagram participant a as Customer.io participant b as Storage Bucket participant c as Yandex loop up to every 15 minutes a->>b: export parquet files b->>c: ingest c->>b: expire/delete files before next sync end  Your initial sync includes historical data During the first sync, you’ll receive a history of your Deliveries, Metrics, Subjects, and Outputs data. However, People who have been deleted or suppressed before the first sync are not included in the People file export and the historical data in the other export files is anonymized for the deleted and suppressed People. The initial export vs incremental exports Your initial sync is a set of files containing historical data to represent your workspace’s current state. Subsequent sync files contain changesets. Metrics: The initial metrics sync is broken up into files with two sequence numbers, as follows. <name>_v5_<workspace_id>_<sequence1>_<sequence2>. Attributes: The initial Attributes sync includes a list of profiles and their current attributes. Subsequent files will only contain attribute changes, with one change per row. Events: The initial events sync includes up to 30 days of past events. Subsequent files contain events since the previous sync interval. We cannot export events older than 30 days. flowchart LR a{is it the initial sync?}-->|yes|b[send all history] a-->|no|c{was the file already enabled?} c-->|yes|d[send changes since last sync] c-->|no|e{was the file ever enabled?} e-->|yes|f[send changeset since file was disabled] e-->|no|g[send all history] For example, let’s say you’ve enabled the Attributes export. We will attempt to sync your data to your storage bucket every 15 minutes: 12:00pm We sync your Attributes Schema for the first time. This includes a list of profiles and their current attributes. 12:05pm User1’s email is updated to company-email@example.com. 12:10pm User1’s email is updated to personal-email@example.com. 12:15 We sync your data again. In this export, you would only see attribute changes, with one change per row. User1 would have one row dedicated to his email changing. Requirements If you use a firewall or an allowlist, you must allow the following IP addresses to support traffic from Customer.io. Make sure you use the correct IP addresses for your account region. Data Warehouse IP Addresses (data-out) US RegionEU Region 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81  Do you use other Customer.io features? These IP addresses are specific to outgoing Data Warehouse integrations. If you use your own SMTP server or receive webhooks, you may also need to allow additional addresses. See our complete IP allowlist. Set up an Amazon S3 data-out integration Before you begin, make sure that you’re prepared to ingest relevant parquet files from Customer.io. For S3, you’ll need to set up your bucket with ListBucketVersions, ListBucket, GetObject, and PutObject before you can sync data from Customer.io. Create an Access Key and a Service Key with read/write permissions to your S3 or Yandex bucket. Go to Integrations and select Yandex and then click Sync Bucket. Enter information about your bucket and click Select data. Enter the Name of your bucket. Enter the path to your bucket. Paste your Access and Secret keys in the appropriate fields. Select the Region your bucket is in. Select the data types that you want to export from Customer.io to your bucket. By default, we export all data types, but you can disable the types that you aren’t interested in. Click Create and sync data. Pausing and resuming your sync You can turn off files you no longer want to receive, or pause them momentarily as you update your integration, and turn them back on. When you turn a file schema on, we send files to catch you up from the last export.If you haven’t exported a particular file before—the file was never “on”—the initial sync contains your historical data. You can also disable your entire sync, in which case we’ll quit sending files all together. When you enable your sync again, we send all of your historical data as if you’re starting a new integration. Before you disable a sync, consider if you simply want to disable individual files and resume them later.  Delete old sync files before you re-enable a sync Before you resume a sync that you previously disabled, you should clear any old files from your storage bucket so that there’s no confusion between your old files and the files we send with the re-enabled sync. Disabling and enabling individual export files Go to Data & Integrations > Integrations and select Yandex. Select the files you want to turn on or off. When you enable a file, the next sync will contain baseline historical data catching up from your previous sync or the complete history if you haven’t synced a file before; subsequent syncs will contain changesets.  Turning the People file off If you turn the People file off for more than 7 days, you will not be able to re-enable it. You’ll need to delete your sync configuration, purge all sync files from your destination storage bucket, and create a new sync to resume syncing people data. Disabling your sync If your sync is already disabled, you can enable it again with these instructions. But, before you re-enable your sync, you should clear the previous sync files from your data warehouse bucket first. See Pausing and resuming your sync for more information. Go to Data & Integrations > Integrations and select Yandex. Click Disable Sync. Manage your configuration You can change settings for a bucket, if your path changes or you need to swap keys for security purposes. Go to Data & Integrations > Integrations and select Yandex. Click Manage Configuration for your bucket. Make your changes. No matter your changes, you must input your Service Account Key (GCS) or Secret Key (S3, Yandex) again. Click Update Configuration. Subsequent syncs will use your new configuration. Update sync schema version Before you prepare to update your data warehouse sync version, see the changelog. You’ll need to update schemas to upgrade to the latest version (v5).  When updating from v1 to a later version, you must: Update ingestion logic to accept the new file name format: <name>_v<x>_<workspace_id>_<sequence>.parquet Delete existing rows in your Subjects and Outputs tables. When you update, we send all of your Subjects and Outputs data from the beginning of your history using the new file schema. Go to Data & Integrations > Integrations and select Yandex. Click Upgrade Schema Version. Follow the instructions to make sure that your ingestion logic is updated accordingly. Confirm that you’ve made the appropriate pages and click Upgrade sync. The next sync uses the updated schema version. Parquet file schemas This section describes the different kinds of files you can export from our Database-out integrations. Many schemas include an internal_customer_id—this is the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. You can use it to resolve a person associated with a subject, delivery, etc. These schemas represent the latest versions available. Check out our changelog for information about earlier versions. DeliveriesDelivery ContentMetricsOutputsPeopleSubjectsAttributesCampaignsBroadcastsActionsObjectsObject TypesObject AttributesEventsInbound Deliveries Deliveries are individual email, in-app, push, SMS, slack, and webhook records sent from your workspace. The first deliveries export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the delivery record. delivery_id ✅ STRING (Required). The ID of the delivery record. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. subject_id Subjects STRING (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the path the person went through in the workflow. Note: This value refers to, and is the same as, the subject_name in the subjects table. event_id Subjects STRING (Nullable). If the delivery was created as part of an event-triggered Campaign, this is the ID for the unique event that triggered the workflow. Note that this is a foreign key for the subjects table, and not the metrics table. delivery_type STRING (Required). The type of delivery: email, push, in-app, sms, slack, or webhook. campaign_id INTEGER (Nullable). If the delivery was created as part of a Campaign or API Triggered Broadcast workflow, this is the ID for the Campaign or API Triggered Broadcast. action_id INTEGER (Nullable). 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. newsletter_id INTEGER (Nullable). If the delivery was created as part of a Newsletter, this is the unique ID of that Newsletter. content_id INTEGER (Nullable). If the delivery was created as part of a Newsletter split test, this is the unique ID of the Newsletter variant. trigger_id INTEGER (Nullable). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. created_at TIMESTAMP (Required). The timestamp the delivery was created at. transactional_message_id INTEGER (Nullable). If the delivery occurred as a part of a transactional message, this is the unique identifier for the API call that triggered the message. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Delivery Content The delivery_content schema represents message contents; each row corresponds to an individual delivery. Use the delivery_id to find more information about the contents of a message, or the recipient to find information about the person who received the message. If your delivery was produced from a campaign, it’ll include campaign and action IDs, and the newsletter and content IDs will be null. If your delivery came from a newsletter, the row will include newsletter and content IDs, and the campaign and action IDs will be null. Delivery content might lag behind other tables by 15-30 minutes (or roughly 1 sync operation). We package delivery contents on a 15 minute interval, and can export to your data warehouse up to every 15 minutes. If these operations don’t line up, we might occasionally export delivery_content after other tables.  Delivery content can be a very large data set Workspaces that have sent many messages may have hundreds or thousands of GB of data.  Delivery content is available in v4 or later The delivery_content schema was introduced in our v4 release. You need to update your data warehouse schemas or later to take advantage of the update and see Delivery Content, Subjects, and Outputs. Field Name Primary Key Foreign Key Description delivery_id ✅ Deliveries STRING (Required). The ID of that delivery associated with the message content. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. type STRING (Required). The delivery type—one of email, sms, push, in-app, or webhook. campaign_id INTEGER (Nullable). The ID for the campaign that produced the content (if applicable). action_id INTEGER (Nullable). The ID for the campaign workflow item that produced the content. newsletter_id INTEGER (Nullable). The ID for the newsletter that produced the content. content_id INTEGER (Nullable). The ID for the newsletter content, 0 indexed. If your newsletter did not include an A/B test or multiple languages, this value is 0. from STRING (Nullable). The from address for an email, if the content represents an email. reply_to STRING (Nullable). The Reply To address for an email, if the content is related to an email. bcc STRING (Nullable). The Blind Carbon Copy (BCC) address for an email, if the content is related to an email. recipient STRING (Required). The person who received the message, dependent on the type. For an email, this is an email address; for an SMS, it's a phone number; for a push notification, it's a device ID. subject STRING (Nullable). The subject line of the message, if applicable; required if the message is an email body STRING (Required). The body of the message, including all HTML markup for an email. body_amp STRING (Nullable). The HTML body of an email including any AMP-enabled JavaScript included in the message. body_plain STRING (nullable). The plain text of an email message, without HTML tags or AMP content. This field is typically null unless you manually set or change the plain-text version of an email (the body_plain field when you use our APIs). preheader STRING (Nullable). "Also known as "preview text", this is the block block of text that users see next to, or underneath, the subject line in their inbox. url STRING (Nullable). If the delivery is an outgoing webhook, this is the URL of the webhook. method STRING (Nullable). If the delivery is an outgoing webhook, this is the HTTP method used—POST, PUT, GET, etc. headers STRING (Nullable). If the delivery is an outgoing webhook, these are the headers included with the webhook. Metrics Metrics exports detail events relating to deliveries (e.g. messages sent, opened, etc). Your initial metrics export contains baseline historical data, broken up into files with two sequence numbers, as follows: <name>_v5_<workspace_id>_<sequence1>_sequence2>. Subsequent files contain rows for data that changed since the last export.  You might have multiple entries per delivery_id For example, person can click a link in a message multiple times, creating multiple “clicked” metrics. We might attempt a message delivery multiple times before it’s successfully sent, creating multiple “attempted” metrics. Depending on the metrics you care about, you might need to deduplicate or aggregate metrics based on the delivery_id to get correct counts. Field Name Primary Key Foreign Key Description event_id ✅ STRING (Required). The unique ID of the metric event. This can be useful for deduplicating purposes. workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the metric record. delivery_id Deliveries STRING (Required). The ID of the delivery record. metric STRING (Required). The type of metric (e.g. sent, delivered, opened, clicked). reason STRING (Nullable). For certain metrics (e.g. attempted), the reason behind the action. link_id INTEGER (Nullable). For "clicked" metrics, the unique ID of the link being clicked. link_url STRING (Nullable). For "clicked" metrics, the URL of the clicked link. (Truncated to 1000 bytes.) created_at TIMESTAMP (Required). The timestamp the metric was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. proxied Boolean. For email opened metrics, this indicates that the open event originated from a proxy server. For example, a proxy server may record an open independently of a message reaching the user’s inbox. For other metrics, this is false. prefetched Boolean. For email opened metrics, this indicates that the metric was the result of prefetching and not necessarily a user action. For example, Gmail prefetches images to speed up rendering in the inbox, which may result in an opened metric—but the user didn’t actually open the email. For other metrics, this this value is false. machine Boolean. For email clicked metrics, it means that the click event originated a non-human, e.g. a security service or email-protection application clicked a link. For other metrics, this is false. user_agent STRING (Nullable). The user agent string of the person (or machine) who performed the action, where available. If we don't have a user agent string, this value is null. email_client STRING (Nullable). For email metrics, the email client related to the action; applies to metrics like opened, clicked, etc. For non email channels, this value is null. inbox_domain STRING (Nullable). For email metrics, the inbox domain of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. inbox_provider STRING (Nullable). For email metrics, the inbox provider of the person who performed the action. If this value isn't discernable, or the metric is not email related, this value is null. mx_host STRING (Nullable). For email metrics, this is the MX host of the inbox (e.g. mailhost1.example.com). If this value isn't discernable, or the metric is not email related, this value is null. Outputs Outputs are the unique steps within each workflow journey. The first outputs file includes historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the output record. output_id ✅ STRING (Required). The ID for the step of the unique path a person went through in a Campaign or API Triggered Broadcast workflow. subject_name Subjects STRING (Required). A secondary unique ID for the path a person took through a campaign or broadcast workflow. output_type STRING (Required). The type of step a person went through in a Campaign or API Triggered Broadcast workflow. Note that the “delay” output_type covers many use cases: a Time Delay or Time Window workflow item, a “grace period”, or a date-based campaign trigger. action_id INTEGER (Required). The ID for the unique workflow item associated with the output. explanation STRING (Required). The explanation for the output. delivery_id Deliveries STRING (Nullable). If a delivery resulted from this step of the workflow, this is the ID of that delivery. draft BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether the delivery was created as a draft. link_tracked BOOLEAN (Nullable). If a delivery resulted from this step of the workflow, this indicates whether links within the delivery are configured for tracking. split_test_index INTEGER (Nullable). If the step of the workflow was a Split Test, this indicates the variant of the Split Test. delay_ends_at TIMESTAMP (Nullable). If the step of the workflow involves a delay, this is the timestamp for when the delay will end. branch_index INTEGER (Nullable). If the step of the workflow was a T/F Branch, a Multi-Split Branch, or a Random Cohort Branch, this indicates the branch that was followed. manual_segment_id INTEGER (Nullable). If the step of the workflow was a Manual Segment Update, this is the ID of the Manual Segment involved. add_to_manual_segment BOOLEAN (Nullable). If the step of the workflow was a Manual Segment Update, this indicates whether a person was added or removed from the Manual Segment involved. created_at TIMESTAMP (Required). The timestamp the output was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. People The first People export file includes a list of current people at the time of your first sync (deleted or suppressed people are not included in the first file). Subsequent exports include people who were created, deleted, or suppressed since the last export. People exports come in two different files: people_v5_<env>_<seq>.parquet: Contains new people. people_v5_chngs_<env>_<seq>.parquet: Contains changes to people since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. customer_id STRING (Required). The ID of the person in question. This will match the ID you see in the Customer.io UI. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. deleted BOOLEAN (Nullable). This indicates whether the person has been deleted. suppressed BOOLEAN (Nullable). This indicates whether the person has been suppressed. created_at TIMESTAMP (Required). The date/time when the person was added to Customer.io (using the _created_in_customerio_at attribute). Note that this is not necessarily the same as a person's created_at value! If you import people from an external system, a CSV, or backdate the created_at value, this value is likely to be different from a person's created_at attribute.Note that this value is 0 for deleted or suppressed people updated_at TIMESTAMP (Required) The date-time when a person was updated. Use the most recent updated_at value for a customer_id to disambiguate between multiple records. email_addr STRING (Optional) The email address of the person. For workspaces using email as a unique identifier, this value may be the same as the customer_id. Subjects Subjects are the unique workflow journeys that people take through Campaigns and API Triggered Broadcasts. The first subjects export file includes baseline historical data. Subsequent files contain rows for data that changed since the last export.  Upgrade to v4 to use subjects and outputs We’ve made some minor changes to subjects and outputs a part of our v4 release. If you’re using a previous schema version, we disabled your subjects and outputs on October 31st, 2022. You need to upgrade to schema version 4 or later, to continue syncing outputs and subjects data. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the subject record. subject_name ✅ STRING (Required). A unique ID for the path a person took through a campaign or broadcast workflow. internal_customer_id People STRING (Nullable). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. campaign_type STRING (Required). The type of Campaign (segment, event, or triggered_broadcast) campaign_id INTEGER (Required). The ID of the Campaign or API Triggered Broadcast. event_id Metrics STRING (Nullable). The ID for the unique event that triggered the workflow. trigger_id INTEGER (Optional). If the delivery was created as part of an API Triggered Broadcast, this is the unique trigger ID associated with the API call that triggered the broadcast. started_campaign_at TIMESTAMP (Required). The timestamp when the person first matched the campaign trigger. For event-triggered campaigns, this is the timestamp of the trigger event. For segment-triggered campaigns, this is the time the user entered the segment. created_at TIMESTAMP (Required). The timestamp the subject was created at. seq_num INTEGER (Required) A monotonically increasing number indicating relative recency for each record: the larger the number, the more recent the record. Attributes Attribute exports represent changes to people (by way of their attribute values) over time. The initial Attributes export includes a list of profiles and their current attributes. Subsequent files contain attribute changes, with one change per row. For changes to nested attributes, like the subscription preferences attribute, the attribute_name will be the top-level attribute and the attribute_value returns the stringified JSON representing the nested changes. Using our subscription preferences example, the attribute_name would be cio_subscription_preferences and the attribute_value would be something like "{\"topics\":{\"topic_7\":false,\"topic_8\":false}}". Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. internal_customer_id ✅ STRING (Required). The cio_id of the person in question. Use the people parquet file to resolve this ID to an external customer_id or email address. attribute_name STRING (Required). The attribute that was updated. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Campaigns When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Campaigns table returns the names and versions of your campaigns and API-triggered broadcasts. Some other tables—like Deliveries and Subjects—return campaign ID values. You can use this table to get campaign names based on those IDs so you can better understand exports related to campaigns. Note that this table includes both Campaigns and API-triggered broadcasts; both have campaign_id values. Newsletters appear in the Broadcasts table with a broadcast_id. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign or API-triggered broadcast is updated. This way, you can keep your campaign names and versions up-to-date.  Each row is an update You’ll see a row for each update to each campaign or API-triggered broadcast. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each campaign_id to get the most recent version. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the campaign. campaign_id ✅ INTEGER (Required). The ID of the campaign or API-triggered broadcast. Note that newsletters appear in the Broadcasts schema with a `broadcast_id`, not here. name STRING (Required). The name of a campaign. You set this in Customer.io when you create your campaign. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the campaign. You can create campaigns without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a campaign was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the “version” of the campaign. The largest version number represents the latest version of the campaign. Versions increment when you change the name, trigger, or goal of a campaign. See the Actions table for changes to messages and other items in your campaign workflow. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Broadcasts The Broadcasts schema returns information about your newsletters. Note that API-triggered broadcasts appear in the Campaigns schema, not the Broadcasts schema. The initial sync returns all your newsletters. Subsequent syncs return only the newsletters that have changed since the last sync.  Each row is an update You’ll see a row for each update to each broadcast. For example, if you edit the content, audience, and settings for a broadcast, you’ll see three rows. If joining to this table, you may want to include a condition so that you only get the MAX updated_at value for each broadcast_id to get the most recent version.  Broadcasts vs Campaigns In the data warehouse schemas: Newsletters appear in the Broadcasts schema with a broadcast_id API-triggered broadcasts appear in the Campaigns schema with a campaign_id This is why newsletters and API-triggered broadcasts can share the same ID value—they exist in different schemas. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the broadcast. broadcast_id ✅ INTEGER (Required). The ID of the newsletter. Note that API-triggered broadcasts appear in the Campaigns schema with a `campaign_id`, not here. name STRING (Required). The name of a broadcast. You set this in Customer.io when you create your broadcast. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the broadcast. You can create broadcasts without activating them! updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a broadcast was last updated. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Actions When you enable the Campaign Metadata schema, we actually return two different tables: Campaigns and Actions. The Actions table returns the names and versions of workflow steps in your campaigns, which we call actionsA block in a campaign workflow—like a message, delay, or attribute change.. Some other tables—like Deliveries and Subjects—return action ID values. You can use this table to get the names of actions in your campaigns, so it’s easier for you to understand your campaign and action-related data. With each sync, we’ll return the rows where the version changed. The version is a number that increments each time a campaign is updated. This way, you can keep your understanding of campaign actions up-to-date. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace containing the workflow action. campaign_id Campaigns INTEGER (Required). The ID of the campaign containing the action. action_id INTEGER (Required). The ID of the action. name STRING (Optional). The name of a workflow action. You set this in Customer.io when you create or edit your action. If you didn't set a name for the action, this field is empty. created_at TIMESTAMP (Required). The date-time (in milliseconds) when you created the workflow action. updated_at TIMESTAMP (Required) The date-time (in milliseconds) when a workflow action was last updated. version INTEGER (Required) An incrementing number starting at 1 representing the "version" of the workflow action. The largest number for any action represents the latest version. The version changes whenever you update the name, content, or settings of your workflow action. topic_names LIST (Nullable). If you use our subscription center feature, this value is a comma-separated list of subscription topics that users must be subscribed to in order to receive the campaign. If the campaign is not associated with any topics, this value is null. Objects The first Object export file includes a list of current objects at the time of your first sync (deleted objects are not included in the first file). Subsequent exports include objects who were created, deleted, or suppressed since the last export. When you enable the Objects export, we also export Object Types. object exports come in two different files: object_v5_<env>_<seq>.parquet: Contains new objects. object_v5_chngs_<env>_<seq>.parquet: Contains changes to objects since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id Object Types INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. object_id STRING (Required). The ID of the object in question. This will match the ID you see in the Customer.io UI. internal_object_id ✅ STRING (Required). A unique, immutable ID that Customer.io assigns to the object. Other exports use this value in to reference your object; you can use this export to resolve internal IDs to your object IDs. deleted BOOLEAN (Nullable). This indicates whether the object has been deleted. created_at TIMESTAMP (Required). The date/time when the object was added to your workspace. updated_at TIMESTAMP (Required) The date-time when a object was updated. Use the most recent updated_at value for an object_id to disambiguate between multiple records. Object Types We export object types when you enable the Objects export. All objects have a type indicating what kind of entity they are—like an account or company. The object_type value is an integer starting at 1. For example, if you create two types of objects in your system, accounts and companies, in that order, accounts have an object_type of 1 and companies have an object_type of 2. The first export includes a list of object types at the time of your first sync (we don’t include deleted types in the first file). Subsequent exports include types you created, updated, or deleted since the last sync. object exports come in two different files: object_types_v5_<env>_<seq>.parquet: Contains new object types. object_types_v5_chngs_<env>_<seq>.parquet: Contains changes to object types since the previous sync. These files have an identical structure and a part of the same data set. You should import them to the same table. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the object. object_type_id ✅ INTEGER (Required). Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. name STRING (Required). The name of the object type, like "Accounts" or "Companies." slug STRING (Required). The value you use to reference objects of this type with 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}}.. For example, if your object type is Accounts, you’ll typically reference objects using {{objects.accounts}}. deleted BOOLEAN (Required). If true, the object type has been deleted. enabled BOOLEAN (Required). If true, the object type is enabled. You can’t use disabled object types in segments, messages, and so on. Learn more updated_at TIMESTAMP (Required). The date and time the object type was last updated. Object Attributes Object attribute exports contain changes to object attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The initial export includes a list of your current objects and their attributes. Subsequent files contain changes to object attributes, with one change per row. If your object attributes contain nested JSON, the attribute_name is the top-level attribute and the attribute_value returns the stringified JSON for that attribute. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. object_type_id Object Types INTEGER (Required). The type of the object represented by the internal_object_id. Object type IDs begin at 1 and increase sequentially. For example, if you created objects call Accounts and Companies, in that order, they’d have object types 1 and 2 respectively. internal_object_id ✅ Objects STRING (Required). A unique, immutable ID that Customer.io assigns to the object. You can resolve this value to the object name or ID you’re familiar with from the associated Objects export. attribute_name STRING (Required). The attribute that changed. attribute_value STRING (Required). The new value of the attribute. timestamp TIMESTAMP (Required). The timestamp of the attribute update. Events Events are the things people do in your app, on your website, etc. The Events export includes a list of events that people have triggered, with one event per row. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the person. event_id ✅ STRING (Required). The ID of the event, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who performed the event. Use the people parquet file to resolve this ID to an external customer_id or email address. name STRING (Required). The event name. type STRING (Required). One of event, page, or screen; page and screen represent page and screenviews respectively. The event value represents any other kind of event. data STRING (Required). A stringified object containing the event properties—the event payload aside from the name, timestamps, and ID. timestamp TIMESTAMP (Required). The Unix timestamp associated with the event. If you don't set this value yourself, this is the date-time when Customer.io received the event. processed_at TIMESTAMP (Required). The Unix time when Customer.io processed the event. sources ARRAY of STRINGS (Required). The source(s) of the event, e.g. Customer.io Data Pipelines via JavaScript. source_uas ARRAY of STRINGS (Required). The user agent source(s) of the event, e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0. Inbound You’ll only see the option to enable this schema if you send SMS through Customer.io. When someone replies to an SMS message you sent, we record an inbound event. The “inbound” export contains one row for each inbound SMS message you receive between syncs. Each event includes an internal_customer_id that you can use in conjunction with the People table to resolve a person’s customer_id or email address. The initial sync includes up to 30-days of past inbound events. Subsequent files contain events since the previous sync interval. We cannot backfill events older than 30 days. Field Name Primary Key Foreign Key Description workspace_id INTEGER (Required). The ID of the Customer.io workspace associated with the inbound message. event_id ✅ STRING (Required). The unique event identifier, which may be useful if you need to dedupe events. internal_customer_id People STRING (Required). The cio_id of the person who sent the message. Use the people parquet file to resolve this ID to an external customer_id or email address. timestamp TIMESTAMP (Required). The Unix timestamp when the person sent the inbound message. processed_at TIMESTAMP (Required). The Unix timestamp when Customer.io processed the event. channel STRING (Required). The messaging channel (e.g., "sms"). from STRING (Required). The phone number the person sent the inbound message from. to STRING (Required). The phone number the person replied to. body STRING (Required). The content of the inbound message. keyword STRING (Required). The keyword detected in the message, if any. optout BOOLEAN (Required). If true, the message was an opt-out request; if false, it was not. messaging_service_sid STRING (Required). The messaging service identifier from the SMS provider. message_sid STRING (Required). The unique message identifier from the SMS provider. in_reply_to_delivery_id Deliveries STRING (Required). The delivery ID of the message this inbound message is replying to, if available. We match inbound messages to deliveries within 72 hours of the original delivery. If the inbound message occurs outside the 72 hour window, or we can't attribute the inbound message to a delivery, this field is `null`. --- ## Zendesk URL: https://docs.customer.io/integrations/data-out/connections/zendesk/ Getting started Go to Data & Integrations > Integrations and select the Zendesk entry in the Directory tab. (Optional) Select the data sources that you want to connect to your outbound integration. You can always connect data sources later. We’ll only show you data sources that work with your integration. Configure your integration. Subdomain: The subdomain of your Zendesk instance. For example, if your Zendesk URL is https://example.zendesk.com, your subdomain is example. Click Enable Destination. Actions When you’re done setting things up, you can go to the Actions tab to see how we map incoming data to your integration. You may need to add actions for this integration While we often have default triggers for actions, we don't always add those actions as defaults. You may need to add actions to make sure that you're sending all the data that you want to send to your integration. See our actions page for help setting up actions. Action Default Trigger Description Create Ticket type = “track” and event = “Ticket Created” Create a new ticket. Track Event type = “track” and event != “Ticket Created” and event != “Ticket Updated” Track a custom event for an existing user. Update Organization Membership (via identify) type = “identify” and traits.company.id != null Associate a user to an organization, or remove them if traits.company.remove is true. Update Ticket type = “track” and event = “Ticket Updated” Update an existing ticket. Upsert Organization type = “group” Upsert an organization. If userId is supplied we also associate the user with the organization. Upsert User type = “identify” Insert or update a user record in Zendesk using the provided user email. Identify Zendesk Users When you call our identify API, we add or update a user record in Zendesk. To add people to Zendesk, your calls must include: traits.email—the email address of the person. A name or first_name and last_name traits. If the incoming calls includes a first_name and last_name, we parse it into the name field. When updating user records in Zendesk, we match users based on their email address of a person you identify (typically $.traits.email) or their external_id (typically $.userId). A Zendesk profile can have multiple email addresses, but each address is unique to an individual user in your Zendesk instance. { "action": "identify", "userId": "12345", "traits": { "name": "Homer Simpson", "email": "homer.simpson@example.com", "timezone": "America/Springfield", "organizationId": 6789, "phone": "555-555-1234" } } Mapping traits to Zendesk users We map incoming traits from your integrations to the following standard Zendesk user attributes. Customer.io Trait Zendesk User Attribute email email name name organizationId organization_id timezone time_zone phone phone userId external_id To store other traits in Zendesk, you must first define them as User Fields. If your identify call sends a trait that you haven’t added to your Zendesk configuration, Zendesk will discard it. You can add your custom traits in Zendesk under Admin Center > People > Configuration > User Fields. When Customer.io sends data to Zendesk, you should see the populated attributes in the customer’s context. See Adding custom fields to users in Zendesk help for more information. Adding or Removing Users to/from a Zendesk Organization You can add a user to a Zendesk organization by passing traits.company.id in your identify calls. You can also remove people from an organization by setting traits.company.remove: true. Add person to organization Remove person from organization { "action": "identify", "userId": "12345", "traits": { "company": { "id": "6789" } } } { "action": "identify", "userId": "12345", "traits": { "company": { "id": "6789", "remove": true } } }  Removing a person from an organization unassigns their tickets When you remove someone from an organization, Zendesk schedules a job to unassign all that person’s working tickets and sets the organization_id on those tickets to null Zendesk Verification Email at User Creation By default, Zendesk sends a verification email to users when you create them. You can prevent people you identify from receiving these verification emails by enabling the Skip Verify Email setting. The user’s email may still require verification if the “Create Users as Verified” option is false. Create Users as Verified By default all users are created as unverified and may receive a verification email (if the Skip Verify Email is false). If you have already verified your user’s email by other means you may wish to create them as verified in Zendesk. To do so, set the Create Users as Verified setting to true on the default Create or Update User action on your Zendesk integration. Track A track call represents an event—something someone does on your website or in your app that you want to report to Zendesk. When you make a track call, you’re sending information about a user’s activity to Zendesk. Events must be associated with a user in Zendesk. If you provide a context.zendesk_user_id we will directly track the event under that ID. Otherwise, if you provide an email and/or external_id we will search for the user and track the event with the found user. If no user is found an error will be returned. { "action": "track", "userId": "1234", "event": "Article Read", "properties": { "title": "How to use Customer.io", "course": "Intro to Customer.io" } } Mapping track events to Zendesk Sunshine  Zendesk Sunshine is not required You can use Sunshine by sending an email as described below, but this integration works without it as well. If you want to map a track call to a Zendesk Sunshine event by email, you can set the user’s email address in as properties.email in your call. This helps Zendesk tie the event to a user. If your event does not include properties.email, we send the userId as external_id. When your call includes an email address, a track call (from our JavaScript library) might look like this: cioanalytics.track('Article Read', { title: 'How to use Customer.io', course: 'Intro to Customer.io', properties: { email : 'person@example.com', } }); Group When you send a group call, we insert or update an organization in Zendesk. We use the groupId in your call to match organization records in Zendesk. If the groupId doesn’t exist, we create a new organization; if it exists, we update the existing organization. Create an organization without users Create an organization and add a user { "action": "group", "groupId": "xyz999", "traits": { "name": "SF Giants", "url": "https://mlb.com/giants" } } { "action": "group", "groupId": "abc123", "userId": "1234", "traits": { "name": "SF Giants", "url": "https://mlb.com/giants" } } Organization attributes Organizations in Zendesk have standard attributes. We map to the subset of these attributes listed below. Segment Field Name Zendesk Field Name name name domainNames domain_names tags tags groupId external_id url url deleted deleted When you pass traits in your group calls, we’ll first try to map traits to known, existing fields in Zendesk—either the standard fields above or custom fields you’ve already created. If a custom field does not exist, we’ll create it. We format incoming traits to camelCase or snake_case as necessary; if you don’t see custom fields populate in Zendesk, you may need to check the way you format traits in your group calls. --- ## Customer.io APIs URL: https://docs.customer.io/integrations/api/customerio-apis/ We have three APIs and webhooks. This page covers our APIs and the best ways to get data into and out of Customer.io if you're using a terminal, Postman, or writing your own integration.  Use one of our libraries or SDKs! We have ready-made libraries and SDKs that make it easy to use our APIs and provide other features like in-app messaging, retry logic, and so on. While our APIs can help you get started—and our API documentation includes examples for some of our libraries—you’ll almost always do better with a library or SDK. Check out our integration directory for more information. Just getting started? To get data into Customer.io, use an SDK or the Pipelines API. This is the API that most of our integrations are based on and likely the API you’ll use the most. If you’re new to Customer.io, this is where you should start. To send broadcasts, transactional messages, and fetch data (outside of a data out integration), use the App API. This API is named as such because it’s the API that you’d use to build an app on top of Customer.io. But the most popular use case is to send broadcasts and transactional messages. There are two ingress APIs We have two different APIs that you can use to get data into Customer.io—the Pipelines API and Track API. They effectively do the same things within Customer.io, so you can’t really go wrong. But if you’re just getting started with Customer.io, we recommend our Pipelines API because it’s easier to use with newer outbound integrations and it’s where we’re focusing our future development efforts. Learn more about the differences between the Pipelines and Track APIs. The APIs are similar. They both use basic authorization, and they’re both designed to send data to Customer.io with minimal resistance (we handle errors after receiving data) and so on. But there are some differences: The Pipelines API only uses POST operations. You’ll perform deletes and other operations by sending events with specific names. The Pipelines API natively supports objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. and relationshipsThe connection between an object and a person in your workspace. For instance, if you have Account objects, people could have relationships to an Account if they’re admins. through the /group endpoint. The Track v1 API and integrations based on this API don’t support most object and relationship operations. The Pipelines API Most of our integrations are based on this API, so we recommend starting here. If you use any of our mobile SDKs or libraries, you’re effectively using this API! Unlike our Track API, this API is made to be relatively generic and support not only Customer.io but all the services we integrate with. If you intend to send data to services outside of Customer.io and use Customer.io like a Customer Data Platform (CDP), then this API is perfect for you. But if you’re only sending data into Customer.io, and you don’t plan to use an SDK or a library, then this API might seem a little obtuse. For example, if you want to delete a person in Customer.io using the Pipelines API, you’ll send a POST call to the /track endpoint and include a specific event parameter. It’s not a DELETE operation, which is what you might expect! Events with special meanings The Pipelines API only contains POST calls. For deletes and other operations, you’ll use track events with a specific name parameters. We use the event name to determine what to do with the data. We sometimes refer to these as semantic events. For example, if you wanted to delete a person, you’d send a track event with a name parameter called Delete Person. If you want to suppress a person, you’d send an event called Suppress Person. This paradigm of “events with specialized meanings” isn’t unique to Customer.io operations. We have suites of semantic events to support different downstream integrations as well. If you integrate with an eCommerce platform or a CRM, you might see events like Add to Cart, Purchase, Update Customer, and so on. The Track API The Track API is oriented towards getting data into Customer.io specifically, which can make it easy to use if you only integrate with Customer.io. But we translate data from the Track API to the Pipelines API for downstream integrations. That can make it hard to troubleshoot integrations, because you’ll have to understand how we map operations between the two APIs. For example, imagine that you use the Track API and integrate with both Customer.io and another system like Mixpanel. In this case, it’ll be easy to understand the data you send to Customer.io, but it may be harder to understand (and troubleshoot) the data that you send to Mixpanel. All of our newer libraries are based on the Pipelines API. You can still use the Track API alongside libraries that use the Pipelines API, but if you’re going to use our libraries anyway, you may want to use the Pipelines API so you don’t have learn two sets of operations! The App API Unlike the Pipelines and Track APIs, the App API uses bearer authorization. The App API serves three major purposes: Trigger broadcasts: you’ll set up a broadcast in our UI and then trigger it with a single call. You can trigger a message to a wide array of people when things like new products become available, you announce tickets for an event, it’s time to register for classes for the next semester, and so on. Trigger transactional messages: send receipts, password reset requests, and important notifications. You can trigger transactional messages for individual people when someone does something on your website or in your app. For example, you might send a receipt when someone buys something, a password reset email when someone forgets their password, and other similar cases. Fetch data from Customer.io: look up people, campaign information, and so on. You can fetch data from Customer.io to use in your application. For example, you might look up a person’s information to show them a personalized message on your website, or you might look up campaign information to see how a campaign is performing. Reporting Webhooks Reporting webhooks send information about messages sent from Customer.io to downstream systems. So when you draft a message, send a message, someone opens a message, and so on, we send a webhook to an address or system you choose. We have various downstream integrations that support message events similar to reporting webhooks. If you use an integration that captures information about messages sent from Customer.io, this documentation may help you understand the different events and information we send to those services. --- ## Pipelines vs Track API URL: https://docs.customer.io/integrations/api/track-vs-cdp-api/ A comprehensive comparison of the Track API and Pipelines API to help you understand when and how to use them. TL;DR: Which API should I use? We recommend that you use the Pipelines API unless you send data to Customer.io through a customer data platform (CDP) like Segment or Rudderstack. The Pipelines API is newer and includes support for newer Customer.io features like geolocation. General differences We have two APIs to help you get data into Customer.io: the Track API and the Pipelines API. If you’re new to Customer.io: most of our libraries and SDKs are based on the Pipelines API; it’s what you’ll use for most integrations. If you’re migrating from an earlier integration that used the Track API, this page can help you understand the differences between your track-based integration and our Pipelines API. The Pipelines API is based on customer data platform (CDP) APIs like Segment or Rudderstack. It’s a bit more generic than the Track API, and that means you may need to map some concepts from the Pipelines API to their equivalents in Customer.io. But, most of our integrations are built on this API. Understanding it can help you better support your integrations. The Track API is purpose-built for Customer.io, but libraries built on it don’t support as many features as our newer, Pipelines-based integrations. While there are two versions of the Track API, we typically mean the track v1 API when we talk about The Track API because the v2 API isn’t used in any of our libraries and rarely used in libraries built by third parties; it’s much more common that you’d encounter the v1 API. Feature Track API Pipelines API Base URL track.customer.io/api cdp.customer.io Version v1, v2 (v2 is not used in any of our libraries) v1 Authentication Basic Site ID:API Key Basic API Key: Delete calls DELETE method Use semantic events Person ID {identifier} in path (v1 API); can be id or email userId in request body; can be id or email Profile Attributes Direct key-value pairs in identify calls traits object in identify calls Event name name parameter event parameter Event properties data object properties object Timestamps Unix timestamps (integers in seconds) ISO 8601 strings (down to milliseconds) Object support v2 version of the Track API /group call Request Limit 32 KB single (v1/v2), 500 KB batch (v2 only) 64 KB single, 1 MB batch JavaScript snippets While we typically offer integrations based on our Pipelines API, we have two different JavaScript snippets—one based on the Pipelines API and one based on the Track API. We recommend the snippet based on the Pipelines API because it offers features that aren’t available for the classic, track API-based snippet. You’ll find the recommended, Pipelines-based JavaScript snippet in our integrations catalog. If you have an older integration, you might be using our Track-based JavaScript snippet. It’s no longer available in our integrations catalog, but we have code samples in our legacy JavaScript documentation. Pipelines-based snippet Track-based snippet t-src https://cdp.customer.io/v1/analytics-js/snippet/ https://track.customer.io/v1/analytics-js/snippet/ Authentication analytics.load("YOUR_WRITE_KEY"); t.setAttribute('data-site-id', 'YOUR_SITE_ID'); Invoked with analytics or cioanalytics _cio Supports anonymous in-app messaging Supports objects requires a work-around Supports batching Get API keys While both APIs use basic authentication, you’ll get and use API keys differently for the two APIs. The Pipelines API uses basic authentication with a single key rather than a username and password. When you use the API directly, you’ll enter the key as the username and a blank password. The Track API uses a traditional basic authentication scheme with a site ID and API key as the username and password respectively. Get a Pipelines API Key Go to Integrations and click Add Integration. Find the Customer.io API integration. Give the integration a name. The name helps you find and differentiate between different API credentials; you might name them for users, environments, or the services you use them for. Use the key to send a successful test call. You can’t save your credentials until you’ve sent a successful test: curl --request POST \ --url https://cdp.customer.io/v1/identify \ --header 'Authorization: Basic <your key here>'\ --header 'content-type: application/json' \ -d ' { "userId": "97980cfea0067", "traits": { "name": "Cool Person", "email": "cool.person@example.com" } }' Click Complete Setup. Get Track API credentials Go to > Workspace Settings > API and webhook credentials. Click Create Track API Key. Give your credentials a name and select the workspace you want to use them in. The name helps you find and differentiate between different API credentials; you might name them for users, environments, or the services you use them for. Click Create Track API Key one last time, and you’ll have your credentials. You’ll use your Site ID and API Key as a username and password for basic authorization when you call our API. You can use these credentials with partners like Segment. If you go to the Integrations page, you’ll see your new credentials listed under Customer.io API: Track as Journeys API: <credential name>. Two versions of the Track API The Track API has two versions: v1 and v2. While we discuss the v2 API in a few places on this page, and it can be useful if you write your own integration, it isn’t used in any of our libraries. Most of the time, when we talk about The Track API, we’re talking about the v1 API. It was much more commonly used in our older integrations and is still in use in some third-party integrations. For example, if you integrate with Customer.io using Segment, Segment sends data to Customer.io using the Track v1 API. The major differences between the v1 and v2 APIs are that the v2 API only has two endpoints: entity and batch and determines what to do based on the type and action in the payload. The type represents the thing you want to work with and the action is what you want to do with it. Unlike our v1 API, the v2 API supports objects and relationships natively. Deleting data The Track API supports DELETE calls to remove data. The Pipelines API does not; it only contains POST calls to add and update data. To delete data with the Pipelines API, you’ll send a POST to the /track endpoint with a specific event name—we call these “semantic events.” For example, you can send a POST to the /track endpoint with the event name User Deleted to remove a person from Customer.io. See Semantic Events for a list of the semantic events you can use with Customer.io and our Pipelines API. Identifying people: id and userId In Customer.io, people have an id and an email address. You can use either to identify them, but they appear differently in our APIs. When you use the Track v1 API, you identify people by their id or email address as a part of the URL path. For the Pipelines API, you pass a userId in your payloads. This value can be an id or an email address and Customer.io will automatically handle it as the correct identifier. If you need to pass both, you’d pass the userId and a traits.email attribute to store the email address. Track API Pipelines API PUT /api/v1/customers/user123 POST /v1/identify/ { "email": "user@example.com", "name": "cool person" } { "userId": "user123", "traits": { "email": "user@example.com", "name": "cool person" } } Attributes in identify calls The Track API treats all values in the payload of an identify call as attributes. The Pipelines API uses a traits object to pass attributes. This is because the Pipelines API supports different objects in its payload (like context and integrations, etc.) Track API Pipelines API PUT https://track.customer.io/api/v1/customers/user123 POST https://cdp.customer.io/v1/identify/ { "email": "user@example.com", "name": "cool person", "likes_pizza": true } { "userId": "user123", "traits": { "email": "user@example.com", "name": "cool person", "likes_pizza": true } } Tracking events The Track v1 API includes the identifier for the person performing the event in the URL path (user123 in the example below), has a name parameter to pass the event name, and includes a data object to pass event properties. The Pipelines API expects the userId in the payload, uses the event parameter to pass the event name, and includes a properties object to pass event properties. Track API Pipelines API PUT https://track.customer.io/api/v1/customers/user123/events POST https://cdp.customer.io/v1/track { "name": "purchase", "data": { "price": 23.45, "product": "socks" } } { "userId": "user123", "event": "purchase", "properties": { "price": 23.45, "product": "socks" } } Managing objects (groups) The Pipelines API is the preferred way to support objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. because it has a native concept of objects and a more consistent structure with the /group endpoint. To support objects with the Track API, we suggest you use the v2 version of the Track API. But, if you’re using any of our libraries based on the Track API—all of which use v1—you’ll need to support objects as a part of your identify calls. You cannot delete objects with the Track v1 API. Pipelines API (Recommended) Pipelines API (Recommended) The Pipelines API has the most conventional payload for objects. You’ll pass a groupId representing the object, traits representing the object’s attributes, and a traits.relationship_traits object representing the relationship between the object and the person. POST https://cdp.customer.io/v1/group/ { "userId": "user123", "groupId": "group123", "traits": { "object_type_id": "1", "account_name": "Acme", "account_type": "enterprise", "relationship_traits": { "is_admin": true, "position": "account manager" } } } Track API v2 Track API v2 The Track v2 API has a more conventional payload for objects. While this is an easy way to support objects, none of our libraries rely on this API, so you’ll only use it when you call the API directly. The v2 API only has entity and batch endpoints. To support objects, you’ll set the type in the payload to object to work with objects. POST https://track.customer.io/v2/entity/ { "identifiers": { "object_type_id": "1", "object_id": "group123" }, "type": "object", "action": "identify", "attributes": { "account_name": "Acme", "account_type": "enterprise" }, "cio_relationships": [ { "identifiers": { "id": "user123" }, "relationship_attributes": { "is_admin": true, "position": "account manager" } } ] } Track API v1 Track API v1 The track v1 API doesn’t support objects, but you can pass a cio_relationships object to your identify calls to add and remove objects. If the object_id in your request doesn’t exist, we’ll create it. While you can create and relate objects to people with this action, you can’t add attributes to an object with this action. You’d need to use one of our other APIs to do that. PUT https://track.customer.io/v1/customers/user123/ { "first_name": "Bob", "plan": "basic", "cio_relationships": { "action": "add_relationships", "relationships": [ { "identifiers": { "object_type_id": "1", "object_id": "group123" }, "relationship_attributes": { "is_admin": true, "position": "account manager" } } ] } } Timestamps By default, the Pipelines API uses ISO 8601 timestamps (down to milliseconds), like 2023-04-26T13:42:19.722Z. The Track API, by comparison, uses Unix timestamps (in seconds), like 1361205308. Customer.io typically relies on Unix timestamps for things like segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. conditions where you might do time-based calculations. For example, if you check that an attribute is a timestamp in a segment, it will evaluate to true if the value is a Unix timestamp and false if it’s an ISO timestamp. Most of the time, you don’t need to worry about this difference. By default, Customer.io converts ISO-8601 timestamps that come from the Pipelines API (and associated libraries) to Unix timestamps to support things you might do in Customer.io. But this distinction can be important if you need to store and manipulate timestamps in your own code. Why does the Pipelines API use ISO 8601 timestamps when Customer.io generally expects Unix timestamps? Because ISO 8601 timestamps are common to many other platforms, and the Pipelines API is designed like a traditional customer data platform (CDP)—where you can send data to both Customer.io and other destinations. The Pipelines API also includes additional timestamps—fields like receivedAt, sentAt, and originalTimestamp. Track API Pipelines API Unix timestamp (seconds) ISO 8601 format { "timestamp": 1361205308 } { "timestamp": "2023-04-26T13:42:19.722Z", "receivedAt": "2023-04-26T13:42:20.512Z", "sentAt": "2023-04-26T13:42:20.477Z", "originalTimestamp": "2023-04-26T13:42:19.722Z" } The Pipelines API captures context and integrations Because the Pipelines API is based off of traditional customer data platforms (CDPs) like Segment, it has reserved objects for context and integrations. The context object contains information about the event, like the IP address and user agent. The integrations object provides a way to determine which destinations you want to send your data to. The Pipelines API has lax and strict modes The Track API automatically validates certain aspects of requests—like the presence of required fields and the size of the request body. The Pipelines API does not do this validation by default. Instead, it returns a 200 response for almost everything and logs errors in Customer.io. But you can enable the same validation available on the Track API by setting the X-Strict-Mode header to 1. Pipelines validation modes Error Type Strict Mode (X-Strict-Mode: 1) Non-Strict Mode (default) Authentication errors Return HTTP 401/400 with error details Logged but return HTTP 200 success Content encoding errors (invalid gzip/deflate) Return HTTP 400 with specific error messages Logged but return HTTP 200 success Request size violations (>32KB) Return HTTP 400 with size limit details Logged but return HTTP 200 success JSON validation failures Return HTTP 400 with validation error details Logged but return HTTP 200 success Missing required fields Return HTTP 400 with field-specific error messages Logged but return HTTP 200 success Required fields by request type When using strict mode, the following fields are validated. If your payload doesn’t include these fields, it’ll fail with a 400 error. Identify, Track, Page, Screen, and Alias calls require at least one of userId or anonymousId. Track calls require an event name. Group calls require a groupId. Alias calls require a previousId. Maximum request sizes The Track API has a 32 KB limit for single requests and a 500 KB limit for batch requests—which are only available in the v2/batch endpoint for the Track API. The Pipelines API has a 64 KB limit for single requests and a 1 MB limit for batch requests. --- ## Integrate external tools with Design Studio URL: https://docs.customer.io/integrations/api/integrate-with-ds/ If you design your emails in an external tool like Figma or Stripo, you can integrate them with Design Studio to reduce the number of manual steps you need to take to start sending your emails in Customer.io. You can programmatically manage emails, translations, and components through the Design Studio endpoints. To upload or update assets like images and PDFs, you’ll use a different set of endpoints: the Assets endpoints. Both the Assets and Design Studio endpoints are part of our App API and use the same bearer token for authentication. How it works Regardless of which external editor you use, the process for preparing and integrating your email with Customer.io follows the same pattern: Design your email in your preferred tool. Follow the best practices below for crafting your HTML. In Workspace settings, create an App API key for authentication and integration with Design Studio. Push the HTML to Customer.io via the Design Studio endpoints. Connect the email to a campaign, broadcast, or transactional message. Limits to Design Studio APIs Currently, the Design Studio endpoints only support content management. It’s not a content delivery platform yet, so you can’t publish emails directly. If you update a message in Design Studio from the API, make sure you go into your workspace and publish your changes before sending. The Design Studio endpoints only manage design studio content. You can’t do the following: Manage messages made in other editors, including email editors like the drag-and-drop editor Manage global styles (color palettes, font families, button formatting, etc) Connect an email to a campaign, broadcast, or transactional message Publish changes from global styles, components or emails to your campaigns, broadcasts, or transactional messages Send emails You can send a Design Studio email programmatically through the App API, but only for broadcasts and transactional messages. You control how campaigns are triggered (and start sending) from within your workspace. Use our standard component syntax if you want to edit in Design Studio’s UI We recommend using our standard component syntax so you have full access to our visual editor. This way you can easily make changes to content, formatting, or styling after adding your email. Standard components use extended HTML, similar but not the same to standard HTML tags. For instance, you’d include <x-paragraph> instead of <p> in your email code. Without our component syntax, you’ll only be able to edit some of the content and styling from the visual editor. You can edit semantic tags like <p> and <img> but need to take an extra step to modify non-semantic tags. Semantic means the name clearly defines the purpose of the element. Non-semantic tags include <div> and <span>, and you can edit the text only if you add some code. To edit the content of non-semantic tags in the visual editor, wrap the text of these elements in <x-edit-text>. This is also needed to edit text within <table>. Unlike semantic tags, you won’t be able to add CSS styles or classes through the visual editor’s Properties menu. HTML/CSS best practices Format your HTML to account for these best practices: Escape HTML strings: In request payloads, you’ll send stringified HTML, not raw HTML. Unescaped, raw HTML in a JSON body is the most common cause of 400 Bad Request errors, so this should help reduce failures: All double quotes must be escaped as \" Literal new lines must be replaced with \n (or the HTML must be on a single line). Before sending, either run your HTML through a JSON string escaper tool, or use JSON.stringify() in a browser console: copy(JSON.stringify(yourHTMLString)) to get a properly escaped string ready to paste. CSS inlining: Most email clients strip <style> blocks. Inline your CSS before import, or enable Design Studio’s CSS inlining transformer. You can set transformers directly in the API request when creating/updating the email, so you don’t need to do this as a separate step after import. Root elements: <x-base> is the root element of Design Studio messages. It replaces <!doctype html>, <html>, <head> and <body> in standard HTML messages and is a container component you can style in the visual editor. For best results, we recommend using it in place of the typical html document structure. However, either will work. Just make sure you don’t include both the <x-base> component and <html>. Dynamic content: liquid personalization and unsubscribe links Liquid personalization: Replace placeholder text with Customer.io liquid tags like {{customer.first_name}} or {{trigger.order_id}} where you want personalized content. Learn more about personalizing messages with liquid. Unsubscribe links: Add {% unsubscribe_url %} somewhere in your email. This is required for marketing emails under regulations like CAN-SPAM and GDPR. Customer.io uses this tag to generate an unsubscribe link unique to each recipient and automatically adds a List-Unsubscribe header to your email. Learn more about options for unsubscribe links. For transactional messages, an unsubscribe link is recommended but not strictly required. Host images publicly or upload them through the Assets API All images must be at publicly accessible URLs. Design Studio does not support base64 inline images. You can host images in a few ways: Through your own Content Delivery Network (CDN) Through your external editor’s built-in image host By uploading files to Customer.io first through the UI or the Assets API and then copying the URL --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/ios/quick-start-guide/ This guide contains the minimum steps you'll need to follow to install and start using the Customer.io SDK in your iOS app.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview Our SDK lets you build native mobile apps for iOS. When you’re done, you’ll be able to identify people, track their activity, and send both push notifications and in-app messages. Install and initialize the SDK Identify and Track Add push notification support Add in-app messaging support 1. Install and initialize the SDK Add a new iOS connection in Customer.io to get your CDP API key. See Get your CDP API key for details. You’ll use this API key to initialize the SDK. Install the SDK. We typically recommend that you install the SDK using Swift Package Manager (SPM). But if your app uses CocoaPods, you can install our pods. Swift Package Manager (SPM) Swift Package Manager (SPM) Follow Apple’s instructions to add https://github.com/customerio/customerio-ios.git as a dependency to your project in Xcode and select all the packages you need. Set the Dependency Rule to Up to Next Major Version. While we encourage you to keep your app up to date with the latest SDK, major versions can include breaking changes or new features that require your attention. CocoaPods CocoaPods Add the pods to your Podfile: pod 'CustomerIO/DataPipelines' # Required pod 'CustomerIO/MessagingPushAPN' # Optional, for APNs pod 'CustomerIO/MessagingPushFCM' # Optional, for FCM pod 'CustomerIO/FirebaseWrapper' # Required for FCM push notifications pod 'CustomerIO/MessagingInApp' # Optional, for in-app messaging Import the appropriate packages. We’re importing all our packages, but you can modify this list if you don’t intend to use all Customer.io features. If you send notifications using Firebase Cloud Messaging (FCM), import the CioMessagingPushFCM and CioFirebaseWrapper package rather than CioMessagingPushAPN. UIKit with Swift UIKit with Swift import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN import UIKit UIKit with Objective-C UIKit with Objective-C @import UIKit; @import CioDataPipelines; @import CioMessagingInApp; @import CioMessagingPushAPN; SwiftUI SwiftUI import SwiftUI import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN Initialize the SDK. You’ll usually do this in the AppDelegate application(_ application: didFinishLaunchingWithOptions) function, and you’ll use the CioAppDelegateWrapper to handle push notifications and device token registration and initialize the SDK. You also need the CDP API Key that you obtained when you added your iOS integration to your workspace. UIKit with Swift UIKit with Swift @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } ``` UIKit with Objective-C UIKit with Objective-C @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @end // AppDelegate.m #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize the Customer.io SDK NSString *cdpApiKey = @"YOUR_CDP_API_KEY"; NSString *siteId = @"YOUR_SITE_ID"; CioSDKConfigBuilder *config = [[CioSDKConfigBuilder alloc] initWithCdpApiKey:cdpApiKey]; // If your account is in the EU region, uncomment the next line // [config region:CioRegionEU]; [config migrationSiteId:siteId]; // only required if you used version 2.x or earlier [config autoTrackUIKitScreenViews]; // Set auto tracking of UIKit screen views [config logLevel:CioLogLevelDebug]; // Add this to troubleshoot issues - disable debug in production [CustomerIO initializeWithConfig:[config build]]; return YES; } @end SwiftUI SwiftUI @main struct MainApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) private var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } 2. Identify and Track Users Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. Swift Swift // Identify a user with just a user ID CustomerIO.shared.identify(userId: "user@example.com") // Identify a user with additional attributes CustomerIO.shared.identify( userId: "user@example.com", traits: [ "email": "user@example.com", "first_name": "John", "last_name": "Doe", "plan_name": "Premium", "device_type": "iOS" ] ) // clear the user identity when they log out CustomerIO.shared.clearIdentify() Objective-C Objective-C // Identify a user with just a user ID [CustomerIO identifyUserWithId:@"user@example.com"]; // Identify a user with additional attributes [CustomerIO identifyUserWithId:@"user@example.com" traits:@{ @"email": @"user@example.com", @"first_name": @"John", @"last_name": @"Doe", @"plan_name": @"Premium", @"device_type": @"iOS" }]; // clear the user identity when they log out [CustomerIO clearIdentify]; Track a custom activity using the CustomerIO.track method. Events help you trigger personalized campaigns and record user behavior in your app. Swift Swift // Track a simple event without properties CustomerIO.shared.track(name: "checkout_started") // Track an event with properties CustomerIO.shared.track( name: "product_viewed", properties: [ "product_id": "SKU-123", "product_name": "Premium Widget", "price": 99.99, "currency": "USD", "category": "Electronics" ] ) Objective-C Objective-C // Track a simple event without properties [CustomerIO trackEventWithName:@"checkout_started"]; // Track an event with properties [CustomerIO trackEventWithName:@"product_viewed" properties:@{ @"product_id": @"SKU-123", @"product_name": @"Premium Widget", @"price": @99.99, @"currency": @"USD", @"category": @"Electronics" }]; 3. Add push notification support In your AppDelegate, after your CustomerIO.initialize call, add the MessagingPushAPN.initialize call. Our push package has its own settings so you can configure push behaviors. These instructions are for Apple’s Push Notification service (APNs). If you send push notifications using Firebase Cloud Messaging (FCM), see our push notification instructions. Swift Swift // Initialize the push package MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) // Request permission to show notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in // Process user response here } Objective-C Objective-C // Initialize the push package [MessagingPushAPN initializeWithConfig:[[MessagingPushConfigBuilder new] autoFetchDeviceToken:YES // Automatically fetch device token and upload to CustomerIO autoTrackPushEvents:YES // Automatically track push metrics showPushAppInForeground:YES // Enable Notifications in the foreground build]]; // Request permission to show notifications [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) { // Process user response here }]; 4. Add in-app messaging support In your AppDelegate, after your CustomerIO.initialize call, add the MessagingInApp.initialize call. Within the call, you’ll need a Site ID, which you can get from your workspace settings. This tells the SDK which workspace your in-app messages come from. Swift Swift // Initialize the in-app package // Change region to .EU if you're in our European Union data center! MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) Objective-C Objective-C // Initialize the in-app package [MessagingInApp initializeWithConfig:[[MessagingInAppConfigBuilder alloc] initWithSiteId:siteId region:CioRegionUS]]; --- ## How it works URL: https://docs.customer.io/integrations/sdk/ios/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our iOS SDK is a data inAn integration that feeds data into Customer.io. in Customer.io. You can route data from your app to both Customer.io and other destinations. This makes it easier to use your app as a part of your larger data stack without relying on extra services or code. When you set up your integration in Customer.io, you’ll determine where you want to route your data to—your workspace and destinations outside of Customer.io. Minimum support requirements To support the Customer.io SDK, you must: Set iOS 13 or later as your minimum deployment target in XCode Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Objective-C support Our SDK is written in Swift and tested in a Swift environment. Our iOS SDK may work in Objective-C-based projects, but we haven’t tested it that way. If you use our SDK in an Objective-C project and run into trouble, please let us know. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/ios/getting-started/auth/ To use the SDK, you'll need to get two kinds of keys: A CDP *API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. To get your SDK keys and send data to the right places, you’ll need to set up your app as a data inAn integration that feeds data into Customer.io. integration in Customer.io, and route it to your workspace. The SDK lets you route data to any number of destinations, but you must connect it to your workspace destination to send data, like the people you identify, the events you track, and so on, to Customer.io. If you haven’t already set up your app as an integration in Customer.io, do that first. API Keys you’ll need API Key: This key, shown in code samples as cdpApiKey, lets you send data to Customer.io. You’ll need it to initialize the SDK. You’ll get this key when you set up your integration in Customer.io. Site ID: This key tells the SDK which workspace your messages come from. You’ll use it to initialize the CioMessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your mobile app’s integration card in Customer.io. If you haven’t already set up your Android integration in Customer.io, you’ll need to do that first. Go to Integrations. Select your iOS integration in the Overview tab. If you don’t see an iOS integration, you’ll need to set it up. Go to Settings and find your API Key. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should keep the siteId that you used in previous versions as the migrationSiteId in your config. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { ... let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId(YOUR_SITE_ID) .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) }  You’re not done yet You still need your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials to initialize the CioMessagingInApp package and to support people updating your app from a previous version of Customer.io SDK. See Get your Site ID below. Set up a new integration If you don’t already have a write key, you’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select the iOS integration. Enter a Name for your integration, like “My iOS App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy; you’ll use it in your initialization call. Click Complete Setup to finish setting up your integration. Now the integrations Overview page shows that your iOS app is connected to your workspace. You can also connect your iOS app to any number of destinations if you want to send your mobile data to additional services—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your Site ID to initialize the CioMessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version, my can also set your Site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the CioMessagingInApp package. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { ... let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId(siteId) .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/ios/getting-started/packages-options/ The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use. SDK packages To minimize our SDK’s impact on your app’s size, we’ve split the SDK into packages. You can limit your install to the packages that you need for your project. You must install the CioDataPipelines package. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Product Required? Description CioDataPipelines ✅ identify people and track events CioMessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) CioMessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) CioFirebaseWrapper Required for FCM push notifications. Provides Firebase Cloud Messaging integration CioMessagingInApp Receive in-app notifications CioLocation Enrich user profiles with accurate device location Configuration options When you install the SDK via CocoaPods, you can find our packages by replacing the Cio in package names with CustomerIO/—e.g. CustomerIO/DataPipelines. For FCM push notifications, you’ll need both CustomerIO/MessagingPushFCM and CustomerIO/FirebaseWrapper. Configuration options You’ll call configuration options before you initialize the SDK with SDKConfigBuilder. When you initialize the SDK, you can pass configuration options. In most cases, you’ll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") // Mandatory for all customers .migrationSiteId("YOUR_SITE_ID") // Mandatory only for migrating customers .autoTrackDeviceAttributes(true) .region(.EU) CustomerIO.initialize(withConfig: config.build()) Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region .EU or .US .US Required if your account is in the EU region. This sets your account region in the format Region.US. apiHost string The domain you’ll proxy requests through. You’ll only need to set this (and cdnHost) if you’re proxying requests. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackUIKitScreenViews boolean false For UIKit-based apps: if true, the SDK automatically sends screen events for every screen your audience visits. cdnHost string The domain you’ll fetch configuration settings from. You’ll only need to set this (and apiHost) if you’re proxying requests. logLevel string error Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. migrationSiteId string Required if you're updating from 2.x: the credential for previous versions of the SDK. This key is used to send remaining tasks to Customer.io when your audience updates your app. screenViewUse .all or .inApp all screenView: .all (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. screenView: inApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events Proxying requests By default, requests go through our domain at cdp.customer.io. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores. To proxy requests, you’ll need to set the apiHost and cdnHost properties in your SDKConfigBuilder. While these are separate settings, you should set them to the same URL. While you need to initialize the SDK with a cdpApiKey, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the cdpApiKey to a value representing basic authentication credentials that you handle on your own. See proxying requests for more information. let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .apiHost("YOUR_PROXY_HOST") .cdnHost("YOUR_CDN_HOST") CustomerIO.initialize(withConfig: config.build()) visionOS Support The iOS SDK supports VisionOS. We have a handy sample app that demonstrates how to use the SDK with Vision Pro devices, along with a handy readme, in the Apps/VisionOS directory. We’ve only tested the iOS SDK with visionOS using Swift Package Manager. If you use CocoaPods, everything might work, but we can’t guarantee it. Also, for now, we only support Apple’s Push Notification Service (APNS) for visionOS. You won’t be able to send push notifications to Vision Pro devices using Firebase Cloud Messaging (FCM). --- ## Swift 6 URL: https://docs.customer.io/integrations/sdk/ios/getting-started/swift-six/ Swift 6 introduces language changes that can affect how you integrate the Customer.io iOS SDK. This page explains what to expect when your app uses Swift 6. General Swift 6 was introduced in 2024 with Xcode 16. It includes language updates focused on safer, more predictable code, including macro improvements, stronger C++ interoperability, and stronger data-race detection through Strict Concurrency. Strict Concurrency helps you catch concurrency issues at compile time by enforcing actor isolation, Sendable requirements, and safer async boundaries. Many iOS apps are still migrating to Swift 6, while others have already completed the transition and run Swift 6 in production. Compatibility To maintain the broadest compatibility possible, our iOS SDK does not yet depend on Swift 6. Starting with Customer.io iOS SDK version 4.2.0, we have no known compatibility issues for apps that use Swift 6. Future SDK versions will be built with Swift 6, so if your app is still in the migration process, we encourage you to complete that transition. The demo apps in the iOS repository now build with Swift 6.2 and enable most optional Swift 6 features, including MEMBER_IMPORT_VISIBILITY. Quirks Some optional Swift 6 features change library behavior. If you enable these features in your app, your integration may differ from the examples in this documentation. MEMBER_IMPORT_VISIBILITY When you enable this flag, you need additional import statements. In practice, anywhere you use import CioDataPipelines, you should also add import CioInternalCommon. As the name implies, CioInternalCommon is not intended for direct app usage. It may bring some Customer.io utility classes into scope, but it should not introduce negative side effects. The same pattern applies when importing CioMessagingPushAPN or CioMessagingPushFCM; in both cases, also import CioMessagingPush. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/ios/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Examine data in and data out traffic Your integrations in Customer.io have Data in and Data out tabs showing you both the calls that come in from your SDK and the data we send out to your destination(s) respectively. You can examine these calls to help you debug issues. If you have a problem, go to Integrations and check: That your iOS integration is connected to your workspace. If you don’t connect your integration to your workspace, you won’t be able to send messages, etc. (Typically, it’s connected to your workspace by default.) Your integration’s Data In tab to make sure that your app is sending the right data. If data isn’t sent to your destination, or it appears incorrect in the destination, go to your outgoing integration’s Data Out tab. Check the calls there to make sure that we’re sending the right data from your SDK to the destination. This includes Customer.io: your workspace is one of the places you’ll send data from your app! Check out Troubleshooting integrations for more help pinpointing issues in your integration. Capture logs Enable debug logging in your app: Everywhere you call CustomerIO.initialize(), enable the debug log level. This includes in the Notification Service Extension that you setup for rich push.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. // During SDK initialization, enable debug logs: CustomerIO.initialize( withConfig: SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .logLevel(.debug) .build() ) Open the Console app (already installed in MacOS). This is a built-in application you can use to view logs produced by the SDK. We recommend that you use Console instead of Xcode to view and capture logs from the SDK because Xcode may not show you all of the logs the SDK generates. In Console, click Action > Include Info messages and Action > Include Debug messages. These settings ensure that you’ll see log messages from the SDK. In Console, on the left, select the iOS device that runs your app with the Customer.io SDK. If you don’t see your device listed, plug in your iOS device into your Mac. Try to use a direct connection via the Apple cable; using a USB hub might prevent the device from showing up. Then, click Start streaming. You will see hundreds or even thousands of logs printed to you. Most of these log messages are not relevant. In the next steps, we’ll filter your log to find relevant messages. In the top right search bar, type “CIO” and press Enter. Click the dropdown and select Category. You will now only see messages sent from the SDK. In the top right, click Save to save this filter. The next time you open Console, just click that saved filter along the top of the screen to see Customer.io SDK logs. Click any of the log entries on the screen (or Edit > Select All), CMD + C, then CMD + P into a text editor on your computer. Save the file as a .txt. Send the file you just saved to our support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Deep links only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. You can learn more about setting up universal links here. You can easily test universal links using your Notes app. Try adding a link to a note and tap it. If it drives you to your app, then you’ve set things up correctly! If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Universal Link opens in browser instead of app If you click on a push notification sent by Customer.io that contains a Universal Link deep link > click on the push notification > app opens for a moment > then the browser opens the URL, this could be a sign that something is wrong with your app’s Universal Link handling. The Customer.io SDK sends a request to your app’s app to give your app an opportunity to handle the Universal Link. If your app does not handle the Universal Link, the SDK will open the link in the browser instead. Let’s walk through some troubleshooting steps to try and fix this behavior so the browser does not open. In our deep links Universal Links guide, we show a function that is required to be added to your app: application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool. Add a print("Universal Links handle code called.") statement or Xcode breakpoint to verify that your code in this function does get called. If you click on a push notification and you do not see this print statement or breakpoint hit, verify that the deep link URL is a valid https URL and you have followed all of the Apple documentation linked in our Universal Links guide. If you do see your print statement or breakpoint hit, then your Universal Link URL is valid and is correctly attached to the push notification for the SDK to understand. Next, verify that your app returns true from the application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool function. If your app returns false, the SDK will open the Universal Link in the browser instead of the app. Lastly, check if there is another SDK interfering with the Customer.io SDK. In some cases, customers have reported instances where Universal Links, despite being correctly configured within your app, may unexpectedly open in a web browser. This can occur due to interactions with third-party SDKs that perform method swizzling inside your app. To address this, consider reviewing the documentation of other SDKs integrated into your app and disabling swizzling as needed. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/ios/tracking/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You need the **CioDataPipelines** package to identify people. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. If you already registered a device token, identifying a person automatically associates the token with the identified person. You can register for a device token before or after you identify a person. See our Push Documentation for help registering device tokens. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: userId (required): The unique value representing a person—an ID or email address that represents a person in Customer.io (and your downstream destinations). traits (Optional): Contains attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to set for a person. https://customer.io/api/track/#operation/identify import CioDataPipelines CustomerIO.shared.identify(userId: "989388339", traits: ["first_name": firstName]) // `traits` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let traits = ["first_name": "Dana", "last_name": "Green"] CustomerIO.shared.identify(userId: "989388339", traits: traits) // 2. `Encodable` object: struct IdentifyRequestTraits: Encodable { let firstName: String let lastName: String } CustomerIO.shared.identify(userId: "989388339", traits: IdentifyRequestTraits(firstName: "Dana", lastName: "Green")) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with profileAttributes. CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] You only need to pass the attributes that you want to create or modify to profileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.shared.deviceAttributes = ["company" : "cio", "checklist" : "complete"] However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId("YOUR_SITE_ID") .autoTrackDeviceAttributes(false) CustomerIO.initialize(withConfig: config.build()) Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Future calls to the Customer.io SDK are anonymous CustomerIO.shared.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/ios/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import CioDataPipelines CustomerIO.shared.track(name: "logged_in", properties: ["ip": "127.0.0.1"]) // You don't need to send `data` CustomerIO.shared.track(name: "played_game") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["product": "socks", "price": "23.45"] CustomerIO.shared.track(name: "purchase", properties: data) // 2. A custom `Encodable` type: struct Purchase: Encodable { let product: String let price: Double } CustomerIO.shared.track(name: "purchase", properties: Purchase(product: "socks", price: 23.45))  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.shared.track(name: "User Deleted) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/ios/tracking/screen-events/ Screen events track the screens people view in your app. In addition to tracking the parts of your app people use, screen tracking is vital to sending in-app messages. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a title representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your they use. Screen events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Automatic screen tracking We can automatically track screens for UIKit-based apps with the .autoTrackUIKitScreenViews configuration option. When you enable automatic screen tracking, the SDK sends a screen call every time a person visits a screen in your app. For apps not using UIKit, like SwiftUI apps, you should manually track screen events. To enable automatic screen tracking, you can pass .autoTrackUIKitScreenViews() to the SDKConfigBuilder or you can customize the behavior for automatic screen tracking if you want to filter the screens the SDK sends events for or add additional properties to the screen events. When you’re done, we recommend you test that automatic screen tracking works for your app. If you encounter issues, you can always send screen events manually. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews() .build() If you don’t use UIKit, or otherwise need to send your own screen events, you can send screen events manually. Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to screenView: inApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews() .screenViewUse(screenView: .all) .build() Screen names When you enable automatic screen views, the SDK automatically names the screen as the class name of the UIViewController, minus ViewController. For example, if you have a class EditProfileViewController in your code base, the SDK will automatically send a screenview event with the screen name EditProfile. Customize automatic screen tracking You can also set additional parameters to customize the behavior of automatic screen tracking. autoScreenViewBody: (optional) Closure that returns a dictionary of properties that you want to send with each automatic screen call. filterAutoScreenViewEvents: (optional) Closure that returns a boolean—true to send an automatic screen call; false to prevent an automatic call. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews( autoScreenViewBody: { return [:] }, // optional filterAutoScreenViewEvents: { (viewController: UIViewController) in return true } // optional ) .build() CustomerIO.initialize(withConfig: config) Test automatic screen tracking for your app The automatic screen tracking feature is designed to work with most UIKit-based apps, but it may not work with especially complex navigation structures. If you’ve got a significantly complex UIKit-based navigation structure, you may need to send screen events manually. When you enable automatic screen tracking, you may want to enable info-level logging and walk through your app to make sure screen events come through as expected. If you encounter issues, you probably need to send events manually for problematic screens. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId("YOUR_SITE_ID") .autoTrackUIKitScreenViews(true) .logLevel("info") CustomerIO.initialize(withConfig: config.build()) Manually track screen events Screen events use the .screen method. Like other event types, you can add a properties object containing additional information about the event or the currently-identified person. import CioDataPipelines // You can send an event with or without `properties`. CustomerIO.shared.screen(title: "DailyBaseballScores") // `properties` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["prev_screen": "homescreen", "seconds_in_app": "120"] CustomerIO.shared.screen(title: "DailyBaseballScores", properties: data) // 2. A custom `Encodable` type: struct Screen: Encodable { let prevScreen: String let secondsInApp: Int } CustomerIO.shared.screen(title: "DailyBaseballScores", properties: Screen(prevScreen: "homescreen", secondsInApp: 120)) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/ios/tracking/lifecycle-events/ By default, the Customer.io SDK for iOS automatically tracks lifecycle events for your users. These are events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track call. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. import CioDataPipelines CustomerIO.shared.track( name: "Application Crashed", properties: [ "url": "urls://page/in/app" ] ) Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option to the SDK’s config builder. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .trackApplicationLifecycleEvents(false) .build() CustomerIO.initialize(withConfig: config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/ios/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, and we merge anonymous activity with the identified person, the previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity with their identified profile, and their previously-anonymous screen view triggers the campaign you set up for people who visited the product page. You can return a person’s anonymous ID at ay time by calling CustomerIO.shared.anonymousId. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Location tracking URL: https://docs.customer.io/integrations/sdk/ios/tracking/location/ Real-time location tracking lets you update a person's profile with accurate coordinates so you can send geo-aware messages and segment users by location. How it works The Location module captures location (with user consent) from your app and attaches it to a person’s profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than IP-based geolocation. When you identify a person, the SDK includes the latest location in the identify call. The SDK also sends a Location Update event to the person’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved more than 1 kilometer since the last update. The SDK does not request location permission on its own—your app must handle the permission flow. Install the location module Add the CioLocation package to your project. If you use Swift Package Manager, add it the same way you added the other Customer.io packages. The CioLocation library is part of the customerio-ios package. If you use CocoaPods, add the pod to your Podfile: pod 'CustomerIO/Location' Initialize the SDK with the location module Register LocationModule when you initialize the SDK. The module takes a LocationConfig where you set the tracking mode. Option Type Default Description mode LocationTrackingMode .manual Controls how and when the SDK captures location. See tracking modes below. Tracking modes Mode Description .manual Your app controls when it captures location. Call setLastKnownLocation(_:) or requestLocationUpdate() to provide location. You should use this option when your app already has a location-tracking mechanism or you want full control over when you capture location data. .onAppStart The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call setLastKnownLocation(_:) or requestLocationUpdate() alongside automatic capture. Use this for hands-off location tracking with minimal battery impact. .off Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime. import CioDataPipelines import CioLocation let config = SDKConfigBuilder(cdpApiKey: "your-cdp-api-key") .addModule(LocationModule(config: LocationConfig(mode: .manual))) .build() CustomerIO.initialize(withConfig: config) Location APIs The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a Location Update event no more than once a day—and only if the person has moved more than 1 kilometer since the last update. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates. setLastKnownLocation Pass a CLLocation directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io. import CoreLocation import CioLocation // From a CLLocationManager delegate callback func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let location = locations.last { CustomerIO.location.setLastKnownLocation(location) } } requestLocationUpdate Request a one-shot location from the SDK using Core Location. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user. If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes. Add the required key to your Info.plist: <key>NSLocationWhenInUseUsageDescription</key> <string>We use your location to personalize your experience.</string> Request permission at runtime, then call the SDK: import CoreLocation import CioLocation let locationManager = CLLocationManager() // Request permission first locationManager.requestWhenInUseAuthorization() // After permission is granted CustomerIO.location.requestLocationUpdate() Profile switch behavior When you call CustomerIO.shared.clearIdentify(), the SDK clears cached location data so that one person’s location doesn’t carry over to another person’s profile. The next person you identify starts with a clean slate. Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next identify() call includes it automatically. --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/ios/push/push-setup/ Our iOS SDK supports push notifications over APN or FCM. This page can help you get started with either service. Before you begin This page explains how to register for push notifications using our SDK. But, before you can send push notifications, you need to add your push service credentials to Customer.io. See our push service certificates to learn more. If you haven’t already, you’ll need to install the push package for the push service you use—APNs or FCM—and enable the Push Notifications capability in XCode. For FCM users, you’ll also need to install the CioFirebaseWrapper dependency. See our quick start guide page for installation instructions. Register for push notifications The instructions in this section set you up to receive simple push notifications with a body and title. After you follow these instructions, you’ll need to do a bit more work to support rich push notifications. The SDK automatically handles push registration and push clicks for you. However, you’ll still need to identify users before you can send them push notifications. 1. Initialize the push service After you initialize the SDK, initialize the push service that you use in your app. Your code changes slightly depending on the push service you use. APNs APNs import CioDataPipelines import CioMessagingPushAPN import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var cdpApiKey = YOUR_CDP_API_KEY var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .autoTrackDeviceAttributes(true) .autoTrackUIKitScreenViews() .migrationSiteId(siteId) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } FCM FCM import CioDataPipelines import CioFirebaseWrapper import FirebaseCore import FirebaseMessaging import Foundation import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // FCM provides a device token to the app that // you send to the Customer.io SDK. // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } 2. Identify your audience Identify the person if you have not already. Even after you add a device token, you can’t use it until you associate it with a person. CustomerIO.shared.identify(userId: "989388339", traits: ["first_name": firstName]) When you identify a person, you should see their device token in your workspace. You can send a simple push notification to test your implementation. Note that when you identify a different person or stop identifying a person, the SDK automatically removes the device token from any previously identified profile. This ensures that a device token is only registered to the currently identified profile in the SDK and prevents you from sending duplicate messages messaging the wrong person. Push configuration options When you initialize your preferred push package (CioMessagingPushAPN or CioMessagingPushFCM), you can pass configuration options determining how the push package functions. Option Type Default Description autoFetchDeviceToken boolean true When true, the package automatically fetches the device token for push notifications. autoTrackPushEvents boolean true Automatically track opened and delivered metrics based on push notifications. showPushAppInForeground boolean true Show push notifications when the app is in the foreground. Used only if customer’s AppDelegate doesn’t implement UNUserNotificationCenterDelegate. appGroupId String nil Sets the App Group identifier for reliable push delivery tracking. When set, the SDK stores undelivered metrics in shared storage so they can be recovered on the next app launch. See App Groups for push tracking for setup instructions. Next steps To ensure reliable push delivery tracking, set up rich push. Rich push adds a Notification Service Extension (NSE) to your app, which the SDK uses to track delivery metrics. Without the NSE, the SDK can’t confirm that a push notification was delivered to the device. After you set up rich push, you can further improve delivery tracking with App Groups. --- ## Set up rich push URL: https://docs.customer.io/integrations/sdk/ios/push/rich-push/ Set up your app to support push notifications with images and deep links. 1. Add a service extension to your project Add a Service App Extension to your project in Xcode. You should now see a new file added to your Xcode project. The file is probably named NotificationService and looks similar to this. import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { } override func serviceExtensionTimeWillExpire() { } } 2. Update the service extension  Xcode 16.3+ users If you encounter errors like “not available due to missing import of defining module ‘CioMessagingPush’”, go to your NotificationServiceExtension target’s Build Settings and set Member Import Visibility (SWIFT_MEMBER_IMPORT_VISIBILITY) to “Package” or disable it. Modify your new NotificationService extension by selecting the push package you want to import and calling the appropriate Customer.io functions. Your code changes if: Customer.io is your only push/rich push provider Customer.io is not your only provider You want to take advantage of push features outside the Customer.io, like action buttons; in this case, you’ll need to set your own completion handler.  App Groups for delivery tracking The code samples below include a commented-out .appGroupId() line. To improve push delivery metric reliability, set up App Groups and enable that line with your App Group identifier. Customer.io push only Customer.io push only // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushAPN import CioMessagingPushFCM import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // Use `MessagingPushFCM` if you are using FCM as push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Multiple push services Multiple push services // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Due to the behavior of Notification Service Extensions in iOS, you need to // initialize the Push Module in both your host app and in your Notification Service. // The config builder also lets you you to configure the push module. // Use `MessagingPushFCM` if you use FCM as your push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .autoTrackPushEvents(true) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) // If you use a service other than Customer.io to send rich push, // you can check if the SDK handled the rich push for you. If it did not, you // know that the push was *not* sent by Customer.io and you can try another way. let handled = MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) if !handled { // Rich push was *not* sent by Customer.io. Handle the rich push in another way. } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Custom completion handler Custom completion handler // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioDataPipelines class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Due to the behavior of Notification Service Extensions in iOS, you need to // initialize the Push Module in both your host app and in your Notification Service. // The config builder also lets you you to configure the push module. // Use `MessagingPushFCM` if you use FCM as your push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .autoTrackPushEvents(true) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) // If you need to add features, like showing action buttons in your push, // you can set your own completion handler. MessagingPush.shared.didReceive(request) { notificationContent in if let mutableContent = notificationContent.mutableCopy() as? UNMutableNotificationContent { // Modify the push notification like adding action buttons! } contentHandler(notificationContent) } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Your app can now display rich push notifications in your app, including images, etc. See Deep Links to enable deep links in your push notifications. Improve delivery metrics with App Groups For the most reliable push delivery tracking, set up App Groups for push tracking. App Groups let the SDK recover delivery metrics that iOS might otherwise discard when it terminates the Notification Service Extension. Without this setup, you may lose some delivery data. --- ## App Groups for push tracking URL: https://docs.customer.io/integrations/sdk/ios/push/app-groups/ Configure App Groups for reliable push delivery tracking. App Groups let the SDK recover delivery metrics that iOS might otherwise discard when it terminates the Notification Service Extension. App Groups are required for reliable push delivery tracking. Without this setup, delivery metrics may be lost if iOS terminates the Notification Service Extension before the tracking request completes. With App Groups configured, the SDK automatically recovers any lost metrics on the next app launch. Before you begin Before you configure App Groups, make sure you’ve completed the following: Set up push notifications in your app Set up rich push with a Notification Service Extension (NSE) 1. Add the App Group capability in Xcode You need to add the App Groups capability to both your main app target and your Notification Service Extension target in Xcode. Automatic signing Automatic signing If you use automatic signing (the most common setup), this is the only step outside of code. Xcode registers the group and updates provisioning profiles automatically. In Xcode, select your main app target > Signing & Capabilities > + Capability > App Groups. Click + and enter your group identifier—for example, group.com.example.myapp.cio. Select your Notification Service Extension target > Signing & Capabilities > + Capability > App Groups. Select the same App Group you created in step 2. Both targets must reference the exact same App Group string. Manual signing Manual signing If you use manual signing, you need to register the group in the Apple Developer Portal and regenerate your provisioning profiles. Sign in to the Apple Developer Portal and go to Certificates, Identifiers & Profiles. Click Identifiers > + > App Groups. Enter your identifier. It must start with group.—for example, group.com.example.myapp.cio. Under Identifiers, select your main app’s App ID, enable App Groups, click Configure, and select your group. Repeat step 4 for your Notification Service Extension’s App ID. Regenerate provisioning profiles for both your main app and Notification Service Extension. Enabling App Groups invalidates your existing provisioning profiles.  You must regenerate your provisioning profiles in the Apple Developer Portal after enabling App Groups. You don’t need to regenerate certificates.  Already have an App Group? You can reuse an existing App Group by passing its identifier to .appGroupId(). There’s no conflict in having multiple App Groups on a target. 2. Pass the App Group ID in your SDK configuration After you add the App Group capability, pass the App Group ID to the SDK in both your host app and your Notification Service Extension. Both are required—App Groups work by sharing storage between the two targets, so the SDK needs the identifier in each place to read and write delivery metrics. The App Group ID must be identical in both places and must match the entitlements you set up in Xcode. Host app initialization APNs APNs MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .appGroupId("group.com.example.myapp.cio") .build() ) FCM FCM MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .appGroupId("group.com.example.myapp.cio") .build() ) Notification Service Extension initialization APNs APNs MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .appGroupId("group.com.example.myapp.cio") .build() ) FCM FCM MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .appGroupId("group.com.example.myapp.cio") .build() )  App Group ID must match everywhere The .appGroupId() value must be identical in your host app initialization, your NSE initialization, and the App Group entitlements on both targets. A mismatch prevents the SDK from accessing shared storage. Fallback behavior If you omit .appGroupId(...), the SDK attempts to infer the identifier using the convention group.{bundleId}.cio. This can work as a fallback, but we recommend explicitly passing the value to avoid configuration issues. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/ios/push/deep-links/ Deep links let you send people who interact with your messages to links in your app. You should set up deep links to make sure that your push notifications are actionable, and take people to screens that matter to your audience. Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Set up Universal Links To enable Universal Links in your iOS app, follow the instructions on the Apple documentation website. Be sure to complete all of the steps required including making modifications to your website to host a new file and making modifications to your mobile app’s code to handle the deep link. Depending on how you set up your mobile app (SwiftUI, UIKit, watchOS, etc), you may need to handle deep links in multiple functions in your code. Handling deep links on push notification click Our SDK automatically handles deep links for Customer.io push notifications, calling The SDK’s calls UIApplication.shared.open() for the deep link URL by default. You don’t need to process deep links yourself in userNotificationCenter(:didReceive:withCompletionHandler). But, if you want to control URL handling and open specific screens in your app, you should implement the application(:continue:restorationHandler:) method in your AppDelegate. class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard let universalLinkUrl = userActivity.webpageURL else { return false } // Parse `universalLinkUrl` object to perform the action you want in your app. // return true from this function if your app handled the deep link. // return false from this function if your app did not handle the deep link and you want sdk to open the URL in a browser. } } Deep link issue with calling application(:continue:restorationHandler:) Some 3rd party SDKs might block calls to the application(:continue:restorationHandler:) method. If you encounter this problem, you can use an alternative approach to receive callbacks when your audience clicks notifications with deep links. @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Step 2: initialize the SDK var cdpApiKey = YOUR_CDP_API_KEY var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .deepLinkCallback { (url: URL) in // You can call any method to process this further, // or redirect it to `application(_:continue:restorationHandler:)` for consistency, if you are already using it let openLinkInHostAppActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) openLinkInHostAppActivity.webpageURL = url return self.application(UIApplication.shared, continue: openLinkInHostAppActivity, restorationHandler: { _ in }) } CustomerIO.initialize(withConfig: config.build()) // Step 3: Initialize the in-app package // Change region to .EU if you're in our European Union data center! MessagingInApp.initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) // Step 4: Initialize the push package MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } }  Check universal links using your Notes app Try creating a note with a universal link and tapping the link to double-check that the link opens in your app and not in a browser window. This is an easy way to make sure that you’ve set up universal links correctly. If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Setup App Scheme Deep Links Open your Xcode project and go to your project’s settings. Select your app Target, click the Info tab, and then click URL Types > to create a new URL Type. Enter a unique value for your app for URL Schemes. --- ## Push metrics URL: https://docs.customer.io/integrations/sdk/ios/push/push-metrics/ Gather metrics for push notifications sent from Customer.io. Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked.  Improve delivery metric reliability Configure App Groups to make sure delivery metrics aren’t lost when iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers any undelivered metrics on the next app launch. If you already configured rich push notifications, the SDK will automatically track opened and delivered events for push notifications originating from Customer.io. See section Automatic push handling below to learn more about this great feature and how to best take advantage of it. Otherwise, you can: Record push metrics with UserNotifications. Extract delivery ID and Delivery Token parameters directly. Automatic push handling After you call MessagingPushAPN.initialize or MessagingPushFCM.initialize in your AppDelegate and set up the CioAppDelegateWrapper, your app is ready to automatically handle push notifications that originate from Customer.io. No additional code is required for your app to track opened push metrics or launch deep links.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Configure push behavior Configure push notification behavior using the MessagingPushConfigBuilder: MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) Configure push display behavior while app in foreground If your app is in the foreground and receives a push notification, Customer.io will show the notification based on the value of the showPushAppInForeground configuration property. You can choose whether or not to display push notifications when your app is foregrounded by implementing the userNotificationCenter(_:didReceive:withCompletionHandler:) method in your AppDelegate. Note that when you implement UNUserNotificationCenterDelegate, it takes priority over the showPushAppInForeground configuration property. Add the highlighted code to your AppDelegate.swift file for custom handling: class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // ... // Set the AppDelegate as a delegate for push notification events: UNUserNotificationCenter.current().delegate = self return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Function called when a push notification arrives while app is in foreground func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { // To show it, return appropriate options for your case completionHandler([.banner, .list, .badge, .sound]) // To hide it, return empty array // completionHandler([]) } } Custom handling when a Customer.io push is clicked Add the highlighted code to your AppDelegate.swift file if your app needs to perform custom handling when users click push notifications. For example, you’d need to perform custom handling if you need to process custom data that you attached to the push notification payload. class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // ... // Set the AppDelegate as a delegate for push notification events: UNUserNotificationCenter.current().delegate = self return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Function called when a push notification is clicked or swiped away. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // If you need to know if a push was clicked: let pushWasClicked = response.actionIdentifier == UNNotificationDefaultActionIdentifier // Process custom data attached to payload, if you need: let pushPayload = response.notification.request.content.userInfo // Important: When you're done processing the push notification, you're required to call the completionHandler. // Even if you do not process a push, you're still required to call the completionHandler() in this function. completionHandler() } } Deep links handling when push is clicked Our SDK handles deep links automatically for notifications originated from Customer.io. You can find more details at Deep Links. Capture push metrics with UserNotifications If you’re using a version of iOS that supports UserNotifications, you can track metrics using our UNNotificationContent helper. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // This 1 line of code might be all that you need! MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If you use `UserNotifications` for more then Customer.io push notifications, you can check // if the SDK handled the push for you or not. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) if !handled { // Notification was *not* displayed by Customer.io. Handle the notification yourself. } } Extract delivery ID and token If you’re not using a version of iOS that supports UserNotifications, you should send the push metric manually by extracting the CIO-Delivery-ID and CIO-Delivery-Token parameters directly to track push metrics. guard let deliveryID: String = notificationContent.userInfo["CIO-Delivery-ID"] as? String, let deviceToken: String = notificationContent.userInfo["CIO-Delivery-Token"] as? String else { // Not a push notification delivered by Customer.io return } MessagingPush.shared.trackMetric(deliveryID: deliveryID, event: .delivered, deviceToken: deviceToken) Disable automatic push tracking Automatic push metric recording is enabled by default when you install the SDK. You can disable this behavior in the SDK’s configuration. MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoTrackPushEvents(false) // Disable automatic push tracking .build() ) --- ## Sound in push notifications URL: https://docs.customer.io/integrations/sdk/ios/push/sound-in-push/ When you send a notification, it can play an alert on your audience's device. The sound file must be bundled in your app, and you'll specify which sound you want to play in your notification payload. When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. --- ## Provisional Push URL: https://docs.customer.io/integrations/sdk/ios/push/provisional/ Provisional push notifications are a way to send notifications on a trial basis. People can then evaluate the notifications and decide whether to authorize them. Provisional push support is available for iOS 15 and above. Customer.io doesn’t have a specific “provisional push” feature. This is just something that iOS supports out of the box with iOS 15+. See Apple’s provisional authorization documentation to learn more. When you request authorization for push notifications, you can include the .provisional option in the requestAuthorization call. class NotificationUtil: NotificationUtility { func showPromptForPushPermission(completionHandler: @escaping (Bool) -> Void) { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .provisional], completionHandler: { status, _ in completionHandler(status) }) } } --- ## Push service certificates URL: https://docs.customer.io/integrations/sdk/ios/push/push-certificates/ Before you can send push notifications, you'll need to add credentials for the push service(s) you use to Customer.io. You can send push notifications to iOS devices using either Apple’s Push Notification service (APNs) or Google’s Firebase Cloud Messaging (FCM) service. To authorize your Customer.io workspace to send notifications through APNs, you’ll need to upload your APNs .p8 certificate and provide your credentials or upload your .JSON to iOS devices over Apple’s Push Notification service, you’ll need to upload your APNs .p8 certificate and enter your Apple Developer Key ID, Team ID, and Bundle ID. Upload your push certificate If you don’t already have your .p8 certificate for APNs or your .JSON file for FCM, you’ll need to get one before you can finish this process and send push notifications. APNs APNs In Customer.io, go to your workspace’s Settings > Workspace Settings and click Settings next to Push. Click Enable under iOS, and select the Apple Push Notification service (APNs) option. Click Choose file… and upload your .p8 certificate. Enter your Key ID, Team ID, and Bundle ID. You can find these in your Apple Developer Account. (Optional) Enable the Send all push notifications to sandbox option. Your iOS certificate may have both sandbox and production environments; this option sends push notifications to both environments.  We recommend creating a separate workspace for your sandbox environment. Click Save Changes FCM FCM In Customer.io, go to Settings > Workspace Settings and click Settings next to Push. For iOS, click Enable, and select the Firebase Cloud Messaging (FCM) option. Get your .p8 file for APNs Log into your Apple Developer account and go to Certificates, Identifiers & Profiles > Keys. Click the blue button to create a new key. Click Apple Push Notifications service (APNs) and enter a name for the key. Click Continue and then Register to create the key. Download your keys and put it somewhere you’ll remember. You can only download your key once! Get your .JSON file for FCM Before you can get a push certificate for Firebase Cloud Messaging, make sure that the FCM API is enabled for your project. You can check that here. Log into the Firebase Console for your project. Click in the sidebar and go to Project settings. Go to Service Accounts and click Generate New Private Key. Confirm your choice and download the credential file. --- ## Test your push implementation URL: https://docs.customer.io/integrations/sdk/ios/push/test-push/ Before you send push notifications, you should test your implementation to make sure it works as expected. After you set up push notifications, you should send some test messages. You can send messages through the Customer.io push composer. If your app is set up to send more than the standard title, body, image, and link, you’ll need to send a custom payload. The payloads below represent what your app can expect to receive from Customer.io. If you use a custom payload, you’ll need to use the format(s) below to make sure that the SDK receives your message properly. When testing, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. APNS payload APNS payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. FCM payload FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. --- ## Set up in-app messaging URL: https://docs.customer.io/integrations/sdk/ios/in-app/set-up-in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. How it works An in-app message is a message that people see within the app; people won’t see your message until they open your app. To set up in app messaging, install and initialize the CioDataPipelines and CioMessagingInApp packages. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which pages a person is visits, so we can display in-app messages on the correct screens (or “pages”) in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging Use Swift Package Manager to install the CioMessagingInApp package. See Getting Started for installation instructions. Initializing your app with the CioMessagingInApp package sets up your app to receive in-app messages. import CioDataPipelines import CioInternalCommon import CioMessagingInApp import UIKit @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: YOUR_CDP_API_KEY) .autoTrackDeviceAttributes(true) .autoTrackUIKitScreenViews() .migrationSiteId(siteId) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) return true } } Anonymous messages As of version 3.14, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. In-app configuration options You must pass both of the following configuration options when you initialize the MessagingInApp package. Option Type Default Description siteId string The Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from. region Region.US or Region.EU The region your Customer.io account resides in. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/ios/in-app/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages Add an inline view to your app’s UI You’ll add a UI element to your app’s UI code to support inline messages using SwiftUI, Storyboard, Interface Builder, or UIKit via code.  We’ve set up examples in our UIKit and SwiftUI sample apps that might help if you want to see a real-world implementation of this feature. Storyboard Storyboard Open your storyboard file and drag a UIView onto your view controller. Set the class of the UIView to InlineMessageUIView in the Identity Inspector. Setup layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See view layout for more information. In your ViewController, set the elementId for the InlineMessageUIView. This is the ID you’ll use in the Customer.io UI when you want to send an in-app message. import CioMessagingInApp @IBOutlet weak var inlineInAppView: InlineMessageUIView! override func viewDidLoad() { super.viewDidLoad() // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. inlineInAppView.elementId = "<element-id-here>" } UIKit Swift UIKit Swift Create an instance of InlineMessageUIView and add it to your view hierarchy. import CioMessagingInApp override func viewDidLoad() { super.viewDidLoad() // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. let inlineMessage = InlineMessageUIView(elementId: "<element-id-here>") } Setup layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See view layout for more information. SwiftUI SwiftUI Create an instance of InlineMessage and add it to your view hierarchy. You shouldn’t set a height on your View to avoid breaking functionality. InlineMessage automatically updates the height for you when messages load and are interacted with. import SwiftUI import CioMessagingInApp struct MyScreen: View { var body: some View { // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. InlineMessage(elementId: "<element-id-here>") // Note: Avoid setting a hard-coded height on the View to avoid breaking functionality. // The InlineMessage dynamically changes it's height when messages are loaded and interacted with. // .frame() } } UIKit: Setup layout constraints for your message InlineMessageUIView uses AutoLayout and modifies its own height. But otherwise, you’ll need to set up layout constraints for the view including the width and the leading, top, trailing, and bottom constraints for the view. You can create a height constraint, but it won’t matter because we modify it at runtime. You may still want to set a height if you use Storyboard because XCode will throw warnings and errors if you don’t set a height. 2. Build and send your message! When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow these steps to implement custom action buttons for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events For inline in-app messages, you have 2 options for listening to these action click events. Register a delegate with inline View: UIKit UIKit import CioMessagingInApp class MyViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Given you have an inline View in your ViewController, set a delegate to listen for action events. // Note: The inline View holds a `weak` reference to the `onActionDelegate`. inlineInAppView.onActionDelegate = self } } extension MyViewController: InlineMessageUIViewDelegate { func onActionClick(message: InAppMessage, actionValue: String, actionName: String) { // Perform some logic when people tap an action button. // Example code handling button tap: switch(actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionValue) // You can add more cases here for other actions default: // Handle unknown actions or do nothing print("Unknown action: \(actionName)") } } } SwiftUI SwiftUI import CioMessagingInApp import SwiftUI struct MyScreen: View { var body: some View { InlineMessage(elementId: "<element-id-here>", onActionClick: { message, actionValue, actionName in // Perform some logic when custom action button pressed. // Example code handling button press: switch(actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action for enabling auto-renew enableAutoRenew(actionValue) // You can add more cases here for other actions default: // Handle unknown actions or do nothing print("Unknown action: \(actionName)") } }) } } Register a global SDK event listener. When you register an event listener with the in-app SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic if you want. Handle responses to messages (event listeners) Similar to modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionDelegate set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! Known limitations We’re actively developing this feature. But, in the meantime, you should be aware of the following limitations: InlineMessageUIView does not work as expected inside a UITableView: If you have a scrolling list, InlineMessageUIView works great in UIStackView and UIScrollView. --- ## Page rules URL: https://docs.customer.io/integrations/sdk/ios/in-app/target-in-app-messages/ Sending people in-app messages often depends on the screens they visit in your app. You can set page rules when you create in-app messages. These rules determine the pages that your audience must visit in your app to see each message. Before you can take advantage of page rules, you need to: Track screens in your app. You can add $0.autoTrackScreenViews = true to your CustomerIO.config to automatically track screens or you can track screens manually. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. The SDK automatically uses the class name of UIViewController, minus ViewController, as the name of each page. For example, if you wanted to display an in-app message on a class called EditProfileViewController, you would enter EditProfile as your page rule.  Make sure your screens use the same names across your apps If you have a screen called DashboardActivity in Android, and DashboardViewController in iOS, we’ll recognize Dashboard as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/ios/in-app/in-app-event-listeners/ When people receive an in-app message, you'll listen for events to handle the message's lifecycle—like dismissing the event or taking a custom action. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { CustomerIO.shared.track(name: "in-app action", properties: [ "delivery-id": message.deliveryId, "message-id": message.messageId, "action-value": actionValue, "action-name": actionName ]) } Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. MessagingInApp.shared.dismissMessage() --- ## Notification inbox URL: https://docs.customer.io/integrations/sdk/ios/in-app/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io delivers inbox messages as JSON payloads, not fully-rendered messages. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message. Get the inbox instance You’ll access inbox functionality through the inbox property on the in-app messaging module. let inbox = MessagingInApp.shared.inbox Inbox methods The inbox instance provides several methods to manage messages. Method Description getMessages() Get all messages from the inbox. Returns an async array of messages. getMessages(topic:) Get messages filtered by topic. Returns an async array of messages. messages() AsyncStream for all messages with real-time updates. Automatically cleans up when task is cancelled. messages(topic:) AsyncStream for messages filtered by topic. Automatically cleans up when task is cancelled. addChangeListener(_:) Add a listener to be notified when messages change. Requires manual cleanup. addChangeListener(_:topic:) Add a listener for messages filtered by topic. Requires manual cleanup. removeChangeListener(_:) Remove a previously added change listener. markMessageOpened(message:) Mark a message as opened. markMessageUnopened(message:) Mark a message as unopened. markMessageDeleted(message:) Mark a message as deleted. trackMessageClicked(message:) Track a click on the message without an action name. trackMessageClicked(message:actionName:) Track a click on the message with an action name. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Get messages // Get all messages let messages = await inbox.getMessages() // Get messages filtered by topic let promoMessages = await inbox.getMessages(topic: "promo") Listen for message updates The SDK provides two approaches for listening to message updates: AsyncStream (modern) and Listener pattern (classic). AsyncStream (modern approach) AsyncStream automatically cleans up when the task is cancelled, making it ideal for SwiftUI views or structured concurrency. // Stream all messages Task { for await messages in inbox.messages() { // Update your UI with the messages updateInboxUI(messages) } } // Stream messages filtered by topic Task { for await messages in inbox.messages(topic: "promo") { // Update your UI with filtered messages updatePromotionsUI(messages) } } Listener pattern (classic approach) The listener pattern requires manual cleanup but works well with UIKit view controllers. // Your class must conform to NotificationInboxChangeListener class InboxViewController: UIViewController, NotificationInboxChangeListener { override func viewDidLoad() { super.viewDidLoad() // Add listener (must be called on MainActor) Task { @MainActor in inbox.addChangeListener(self) } } // Implement the protocol method func onMessagesChanged(messages: [InboxMessage]) { // Update your UI with the messages updateInboxUI(messages) } deinit { // Remove listener when view controller is deallocated inbox.removeChangeListener(self) } } // Add listener for specific topic Task { @MainActor in inbox.addChangeListener(self, topic: "promo") } Mark messages as opened or unopened // Mark a message as opened inbox.markMessageOpened(message: message) // Mark a message as unopened inbox.markMessageUnopened(message: message) Track message clicks // Track a click without an action name inbox.trackMessageClicked(message: message) // Track a click with an action name inbox.trackMessageClicked(message: message, actionName: "view_details") Delete messages // Mark a message as deleted inbox.markMessageDeleted(message: message) Working with message properties You can access message properties to display custom content in your inbox: // Access message properties let title = message.properties["title"] as? String let body = message.properties["body"] as? String let link = message.properties["link"] as? String let imageUrl = message.properties["image"] as? String // Handle message action when user taps func handleMessageTap(_ message: InboxMessage) { // Mark as opened inbox.markMessageOpened(message: message) // Track click inbox.trackMessageClicked(message: message) // Open link if available if let link = message.properties["link"] as? String, let url = URL(string: link) { UIApplication.shared.open(url) } } --- ## 4.x -> 4.4.0 URL: https://docs.customer.io/integrations/sdk/ios/whats-new/4.4.0-upgrade/ Version 4.4.0 adds App Groups support for more reliable push delivery metric tracking. This update is additive—existing integrations work without modification. What changed? Version 4.4.0 adds support for App Groups, which improves the reliability of push delivery metric tracking. This update is additive—your existing integration continues to work without changes. However, to take advantage of App Groups, you’ll need to update your Xcode project configuration and regenerate provisioning profiles if you use manual signing. Why App Groups? When you send a push notification, the SDK tracks delivery metrics in your Notification Service Extension (NSE). However, iOS can kill the NSE before the tracking request completes, which means some delivery data may never reach Customer.io. App Groups provide shared storage between your main app and the NSE, so the SDK can save metrics and recover them on the next app launch. Upgrade process Update to version 4.4.0 or later of the Customer.io iOS SDK. Follow the App Groups setup instructions to configure your Xcode project and pass the .appGroupId() to the SDK. No other code changes are required. --- ## 3.x -> 4.0.0 URL: https://docs.customer.io/integrations/sdk/ios/whats-new/4.0.0-upgrade/ This page details breaking changes from version 3.x to 4.0.0 of the SDK, specifically for users utilizing Firebase Cloud Messaging (FCM) for push notifications. What changed? Version 4.0.0 introduces a breaking change for users who utilize Firebase Cloud Messaging (FCM) for push notifications. The change improves the FCM integration by consolidating Firebase dependencies into a dedicated wrapper package. FCM Integration Changes New dependency required: CioFirebaseWrapper package is now required for FCM push notifications Import statement updated: Add import CioFirebaseWrapper wherever you use import CioMessagingPushFCM Initialize method unchanged: Continue using MessagingPushFCM.initialize() but it now comes from the new package Note: This change only affects users utilizing FCM for push notifications. If you use Apple Push Notification Service (APNs), no changes are required. Upgrade process 1. Update dependencies Update your dependency management configuration to include the new CioFirebaseWrapper package. Swift Package Manager (SPM) Swift Package Manager (SPM) Add the CioFirebaseWrapper package to your project in Xcode: In Xcode, go to File > Add Package Dependencies Add https://github.com/customerio/customerio-ios-fcm.git if not already added Select the CioFirebaseWrapper package in addition to your existing packages CocoaPods CocoaPods Add the new pod to your Podfile: # Add this new dependency for FCM push notifications pod 'CustomerIO/CioFirebaseWrapper' # Keep your existing dependencies pod 'CustomerIO/DataPipelines' pod 'CustomerIO/MessagingPushFCM' pod 'CustomerIO/MessagingInApp' Then run: pod install 2. Update import statements Add CioFirebaseWrapper import statement in all files where you import CioMessagingPushFCM. Don’t change your other import statements: import CioDataPipelines import CioFirebaseWrapper import CioMessagingPushFCM import FirebaseCore import FirebaseMessaging import Foundation import UIKit 3. Verify initialization code Your initialization code should remain the same. The MessagingPushFCM.initialize() method continues to work exactly as before, but now it comes from the CioFirebaseWrapper package: // This initialization code remains unchanged MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) .build() ) Complete example Here’s a complete example of the updated FCM setup: import CioDataPipelines import CioFirebaseWrapper import CioMessagingPushFCM import FirebaseCore import FirebaseMessaging import Foundation import UIKit @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { // Initialize Firebase FirebaseApp.configure() let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features - method remains the same MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) .build() ) return true } } Troubleshooting Build errors after upgrade If you encounter build errors after upgrading: Clean your build: In Xcode, go to Product > Clean Build Folder Remove old packages: If using SPM, remove the old packages and re-add them Update CocoaPods: If using CocoaPods, run pod deintegrate followed by pod install Import errors If you see errors related to missing imports: Verify that CioFirebaseWrapper is properly added to your project dependencies Check that the file initializing FCM functionality has both import CioFirebaseWrapper and import CioMessagingPushFCM --- ## 3.x -> 3.13.0 URL: https://docs.customer.io/integrations/sdk/ios/whats-new/3.13.0-upgrade/ This page introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. What changed? The changes are mainly to align our SDK APIs across different platforms. No functional changes to be expected. Upgrade process Attributes profileAttributes property is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s profile attributes Setter is replaced with setProfileAttributes(attributes: [String: Any]) deviceAttributes property is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s device attributes Setter is replaced with setDeviceAttributes(attributes: [String: Any]) Tracking Identifying a user These variants of identify are deprecated: identify<T: Codable>(traits: T) where traits are a generic type identify<RequestBody: Codable>(userId: String, traits: RequestBody?) You should use this instead: identify(userId: String, traits: [String: Any]?) setProfileAttributes(attributes: [String: Any]) can now be used to track anonymous profile attributes Tracking an event These variants of track are deprecated: track<RequestBody: Codable>(name: String, properties: RequestBody?) where traits are a generic type You should use this instead: track(name: String, properties: [String: Any]?) Screen tracking These variants of screen are deprecated: screen<RequestBody: Codable>(title: String, properties: RequestBody?) where traits are a generic type You should use this instead: screen(title: String, properties: [String: Any]? --- ## 3.x -> 3.9.0 URL: https://docs.customer.io/integrations/sdk/ios/whats-new/3.9.0-upgrade/ This page introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 3.9.0 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your app depending on whether you send push notifications with APN or FCM and whether you use UIKit or SwiftUI. Update with APNs UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import UIKit import CioMessagingPushAPN @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import UIKit import CioMessagingPushAPN @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import SwiftUI import CioMessagingPushAPN @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import SwiftUI import CioMessagingPushAPN @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } Update with FCM UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import UIKit import CioMessagingPushFCM import Firebase import FirebaseMessaging @main class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } After (3.9.0) After (3.9.0) import UIKit import CioMessagingPushFCM import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } Important Notes CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError didReceiveRemoteNotification userNotificationCenter(_:willPresent:withCompletionHandler:) userNotificationCenter(_:didReceive:withCompletionHandler:) All other push-related delegate methods The @main attribute must be on the wrapper class, not your AppDelegate. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushAPN.initialize() or MessagingPushFCM.initialize() --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/ios/whats-new/3.x-upgrade/ This page details breaking changes from the previous major version of the SDK, so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK, the in-app messaging module, and the push module. The in-app and push modules are now required. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select iOS. Enter a Name for your integration, like “My iOS App”. We’ll present you with a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your iOS app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Import CioDataPipelines instead of CioTracking We’ve replaced the CioTracking package with CioDataPipelines. You’ll need to update your import statements to reflect this change. If you see errors like Missing required module ‘CioTracking’, you can remove the package in XCode under Frameworks and Libraries. // replace import CioTracking with: import CioDataPipelines 3. Update your initialize calls You’ll initialize the new version of the SDK and its packages with SDKConfigBuilder objects instead of a CustomerIOConfig. A few of the configuration options changed. In particular, cdpApiKey replaces apiKey: this is a new key that you got from Step 1 migrationSiteId replaces siteId: this is the same key you used in the previous version of the SDK. You need to include this property to send remaining traffic when people update your app. autoTrackUIKitScreenViews replaces autoTrackScreenViews: functionality is unchanged; we simply renamed the option to reflect support for UIKit and not SwiftUI. If you’re in our EU region, make sure that you uncomment the .region(.EU) line in the sample below. Your config must include this property to send data to our EU data center. APNS APNS import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. let cdpApiKey = YOUR_CDP_API_KEY let siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // uncomment the line below if your account is in the EU region // .region(.EU) .autoTrackDeviceAttributes(true) .migrationSiteId(siteId) //replaces autoTrackScreenViews .autoTrackUIKitScreenViews() CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) UNUserNotificationCenter.current().delegate = self return true } FCM FCM import CioDataPipelines import CioMessagingInApp import CioMessagingPushFCM import FirebaseCore import FirebaseMessaging import Foundation import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // To set up FCM push: https://firebase.google.com/docs/cloud-messaging/ios/client // FCM provides a device token to the app that // you send to the Customer.io SDK. // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_JOURNEYS_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // uncomment this line below if your account is in the EU region // .region(.EU) .migrationSiteId(siteId) .autoTrackDeviceAttributes(true) //replaces autoTrackScreenViews .autoTrackUIKitScreenViews() CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) // Manually get FCM device token. Then, we will forward to the Customer.io SDK. Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken } } 4. Update your NotificationServiceExtension You need to initialize the push module with your integration’s CDP API Key. Update your NotificationServiceExtension (using the appropriate push package, APN or FCM). If you previously initialized the SDK using CustomerIO.initialize, you can remove that now. You only need to initialize the push package. // use MessagingPushFCM if FCM is your push service MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder( cdpApiKey: "YOUR_CDP_API_KEY" ) // Optional: set your Customer.io account region (.US or .EU). Default: US .region(.US) .build() ) 5. Update your identify, track, and screen calls Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track: data becomes properties screen: name becomes title, and data becomes properties New (3.x) New (3.x) // CioDataPipelines replaces CioTracking import CioDataPipelines //identify: identifier becomes userId, body becomes traits CustomerIO.shared.identify(userId: "USER_ID", traits: ["age": 30]) // track: data becomes properties CustomerIO.shared.track(name: "Purchase", properties: ["product": "shirt"]) // screen: name becomes title, data becomes properties CustomerIO.shared.track(title: "Cart", properties: ["source": "link"]) Old (2.x) Old (2.x) import CioTracking // identify CustomerIO.shared.identify(identifier: "USER_ID", body: ["age": 30]) {} // track CustomerIO.shared.track(name: "Purchase", data: ["product": "shirt"]) // screen CustomerIO.shared.track(name: "Cart", data: ["source": "link"]) Configuration Changes As a part of this release, we’ve changed a few configuration options. The MessagingInApp and MessagingPush modules also now take their own configuration options. DataPipelines configuration options For the base SDK, you’ll use SDKConfigBuilder to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data into Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. autoTrackUIKitScreenViews boolean false Replaces autoTrackScreenViews; functionality is unchanged. We simply renamed the option to reflect support for UIKit and not SwiftUI. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). MessagingPush configuration options You need to initialize the push package in both your AppDelegate and NotificationServiceExtension files. In your AppDelegate, you don’t need to pass options. You can simply pass MessagingPushAPN.initialize(). But, in your NotificationServiceExtension, you’ll need to pass the cdpApiKey to initialize the push package. MessagingPushAPN.initializeForExtension(withConfig: MessagingPushConfigBuilder(cdpApiKey: "CDP_API_KEY").build()) Option Type Default Description region .US or .EU .US The region your Customer.io account resides in US or EU. autoFetchDeviceToken boolean true When true, the package automatically fetches the device token for push notifications. autoTrackPushEvents boolean true Automatically track opened and delivered metrics based on push notifications. showPushAppInForeground boolean true Show push notifications when the app is in the foreground. Used only if customer’s AppDelegate doesn’t implement UNUserNotificationCenterDelegate. MessagingInApp configuration options When you initialize the CioMessagingInApp package, you must pass both of these configuration options. Option Type Default Description siteId string The Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from. Region .US or .EU .US The region your Customer.io account resides in—US or EU. --- ## 1x -> 2.x URL: https://docs.customer.io/integrations/sdk/ios/whats-new/2.x-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 1.x to 2.x Singleton API is now enforced In version 1.x of the Customer.io iOS SDK, could use the SDK in 2 ways: Singleton API: CustomerIO.initialize(...) // initialize the singleton SDK instance Customer.shared.track(...) // use the singleton SDK instance Non-singleton API: let cio = CustomerIO(...) // initialize the non-singleton SDK instance cio.track(...) // use the non-singleton SDK instance In version 2.x, we removed the non-singleton API. To successfully migrate, you need to replace any code using a non-singleton with the singleton instance: // Replace the non-singleton instance: cio.track(...) messagingPush.application(...) // With `CustomerIO.shared` or `MessagingPush.shared`: CustomerIO.shared.track(...) MessagingPush.shared.application(...) If your app uses a technique like dependency injection, you can keep your code base as-is and simply replace code where you create new instances of the SDK: // For example, if you have code that accepts a CustomerIO dependency in the constructor (to easily allow mocking the Customer.io SDK): class Repository { let customerIO: CustomerIO init(customerIO: CustomerIO) { self.customerIO = customerIO } func acceptFriendRequest() { ... self.customerIO.track(...) ... } } // You can keep your Repository as-is, but you need to change where you create instances from: let repository = Repository(customerIO: CustomerIO(...)) repository.acceptFriendRequest() // To: let repository = Repository(customerIO: CustomerIO.shared) // Don't forget to initialize the SDK 😉 repository.acceptFriendRequest() Configuration of the SDK happens during initialization In version 1.x of the Customer.io iOS SDK, you configured the SDK through a .config function: CustomerIO.config { $0.autoTrackScreenViews = true } In version 2.x of the Customer.io iOS SDK, we moved the .config function into CustomerIO.initialize. You’ll need to move your configuration into the SDK initialization process to migrate: CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. SDK initialization: required parameters In version 1.x of the Customer.io SDK, the function CustomerIO.initialize contained optional parameters. We had to remove those and make all parameters required. To migrate to 2.x of the SDK, fill in the rest of the parameters in your initialize function: // v1.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // v2.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) {} // Optionally, if you want to configure settings of the SDK, do so in initialization. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. Rich push initialization If you have followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Because of the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your host app and in your Notification Service. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Cocoapods users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. This means that you need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. If you installed the Customer.io SDK using Swift Package Manager, this change does not effect you. We fixed a bug with custom attributes that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in v1 that converted keys in your custom data to snake_case. This bug is fixed in v2 of the iOS SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect our Android SDK.. // If you passed in custom attributes using camelCase keys: data = ["firstName": "Dana"] // The SDK v1 may have converted this data into: data = ["first_name": "Dana"] // Or, if you used a different format that was not snake_case: data = ["FIRSTNAME": "Dana"] // The SDK v1 may have converted this data into: data = ["f_irstname": "Dana"] You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Anywhere you call the Customer.io SDK and provide custom attributes like this: CustomerIO.shared.identify("dana@example.com", data: ["firstName": "Dana"]) // Consider sending duplicate data with snake_case CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. ]) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // We can remove the snake_case attribute and go back to just camelCase! ]) --- ## Changelog URL: https://docs.customer.io/integrations/sdk/ios/whats-new/changelog/ Check out release history for stable releases of iOS SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/ios/3.x/quick-start-guide/ This guide contains the minimum steps you'll need to follow to install and start using the Customer.io SDK in your iOS app.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview Our SDK lets you build native mobile apps for iOS. When you’re done, you’ll be able to identify people, track their activity, and send both push notifications and in-app messages. Install and initialize the SDK Identify and Track Add push notification support Add in-app messaging support 1. Install and initialize the SDK Add a new iOS connection in Customer.io to get your CDP API key. See Get your CDP API key for details. You’ll use this API key to initialize the SDK. Install the SDK. We typically recommend that you install the SDK using Swift Package Manager (SPM). But if your app uses CocoaPods, you can install our pods. Swift Package Manager (SPM) Swift Package Manager (SPM) Follow Apple’s instructions to add https://github.com/customerio/customerio-ios.git as a dependency to your project in Xcode and select all the packages you need. Set the Dependency Rule to Up to Next Major Version. While we encourage you to keep your app up to date with the latest SDK, major versions can include breaking changes or new features that require your attention. CocoaPods CocoaPods Add the pods to your Podfile: pod 'CustomerIO/DataPipelines' # Required pod 'CustomerIO/MessagingPushAPN' # Optional, for APNs pod 'CustomerIO/MessagingPushFCM' # Optional, for FCM pod 'CustomerIO/MessagingInApp' # Optional, for in-app messaging Import the appropriate packages. We’re importing all our packages, but you can modify this list if you don’t intend to use all Customer.io features. If you send notifications using Firebase Cloud Messaging (FCM), import the MessagingPushFCM package rather than MessagingPushAPN. UIKit with Swift UIKit with Swift import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN import UIKit UIKit with Objective-C UIKit with Objective-C @import UIKit; @import CioDataPipelines; @import CioMessagingInApp; @import CioMessagingPushAPN; SwiftUI SwiftUI import SwiftUI import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN Initialize the SDK. You’ll usually do this in the AppDelegate application(_ application: didFinishLaunchingWithOptions) function, and you’ll use the CioAppDelegateWrapper to handle push notifications and device token registration and initialize the SDK. You also need the CDP API Key that you obtained when you added your iOS integration to your workspace. UIKit with Swift UIKit with Swift @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } ``` UIKit with Objective-C UIKit with Objective-C @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @end // AppDelegate.m #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize the Customer.io SDK NSString *cdpApiKey = @"YOUR_CDP_API_KEY"; NSString *siteId = @"YOUR_SITE_ID"; CioSDKConfigBuilder *config = [[CioSDKConfigBuilder alloc] initWithCdpApiKey:cdpApiKey]; // If your account is in the EU region, uncomment the next line // [config region:CioRegionEU]; [config migrationSiteId:siteId]; // only required if you used version 2.x or earlier [config autoTrackUIKitScreenViews]; // Set auto tracking of UIKit screen views [config logLevel:CioLogLevelDebug]; // Add this to troubleshoot issues - disable debug in production [CustomerIO initializeWithConfig:[config build]]; return YES; } @end SwiftUI SwiftUI @main struct MainApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) private var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } 2. Identify and Track Users Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. Swift Swift // Identify a user with just a user ID CustomerIO.shared.identify(userId: "user@example.com") // Identify a user with additional attributes CustomerIO.shared.identify( userId: "user@example.com", traits: [ "email": "user@example.com", "first_name": "John", "last_name": "Doe", "plan_name": "Premium", "device_type": "iOS" ] ) // clear the user identity when they log out CustomerIO.shared.clearIdentify() Objective-C Objective-C // Identify a user with just a user ID [CustomerIO identifyUserWithId:@"user@example.com"]; // Identify a user with additional attributes [CustomerIO identifyUserWithId:@"user@example.com" traits:@{ @"email": @"user@example.com", @"first_name": @"John", @"last_name": @"Doe", @"plan_name": @"Premium", @"device_type": @"iOS" }]; // clear the user identity when they log out [CustomerIO clearIdentify]; Track a custom activity using the CustomerIO.track method. Events help you trigger personalized campaigns and record user behavior in your app. Swift Swift // Track a simple event without properties CustomerIO.shared.track(name: "checkout_started") // Track an event with properties CustomerIO.shared.track( name: "product_viewed", properties: [ "product_id": "SKU-123", "product_name": "Premium Widget", "price": 99.99, "currency": "USD", "category": "Electronics" ] ) Objective-C Objective-C // Track a simple event without properties [CustomerIO trackEventWithName:@"checkout_started"]; // Track an event with properties [CustomerIO trackEventWithName:@"product_viewed" properties:@{ @"product_id": @"SKU-123", @"product_name": @"Premium Widget", @"price": @99.99, @"currency": @"USD", @"category": @"Electronics" }]; 3. Add push notification support In your AppDelegate, after your CustomerIO.initialize call, add the MessagingPushAPN.initialize call. Our push package has its own settings so you can configure push behaviors. These instructions are for Apple’s Push Notification service (APNs). If you send push notifications using Firebase Cloud Messaging (FCM), see our push notification instructions. Swift Swift // Initialize the push package MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) // Request permission to show notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in // Process user response here } Objective-C Objective-C // Initialize the push package [MessagingPushAPN initializeWithConfig:[[MessagingPushConfigBuilder new] autoFetchDeviceToken:YES // Automatically fetch device token and upload to CustomerIO autoTrackPushEvents:YES // Automatically track push metrics showPushAppInForeground:YES // Enable Notifications in the foreground build]]; // Request permission to show notifications [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) { // Process user response here }]; 4. Add in-app messaging support In your AppDelegate, after your CustomerIO.initialize call, add the MessagingInApp.initialize call. Within the call, you’ll need a Site ID, which you can get from your workspace settings. This tells the SDK which workspace your in-app messages come from. Swift Swift // Initialize the in-app package // Change region to .EU if you're in our European Union data center! MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) Objective-C Objective-C // Initialize the in-app package [MessagingInApp initializeWithConfig:[[MessagingInAppConfigBuilder alloc] initWithSiteId:siteId region:CioRegionUS]]; --- ## How it works URL: https://docs.customer.io/integrations/sdk/ios/3.x/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our iOS SDK is a data inAn integration that feeds data into Customer.io. in Customer.io. You can route data from your app to both Customer.io and other destinations. This makes it easier to use your app as a part of your larger data stack without relying on extra services or code. When you set up your integration in Customer.io, you’ll determine where you want to route your data to—your workspace and destinations outside of Customer.io. Minimum support requirements To support the Customer.io SDK, you must: Set iOS 13 or later as your minimum deployment target in XCode Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Objective-C support Our SDK is written in Swift and tested in a Swift environment. Our iOS SDK may work in Objective-C-based projects, but we haven’t tested it that way. If you use our SDK in an Objective-C project and run into trouble, please let us know. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/ios/3.x/getting-started/auth/ To use the SDK, you'll need to get two kinds of keys: A CDP *API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. To get your SDK keys and send data to the right places, you’ll need to set up your app as a data inAn integration that feeds data into Customer.io. integration in Customer.io, and route it to your workspace. The SDK lets you route data to any number of destinations, but you must connect it to your workspace destination to send data, like the people you identify, the events you track, and so on, to Customer.io. If you haven’t already set up your app as an integration in Customer.io, do that first. API Keys you’ll need API Key: This key, shown in code samples as cdpApiKey, lets you send data to Customer.io. You’ll need it to initialize the SDK. You’ll get this key when you set up your integration in Customer.io. Site ID: This key tells the SDK which workspace your messages come from. You’ll use it to initialize the CioMessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your mobile app’s integration card in Customer.io. If you haven’t already set up your Android integration in Customer.io, you’ll need to do that first. Go to Integrations. Select your iOS integration in the Overview tab. If you don’t see an iOS integration, you’ll need to set it up. Go to Settings and find your API Key. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should keep the siteId that you used in previous versions as the migrationSiteId in your config. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { ... let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId(YOUR_SITE_ID) .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) }  You’re not done yet You still need your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials to initialize the CioMessagingInApp package and to support people updating your app from a previous version of Customer.io SDK. See Get your Site ID below. Set up a new integration If you don’t already have a write key, you’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select the iOS integration. Enter a Name for your integration, like “My iOS App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy; you’ll use it in your initialization call. Click Complete Setup to finish setting up your integration. Now the integrations Overview page shows that your iOS app is connected to your workspace. You can also connect your iOS app to any number of destinations if you want to send your mobile data to additional services—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your Site ID to initialize the CioMessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version, my can also set your Site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the CioMessagingInApp package. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { ... let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId(siteId) .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/ios/3.x/getting-started/packages-options/ The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use. SDK packages To minimize our SDK’s impact on your app’s size, we’ve split the SDK into packages. You can limit your install to the packages that you need for your project. You must install the CioDataPipelines package. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Product Required? Description CioDataPipelines ✅ identify people and track events CioMessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) CioMessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) CioFirebaseWrapper Required for FCM push notifications. Provides Firebase Cloud Messaging integration CioMessagingInApp Receive in-app notifications CioLocation Enrich user profiles with accurate device location Configuration options When you install the SDK via CocoaPods, you can find our packages by replacing the Cio in package names with CustomerIO/—e.g. CustomerIO/DataPipelines. Configuration options You’ll call configuration options before you initialize the SDK with SDKConfigBuilder. When you initialize the SDK, you can pass configuration options. In most cases, you’ll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") // Mandatory for all customers .migrationSiteId("YOUR_SITE_ID") // Mandatory only for migrating customers .autoTrackDeviceAttributes(true) .region(.EU) CustomerIO.initialize(withConfig: config.build()) Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io migrationSiteId string Required if you're updating from 2.x: the credential for previous versions of the SDK. This key is used to send remaining tasks to Customer.io when your audience updates your app. region .EU or .US .US Required if your account is in the EU region. This sets your account region in the format Region.US. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackUIKitScreenViews boolean false For UIKit-based apps: if true, the SDK automatically sends screen events for every screen your audience visits. screenViewUse .all or .inApp all screenView: .all (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. screenView: inApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events logLevel string error Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. visionOS Support The iOS SDK supports VisionOS. We have a handy sample app that demonstrates how to use the SDK with Vision Pro devices, along with a handy readme, in the Apps/VisionOS directory. We’ve only tested the iOS SDK with visionOS using Swift Package Manager. If you use CocoaPods, everything might work, but we can’t guarantee it. Also, for now, we only support Apple’s Push Notification Service (APNS) for visionOS. You won’t be able to send push notifications to Vision Pro devices using Firebase Cloud Messaging (FCM). --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/ios/3.x/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Examine data in and data out traffic Your integrations in Customer.io have Data in and Data out tabs showing you both the calls that come in from your SDK and the data we send out to your destination(s) respectively. You can examine these calls to help you debug issues. If you have a problem, go to Integrations and check: That your iOS integration is connected to your workspace. If you don’t connect your integration to your workspace, you won’t be able to send messages, etc. (Typically, it’s connected to your workspace by default.) Your integration’s Data In tab to make sure that your app is sending the right data. If data isn’t sent to your destination, or it appears incorrect in the destination, go to your outgoing integration’s Data Out tab. Check the calls there to make sure that we’re sending the right data from your SDK to the destination. This includes Customer.io: your workspace is one of the places you’ll send data from your app! Check out Troubleshooting integrations for more help pinpointing issues in your integration. Capture logs Enable debug logging in your app: Everywhere you call CustomerIO.initialize(), enable the debug log level. This includes in the Notification Service Extension that you setup for rich push.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. // During SDK initialization, enable debug logs: CustomerIO.initialize( withConfig: SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .logLevel(.debug) .build() ) Open the Console app (already installed in MacOS). This is a built-in application you can use to view logs produced by the SDK. We recommend that you use Console instead of Xcode to view and capture logs from the SDK because Xcode may not show you all of the logs the SDK generates. In Console, click Action > Include Info messages and Action > Include Debug messages. These settings ensure that you’ll see log messages from the SDK. In Console, on the left, select the iOS device that runs your app with the Customer.io SDK. If you don’t see your device listed, plug in your iOS device into your Mac. Try to use a direct connection via the Apple cable; using a USB hub might prevent the device from showing up. Then, click Start streaming. You will see hundreds or even thousands of logs printed to you. Most of these log messages are not relevant. In the next steps, we’ll filter your log to find relevant messages. In the top right search bar, type “CIO” and press Enter. Click the dropdown and select Category. You will now only see messages sent from the SDK. In the top right, click Save to save this filter. The next time you open Console, just click that saved filter along the top of the screen to see Customer.io SDK logs. Click any of the log entries on the screen (or Edit > Select All), CMD + C, then CMD + P into a text editor on your computer. Save the file as a .txt. Send the file you just saved to our support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Deep links only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. You can learn more about setting up universal links here. You can easily test universal links using your Notes app. Try adding a link to a note and tap it. If it drives you to your app, then you’ve set things up correctly! If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Universal Link opens in browser instead of app If you click on a push notification sent by Customer.io that contains a Universal Link deep link > click on the push notification > app opens for a moment > then the browser opens the URL, this could be a sign that something is wrong with your app’s Universal Link handling. The Customer.io SDK sends a request to your app’s app to give your app an opportunity to handle the Universal Link. If your app does not handle the Universal Link, the SDK will open the link in the browser instead. Let’s walk through some troubleshooting steps to try and fix this behavior so the browser does not open. In our deep links Universal Links guide, we show a function that is required to be added to your app: application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool. Add a print("Universal Links handle code called.") statement or Xcode breakpoint to verify that your code in this function does get called. If you click on a push notification and you do not see this print statement or breakpoint hit, verify that the deep link URL is a valid https URL and you have followed all of the Apple documentation linked in our Universal Links guide. If you do see your print statement or breakpoint hit, then your Universal Link URL is valid and is correctly attached to the push notification for the SDK to understand. Next, verify that your app returns true from the application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool function. If your app returns false, the SDK will open the Universal Link in the browser instead of the app. Lastly, check if there is another SDK interfering with the Customer.io SDK. In some cases, customers have reported instances where Universal Links, despite being correctly configured within your app, may unexpectedly open in a web browser. This can occur due to interactions with third-party SDKs that perform method swizzling inside your app. To address this, consider reviewing the documentation of other SDKs integrated into your app and disabling swizzling as needed. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/ios/3.x/tracking/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You need the **CioDataPipelines** package to identify people. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. If you already registered a device token, identifying a person automatically associates the token with the identified person. You can register for a device token before or after you identify a person. See our Push Documentation for help registering device tokens. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: userId (required): The unique value representing a person—an ID or email address that represents a person in Customer.io (and your downstream destinations). traits (Optional): Contains attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to set for a person. https://customer.io/api/track/#operation/identify import CioDataPipelines CustomerIO.shared.identify(userId: "989388339", traits: ["first_name": firstName]) // `traits` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let traits = ["first_name": "Dana", "last_name": "Green"] CustomerIO.shared.identify(userId: "989388339", traits: traits) // 2. `Encodable` object: struct IdentifyRequestTraits: Encodable { let firstName: String let lastName: String } CustomerIO.shared.identify(userId: "989388339", traits: IdentifyRequestTraits(firstName: "Dana", lastName: "Green")) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with profileAttributes. CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] You only need to pass the attributes that you want to create or modify to profileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.shared.deviceAttributes = ["company" : "cio", "checklist" : "complete"] However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId("YOUR_SITE_ID") .autoTrackDeviceAttributes(false) CustomerIO.initialize(withConfig: config.build()) Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Future calls to the Customer.io SDK are anonymous CustomerIO.shared.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/ios/3.x/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import CioDataPipelines CustomerIO.shared.track(name: "logged_in", properties: ["ip": "127.0.0.1"]) // You don't need to send `data` CustomerIO.shared.track(name: "played_game") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["product": "socks", "price": "23.45"] CustomerIO.shared.track(name: "purchase", properties: data) // 2. A custom `Encodable` type: struct Purchase: Encodable { let product: String let price: Double } CustomerIO.shared.track(name: "purchase", properties: Purchase(product: "socks", price: 23.45))  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.shared.track(name: "User Deleted) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/ios/3.x/tracking/screen-events/ Screen events track the screens people view in your app. In addition to tracking the parts of your app people use, screen tracking is vital to sending in-app messages. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a title representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your they use. Screen events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Automatic screen tracking We can automatically track screens for UIKit-based apps with the .autoTrackUIKitScreenViews configuration option. When you enable automatic screen tracking, the SDK sends a screen call every time a person visits a screen in your app. For apps not using UIKit, like SwiftUI apps, you should manually track screen events. To enable automatic screen tracking, you can pass .autoTrackUIKitScreenViews() to the SDKConfigBuilder or you can customize the behavior for automatic screen tracking if you want to filter the screens the SDK sends events for or add additional properties to the screen events. When you’re done, we recommend you test that automatic screen tracking works for your app. If you encounter issues, you can always send screen events manually. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews() .build() If you don’t use UIKit, or otherwise need to send your own screen events, you can send screen events manually. Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to screenView: inApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews() .screenViewUse(screenView: .all) .build() Screen names When you enable automatic screen views, the SDK automatically names the screen as the class name of the UIViewController, minus ViewController. For example, if you have a class EditProfileViewController in your code base, the SDK will automatically send a screenview event with the screen name EditProfile. Customize automatic screen tracking You can also set additional parameters to customize the behavior of automatic screen tracking. autoScreenViewBody: (optional) Closure that returns a dictionary of properties that you want to send with each automatic screen call. filterAutoScreenViewEvents: (optional) Closure that returns a boolean—true to send an automatic screen call; false to prevent an automatic call. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .autoTrackUIKitScreenViews( autoScreenViewBody: { return [:] }, // optional filterAutoScreenViewEvents: { (viewController: UIViewController) in return true } // optional ) .build() CustomerIO.initialize(withConfig: config) Test automatic screen tracking for your app The automatic screen tracking feature is designed to work with most UIKit-based apps, but it may not work with especially complex navigation structures. If you’ve got a significantly complex UIKit-based navigation structure, you may need to send screen events manually. When you enable automatic screen tracking, you may want to enable info-level logging and walk through your app to make sure screen events come through as expected. If you encounter issues, you probably need to send events manually for problematic screens. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .migrationSiteId("YOUR_SITE_ID") .autoTrackUIKitScreenViews(true) .logLevel("info") CustomerIO.initialize(withConfig: config.build()) Manually track screen events Screen events use the .screen method. Like other event types, you can add a properties object containing additional information about the event or the currently-identified person. import CioDataPipelines // You can send an event with or without `properties`. CustomerIO.shared.screen(title: "DailyBaseballScores") // `properties` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["prev_screen": "homescreen", "seconds_in_app": "120"] CustomerIO.shared.screen(title: "DailyBaseballScores", properties: data) // 2. A custom `Encodable` type: struct Screen: Encodable { let prevScreen: String let secondsInApp: Int } CustomerIO.shared.screen(title: "DailyBaseballScores", properties: Screen(prevScreen: "homescreen", secondsInApp: 120)) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/ios/3.x/tracking/lifecycle-events/ By default, the Customer.io SDK for iOS automatically tracks lifecycle events for your users. These are events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track call. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. import CioDataPipelines CustomerIO.shared.track( name: "Application Crashed", properties: [ "url": "urls://page/in/app" ] ) Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option to the SDK’s config builder. import CioDataPipelines let config = SDKConfigBuilder(cdpApiKey: "CDP_API_KEY") .trackApplicationLifecycleEvents(false) .build() CustomerIO.initialize(withConfig: config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/ios/3.x/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, and we merge anonymous activity with the identified person, the previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity with their identified profile, and their previously-anonymous screen view triggers the campaign you set up for people who visited the product page. You can return a person’s anonymous ID at ay time by calling CustomerIO.shared.anonymousId. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/push-setup/ Our iOS SDK supports push notifications over APN or FCM. This page can help you get started with either service. Before you begin This page explains how to register for push notifications using our SDK. But, before you can send push notifications, you need to add your push service credentials to Customer.io. See our push service certificates to learn more. If you haven’t already, you’ll need to install the push package for the push service you use—APNs or FCM—and enable the Push Notifications capability in XCode. See our quick start guide page for installation instructions. Register for push notifications The instructions in this section set you up to receive simple push notifications with a body and title. After you follow these instructions, you’ll need to do a bit more work to support rich push notifications. The SDK automatically handles push registration and push clicks for you. However, you’ll still need to identify users before you can send them push notifications. 1. Initialize the push service After you initialize the SDK, initialize the push service that you use in your app. Your code changes slightly depending on the push service you use. APNs APNs import CioDataPipelines import CioMessagingPushAPN import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var cdpApiKey = YOUR_CDP_API_KEY var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .autoTrackDeviceAttributes(true) .autoTrackUIKitScreenViews() .migrationSiteId(siteId) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } FCM FCM import CioDataPipelines import CioMessagingPushFCM import FirebaseCore import FirebaseMessaging import Foundation import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // FCM provides a device token to the app that // you send to the Customer.io SDK. // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } 2. Identify your audience Identify the person if you have not already. Even after you add a device token, you can’t use it until you associate it with a person. CustomerIO.shared.identify(userId: "989388339", traits: ["first_name": firstName]) When you identify a person, you should see their device token in your workspace. You can send a simple push notification to test your implementation. Note that when you identify a different person or stop identifying a person, the SDK automatically removes the device token from any previously identified profile. This ensures that a device token is only registered to the currently identified profile in the SDK and prevents you from sending duplicate messages messaging the wrong person. Push configuration options When you initialize your preferred push package (CioMessagingPushAPN or CioMessagingPushFCM), you can pass configuration options determining how the push package functions. Option Type Default Description autoFetchDeviceToken boolean true When true, the package automatically fetches the device token for push notifications. autoTrackPushEvents boolean true Automatically track opened and delivered metrics based on push notifications. showPushAppInForeground boolean true Show push notifications when the app is in the foreground. Used only if customer’s AppDelegate doesn’t implement UNUserNotificationCenterDelegate. --- ## Set up rich push URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/rich-push/ Set up your app to support push notifications with images and deep links. 1. Add a service extension to your project Add a Service App Extension to your project in Xcode. You should now see a new file added to your Xcode project. The file is probably named NotificationService and looks similar to this. import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { } override func serviceExtensionTimeWillExpire() { } } 2. Update the service extension Modify your new NotificationService extension by selecting the push package you want to import and calling the appropriate Customer.io functions. Your code changes if: Customer.io is your only push/rich push provider Customer.io is not your only provider You want to take advantage of push features outside the Customer.io, like action buttons; in this case, you’ll need to set your own completion handler. Customer.io push only Customer.io push only // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushAPN import CioMessagingPushFCM import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // Use `MessagingPushFCM` if you are using FCM as push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Multiple push services Multiple push services // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Due to the behavior of Notification Service Extensions in iOS, you need to // initialize the Push Module in both your host app and in your Notification Service. // The config builder also lets you you to configure the push module. // Use `MessagingPushFCM` if you use FCM as your push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .autoTrackPushEvents(true) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) .build() ) // If you use a service other than Customer.io to send rich push, // you can check if the SDK handled the rich push for you. If it did not, you // know that the push was *not* sent by Customer.io and you can try another way. let handled = MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) if !handled { // Rich push was *not* sent by Customer.io. Handle the rich push in another way. } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Custom completion handler Custom completion handler // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioDataPipelines class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Due to the behavior of Notification Service Extensions in iOS, you need to // initialize the Push Module in both your host app and in your Notification Service. // The config builder also lets you you to configure the push module. // Use `MessagingPushFCM` if you use FCM as your push service provider MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .autoTrackPushEvents(true) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US .region(.US) .build() ) // If you need to add features, like showing action buttons in your push, // you can set your own completion handler. MessagingPush.shared.didReceive(request) { notificationContent in if let mutableContent = notificationContent.mutableCopy() as? UNMutableNotificationContent { // Modify the push notification like adding action buttons! } contentHandler(notificationContent) } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Your app can now display rich push notifications in your app, including images, etc. See Deep Links to enable deep links in your push notifications. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/deep-links/ Deep links let you send people who interact with your messages to links in your app. You should set up deep links to make sure that your push notifications are actionable, and take people to screens that matter to your audience. Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Set up Universal Links To enable Universal Links in your iOS app, follow the instructions on the Apple documentation website. Be sure to complete all of the steps required including making modifications to your website to host a new file and making modifications to your mobile app’s code to handle the deep link. Depending on how you set up your mobile app (SwiftUI, UIKit, watchOS, etc), you may need to handle deep links in multiple functions in your code. Handling deep links on push notification click Our SDK automatically handles deep links for Customer.io push notifications, calling The SDK’s calls UIApplication.shared.open() for the deep link URL by default. You don’t need to process deep links yourself in userNotificationCenter(:didReceive:withCompletionHandler). But, if you want to control URL handling and open specific screens in your app, you should implement the application(:continue:restorationHandler:) method in your AppDelegate. class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard let universalLinkUrl = userActivity.webpageURL else { return false } // Parse `universalLinkUrl` object to perform the action you want in your app. // return true from this function if your app handled the deep link. // return false from this function if your app did not handle the deep link and you want sdk to open the URL in a browser. } } Deep link issue with calling application(:continue:restorationHandler:) Some 3rd party SDKs might block calls to the application(:continue:restorationHandler:) method. If you encounter this problem, you can use an alternative approach to receive callbacks when your audience clicks notifications with deep links. @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Step 2: initialize the SDK var cdpApiKey = YOUR_CDP_API_KEY var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .deepLinkCallback { (url: URL) in // You can call any method to process this further, // or redirect it to `application(_:continue:restorationHandler:)` for consistency, if you are already using it let openLinkInHostAppActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) openLinkInHostAppActivity.webpageURL = url return self.application(UIApplication.shared, continue: openLinkInHostAppActivity, restorationHandler: { _ in }) } CustomerIO.initialize(withConfig: config.build()) // Step 3: Initialize the in-app package // Change region to .EU if you're in our European Union data center! MessagingInApp.initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) // Step 4: Initialize the push package MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } }  Check universal links using your Notes app Try creating a note with a universal link and tapping the link to double-check that the link opens in your app and not in a browser window. This is an easy way to make sure that you’ve set up universal links correctly. If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Setup App Scheme Deep Links Open your Xcode project and go to your project’s settings. Select your app Target, click the Info tab, and then click URL Types > to create a new URL Type. Enter a unique value for your app for URL Schemes. --- ## Push metrics URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/push-metrics/ Gather metrics for push notifications sent from Customer.io. Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. If you already configured rich push notifications, the SDK will automatically track opened and delivered events for push notifications originating from Customer.io. See section Automatic push handling below to learn more about this great feature and how to best take advantage of it. Otherwise, you can: Record push metrics with UserNotifications. Extract delivery ID and Delivery Token parameters directly. Automatic push handling After you call MessagingPushAPN.initialize or MessagingPushFCM.initialize in your AppDelegate and set up the CioAppDelegateWrapper, your app is ready to automatically handle push notifications that originate from Customer.io. No additional code is required for your app to track opened push metrics or launch deep links.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Configure push behavior Configure push notification behavior using the MessagingPushConfigBuilder: MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) Configure push display behavior while app in foreground If your app is in the foreground and receives a push notification, Customer.io will show the notification based on the value of the showPushAppInForeground configuration property. You can choose whether or not to display push notifications when your app is foregrounded by implementing the userNotificationCenter(_:didReceive:withCompletionHandler:) method in your AppDelegate. Note that when you implement UNUserNotificationCenterDelegate, it takes priority over the showPushAppInForeground configuration property. Add the highlighted code to your AppDelegate.swift file for custom handling: class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // ... // Set the AppDelegate as a delegate for push notification events: UNUserNotificationCenter.current().delegate = self return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Function called when a push notification arrives while app is in foreground func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { // To show it, return appropriate options for your case completionHandler([.banner, .list, .badge, .sound]) // To hide it, return empty array // completionHandler([]) } } Custom handling when a Customer.io push is clicked Add the highlighted code to your AppDelegate.swift file if your app needs to perform custom handling when users click push notifications. For example, you’d need to perform custom handling if you need to process custom data that you attached to the push notification payload. class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // ... // Set the AppDelegate as a delegate for push notification events: UNUserNotificationCenter.current().delegate = self return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Function called when a push notification is clicked or swiped away. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // If you need to know if a push was clicked: let pushWasClicked = response.actionIdentifier == UNNotificationDefaultActionIdentifier // Process custom data attached to payload, if you need: let pushPayload = response.notification.request.content.userInfo // Important: When you're done processing the push notification, you're required to call the completionHandler. // Even if you do not process a push, you're still required to call the completionHandler() in this function. completionHandler() } } Deep links handling when push is clicked Our SDK handles deep links automatically for notifications originated from Customer.io. You can find more details at Deep Links. Capture push metrics with UserNotifications If you’re using a version of iOS that supports UserNotifications, you can track metrics using our UNNotificationContent helper. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // This 1 line of code might be all that you need! MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If you use `UserNotifications` for more then Customer.io push notifications, you can check // if the SDK handled the push for you or not. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) if !handled { // Notification was *not* displayed by Customer.io. Handle the notification yourself. } } Extract delivery ID and token If you’re not using a version of iOS that supports UserNotifications, you should send the push metric manually by extracting the CIO-Delivery-ID and CIO-Delivery-Token parameters directly to track push metrics. guard let deliveryID: String = notificationContent.userInfo["CIO-Delivery-ID"] as? String, let deviceToken: String = notificationContent.userInfo["CIO-Delivery-Token"] as? String else { // Not a push notification delivered by Customer.io return } MessagingPush.shared.trackMetric(deliveryID: deliveryID, event: .delivered, deviceToken: deviceToken) Disable automatic push tracking Automatic push metric recording is enabled by default when you install the SDK. You can disable this behavior in the SDK’s configuration. MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoTrackPushEvents(false) // Disable automatic push tracking .build() ) --- ## Sound in push notifications URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/sound-in-push/ When you send a notification, it can play an alert on your audience's device. The sound file must be bundled in your app, and you'll specify which sound you want to play in your notification payload. When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. --- ## Provisional Push URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/provisional/ Provisional push notifications are a way to send notifications on a trial basis. People can then evaluate the notifications and decide whether to authorize them. Provisional push support is available for iOS 15 and above. Customer.io doesn’t have a specific “provisional push” feature. This is just something that iOS supports out of the box with iOS 15+. See Apple’s provisional authorization documentation to learn more. When you request authorization for push notifications, you can include the .provisional option in the requestAuthorization call. class NotificationUtil: NotificationUtility { func showPromptForPushPermission(completionHandler: @escaping (Bool) -> Void) { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .provisional], completionHandler: { status, _ in completionHandler(status) }) } } --- ## Push service certificates URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/push-certificates/ Before you can send push notifications, you'll need to add credentials for the push service(s) you use to Customer.io. You can send push notifications to iOS devices using either Apple’s Push Notification service (APNs) or Google’s Firebase Cloud Messaging (FCM) service. To authorize your Customer.io workspace to send notifications through APNs, you’ll need to upload your APNs .p8 certificate and provide your credentials or upload your .JSON to iOS devices over Apple’s Push Notification service, you’ll need to upload your APNs .p8 certificate and enter your Apple Developer Key ID, Team ID, and Bundle ID. Upload your push certificate If you don’t already have your .p8 certificate for APNs or your .JSON file for FCM, you’ll need to get one before you can finish this process and send push notifications. APNs APNs In Customer.io, go to your workspace’s Settings > Workspace Settings and click Settings next to Push. Click Enable under iOS, and select the Apple Push Notification service (APNs) option. Click Choose file… and upload your .p8 certificate. Enter your Key ID, Team ID, and Bundle ID. You can find these in your Apple Developer Account. (Optional) Enable the Send all push notifications to sandbox option. Your iOS certificate may have both sandbox and production environments; this option sends push notifications to both environments.  We recommend creating a separate workspace for your sandbox environment. Click Save Changes FCM FCM In Customer.io, go to Settings > Workspace Settings and click Settings next to Push. For iOS, click Enable, and select the Firebase Cloud Messaging (FCM) option. Get your .p8 file for APNs Log into your Apple Developer account and go to Certificates, Identifiers & Profiles > Keys. Click the blue button to create a new key. Click Apple Push Notifications service (APNs) and enter a name for the key. Click Continue and then Register to create the key. Download your keys and put it somewhere you’ll remember. You can only download your key once! Get your .JSON file for FCM Before you can get a push certificate for Firebase Cloud Messaging, make sure that the FCM API is enabled for your project. You can check that here. Log into the Firebase Console for your project. Click in the sidebar and go to Project settings. Go to Service Accounts and click Generate New Private Key. Confirm your choice and download the credential file. --- ## Test your push implementation URL: https://docs.customer.io/integrations/sdk/ios/3.x/push/test-push/ Before you send push notifications, you should test your implementation to make sure it works as expected. After you set up push notifications, you should send some test messages. You can send messages through the Customer.io push composer. If your app is set up to send more than the standard title, body, image, and link, you’ll need to send a custom payload. The payloads below represent what your app can expect to receive from Customer.io. If you use a custom payload, you’ll need to use the format(s) below to make sure that the SDK receives your message properly. When testing, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. APNS payload APNS payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. FCM payload FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. --- ## Set up in-app messaging URL: https://docs.customer.io/integrations/sdk/ios/3.x/in-app/set-up-in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. How it works An in-app message is a message that people see within the app; people won’t see your message until they open your app. To set up in app messaging, install and initialize the CioDataPipelines and CioMessagingInApp packages. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which pages a person is visits, so we can display in-app messages on the correct screens (or “pages”) in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging Use Swift Package Manager to install the CioMessagingInApp package. See Getting Started for installation instructions. Initializing your app with the CioMessagingInApp package sets up your app to receive in-app messages. import CioDataPipelines import CioInternalCommon import CioMessagingInApp import UIKit @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: YOUR_CDP_API_KEY) .autoTrackDeviceAttributes(true) .autoTrackUIKitScreenViews() .migrationSiteId(siteId) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) return true } } Anonymous messages As of version 3.14, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. In-app configuration options You must pass both of the following configuration options when you initialize the MessagingInApp package. Option Type Default Description siteId string The Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from. region Region.US or Region.EU The region your Customer.io account resides in. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/ios/3.x/in-app/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages Add an inline view to your app’s UI You’ll add a UI element to your app’s UI code to support inline messages using SwiftUI, Storyboard, Interface Builder, or UIKit via code.  We’ve set up examples in our UIKit and SwiftUI sample apps that might help if you want to see a real-world implementation of this feature. Storyboard Storyboard Open your storyboard file and drag a UIView onto your view controller. Set the class of the UIView to InlineMessageUIView in the Identity Inspector. Setup layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See view layout for more information. In your ViewController, set the elementId for the InlineMessageUIView. This is the ID you’ll use in the Customer.io UI when you want to send an in-app message. import CioMessagingInApp @IBOutlet weak var inlineInAppView: InlineMessageUIView! override func viewDidLoad() { super.viewDidLoad() // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. inlineInAppView.elementId = "<element-id-here>" } UIKit Swift UIKit Swift Create an instance of InlineMessageUIView and add it to your view hierarchy. import CioMessagingInApp override func viewDidLoad() { super.viewDidLoad() // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. let inlineMessage = InlineMessageUIView(elementId: "<element-id-here>") } Setup layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See view layout for more information. SwiftUI SwiftUI Create an instance of InlineMessage and add it to your view hierarchy. You shouldn’t set a height on your View to avoid breaking functionality. InlineMessage automatically updates the height for you when messages load and are interacted with. import SwiftUI import CioMessagingInApp struct MyScreen: View { var body: some View { // Replace <element-id-here> with an ID that makes sense to you. // You'll use this ID when you build an in-app message in Customer.io. InlineMessage(elementId: "<element-id-here>") // Note: Avoid setting a hard-coded height on the View to avoid breaking functionality. // The InlineMessage dynamically changes it's height when messages are loaded and interacted with. // .frame() } } UIKit: Setup layout constraints for your message InlineMessageUIView uses AutoLayout and modifies its own height. But otherwise, you’ll need to set up layout constraints for the view including the width and the leading, top, trailing, and bottom constraints for the view. You can create a height constraint, but it won’t matter because we modify it at runtime. You may still want to set a height if you use Storyboard because XCode will throw warnings and errors if you don’t set a height. 2. Build and send your message! When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow these steps to implement custom action buttons for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events For inline in-app messages, you have 2 options for listening to these action click events. Register a delegate with inline View: UIKit UIKit import CioMessagingInApp class MyViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Given you have an inline View in your ViewController, set a delegate to listen for action events. // Note: The inline View holds a `weak` reference to the `onActionDelegate`. inlineInAppView.onActionDelegate = self } } extension MyViewController: InlineMessageUIViewDelegate { func onActionClick(message: InAppMessage, actionValue: String, actionName: String) { // Perform some logic when people tap an action button. // Example code handling button tap: switch(actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionValue) // You can add more cases here for other actions default: // Handle unknown actions or do nothing print("Unknown action: \(actionName)") } } } SwiftUI SwiftUI import CioMessagingInApp import SwiftUI struct MyScreen: View { var body: some View { InlineMessage(elementId: "<element-id-here>", onActionClick: { message, actionValue, actionName in // Perform some logic when custom action button pressed. // Example code handling button press: switch(actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action for enabling auto-renew enableAutoRenew(actionValue) // You can add more cases here for other actions default: // Handle unknown actions or do nothing print("Unknown action: \(actionName)") } }) } } Register a global SDK event listener. When you register an event listener with the in-app SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic if you want. Handle responses to messages (event listeners) Similar to modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionDelegate set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! Known limitations We’re actively developing this feature. But, in the meantime, you should be aware of the following limitations: InlineMessageUIView does not work as expected inside a UITableView: If you have a scrolling list, InlineMessageUIView works great in UIStackView and UIScrollView. --- ## Page rules URL: https://docs.customer.io/integrations/sdk/ios/3.x/in-app/target-in-app-messages/ Sending people in-app messages often depends on the screens they visit in your app. You can set page rules when you create in-app messages. These rules determine the pages that your audience must visit in your app to see each message. Before you can take advantage of page rules, you need to: Track screens in your app. You can add $0.autoTrackScreenViews = true to your CustomerIO.config to automatically track screens or you can track screens manually. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. The SDK automatically uses the class name of UIViewController, minus ViewController, as the name of each page. For example, if you wanted to display an in-app message on a class called EditProfileViewController, you would enter EditProfile as your page rule.  Make sure your screens use the same names across your apps If you have a screen called DashboardActivity in Android, and DashboardViewController in iOS, we’ll recognize Dashboard as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/ios/3.x/in-app/in-app-event-listeners/ When people receive an in-app message, you'll listen for events to handle the message's lifecycle—like dismissing the event or taking a custom action. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { CustomerIO.shared.track(name: "in-app action", properties: [ "delivery-id": message.deliveryId, "message-id": message.messageId, "action-value": actionValue, "action-name": actionName ]) } Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. MessagingInApp.shared.dismissMessage() --- ## 3.x -> 3.13.0 URL: https://docs.customer.io/integrations/sdk/ios/3.x/whats-new/3.13.0-upgrade/ This page introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. What changed? The changes are mainly to align our SDK APIs across different platforms. No functional changes to be expected. Upgrade process Attributes profileAttributes property is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s profile attributes Setter is replaced with setProfileAttributes(attributes: [String: Any]) deviceAttributes property is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s device attributes Setter is replaced with setDeviceAttributes(attributes: [String: Any]) Tracking Identifying a user These variants of identify are deprecated: identify<T: Codable>(traits: T) where traits are a generic type identify<RequestBody: Codable>(userId: String, traits: RequestBody?) You should use this instead: identify(userId: String, traits: [String: Any]?) setProfileAttributes(attributes: [String: Any]) can now be used to track anonymous profile attributes Tracking an event These variants of track are deprecated: track<RequestBody: Codable>(name: String, properties: RequestBody?) where traits are a generic type You should use this instead: track(name: String, properties: [String: Any]?) Screen tracking These variants of screen are deprecated: screen<RequestBody: Codable>(title: String, properties: RequestBody?) where traits are a generic type You should use this instead: screen(title: String, properties: [String: Any]? --- ## 3.x -> 3.9.0 URL: https://docs.customer.io/integrations/sdk/ios/3.x/whats-new/3.9.0-upgrade/ This page introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 3.9.0 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your app depending on whether you send push notifications with APN or FCM and whether you use UIKit or SwiftUI. Update with APNs UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import UIKit import CioMessagingPushAPN @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import UIKit import CioMessagingPushAPN @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required if you used version 2.x or earlier .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import SwiftUI import CioMessagingPushAPN @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import SwiftUI import CioMessagingPushAPN @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } Update with FCM UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import UIKit import CioMessagingPushFCM import Firebase import FirebaseMessaging @main class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } After (3.9.0) After (3.9.0) import UIKit import CioMessagingPushFCM import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) .migrationSiteId(siteId) .autoTrackUIKitScreenViews() .autoTrackDeviceAttributes(true) CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // optionally, configure the push module by calling functions on the builder. Such as: .autoFetchDeviceToken(true) // See section below to find all the configuration options you can set. .build() ) return true } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (3.x) Before (3.x) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } After (3.9.0) After (3.9.0) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize the Customer.io SDK let cdpApiKey = "YOUR_CDP_API_KEY" let siteId = "YOUR_SITE_ID" let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // If your account is in the EU region, uncomment the next line // .region(.EU) .migrationSiteId(siteId) // only required for migration .autoTrackUIKitScreenViews() // Set auto tracking of UIKit screen views .logLevel(CioLogLevel.debug) // Add this to troubleshoot issues - disable debug in production CustomerIO.initialize(withConfig: config.build()) return true } } Important Notes CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError didReceiveRemoteNotification userNotificationCenter(_:willPresent:withCompletionHandler:) userNotificationCenter(_:didReceive:withCompletionHandler:) All other push-related delegate methods The @main attribute must be on the wrapper class, not your AppDelegate. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushAPN.initialize() or MessagingPushFCM.initialize() --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/ios/3.x/whats-new/3.x-upgrade/ This page details breaking changes from the previous major version of the SDK, so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK, the in-app messaging module, and the push module. The in-app and push modules are now required. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select iOS. Enter a Name for your integration, like “My iOS App”. We’ll present you with a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your iOS app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Import CioDataPipelines instead of CioTracking We’ve replaced the CioTracking package with CioDataPipelines. You’ll need to update your import statements to reflect this change. If you see errors like Missing required module ‘CioTracking’, you can remove the package in XCode under Frameworks and Libraries. // replace import CioTracking with: import CioDataPipelines 3. Update your initialize calls You’ll initialize the new version of the SDK and its packages with SDKConfigBuilder objects instead of a CustomerIOConfig. A few of the configuration options changed. In particular, cdpApiKey replaces apiKey: this is a new key that you got from Step 1 migrationSiteId replaces siteId: this is the same key you used in the previous version of the SDK. You need to include this property to send remaining traffic when people update your app. autoTrackUIKitScreenViews replaces autoTrackScreenViews: functionality is unchanged; we simply renamed the option to reflect support for UIKit and not SwiftUI. If you’re in our EU region, make sure that you uncomment the .region(.EU) line in the sample below. Your config must include this property to send data to our EU data center. APNS APNS import CioDataPipelines import CioMessagingInApp import CioMessagingPushAPN import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. let cdpApiKey = YOUR_CDP_API_KEY let siteId = YOUR_SITE_ID let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // uncomment the line below if your account is in the EU region // .region(.EU) .autoTrackDeviceAttributes(true) .migrationSiteId(siteId) //replaces autoTrackScreenViews .autoTrackUIKitScreenViews() CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) UNUserNotificationCenter.current().delegate = self return true } FCM FCM import CioDataPipelines import CioMessagingInApp import CioMessagingPushFCM import FirebaseCore import FirebaseMessaging import Foundation import UIKit @main // Add the CioAppDelegateWrapper to handle push notifications and device token registration class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // To set up FCM push: https://firebase.google.com/docs/cloud-messaging/ios/client // FCM provides a device token to the app that // you send to the Customer.io SDK. // Initialize the Firebase SDK. FirebaseApp.configure() let siteId = YOUR_JOURNEYS_SITE_ID let cdpApiKey = YOUR_CDP_API_KEY // Configure and initialize the Customer.io SDK let config = SDKConfigBuilder(cdpApiKey: cdpApiKey) // uncomment this line below if your account is in the EU region // .region(.EU) .migrationSiteId(siteId) .autoTrackDeviceAttributes(true) //replaces autoTrackScreenViews .autoTrackUIKitScreenViews() CustomerIO.initialize(withConfig: config.build()) // Initialize messaging features after initializing Customer.io SDK MessagingInApp .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) .setEventListener(self) MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .autoFetchDeviceToken(true) // Automatically fetch device token and upload to CustomerIO .autoTrackPushEvents(true) // Automatically track push metrics .showPushAppInForeground(true) // Enable Notifications in the foreground .build() ) // Manually get FCM device token. Then, we will forward to the Customer.io SDK. Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken } } 4. Update your NotificationServiceExtension You need to initialize the push module with your integration’s CDP API Key. Update your NotificationServiceExtension (using the appropriate push package, APN or FCM). If you previously initialized the SDK using CustomerIO.initialize, you can remove that now. You only need to initialize the push package. // use MessagingPushFCM if FCM is your push service MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder( cdpApiKey: "YOUR_CDP_API_KEY" ) // Optional: set your Customer.io account region (.US or .EU). Default: US .region(.US) .build() ) 5. Update your identify, track, and screen calls Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track: data becomes properties screen: name becomes title, and data becomes properties New (3.x) New (3.x) // CioDataPipelines replaces CioTracking import CioDataPipelines //identify: identifier becomes userId, body becomes traits CustomerIO.shared.identify(userId: "USER_ID", traits: ["age": 30]) // track: data becomes properties CustomerIO.shared.track(name: "Purchase", properties: ["product": "shirt"]) // screen: name becomes title, data becomes properties CustomerIO.shared.track(title: "Cart", properties: ["source": "link"]) Old (2.x) Old (2.x) import CioTracking // identify CustomerIO.shared.identify(identifier: "USER_ID", body: ["age": 30]) {} // track CustomerIO.shared.track(name: "Purchase", data: ["product": "shirt"]) // screen CustomerIO.shared.track(name: "Cart", data: ["source": "link"]) Configuration Changes As a part of this release, we’ve changed a few configuration options. The MessagingInApp and MessagingPush modules also now take their own configuration options. DataPipelines configuration options For the base SDK, you’ll use SDKConfigBuilder to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data into Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. autoTrackUIKitScreenViews boolean false Replaces autoTrackScreenViews; functionality is unchanged. We simply renamed the option to reflect support for UIKit and not SwiftUI. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). MessagingPush configuration options You need to initialize the push package in both your AppDelegate and NotificationServiceExtension files. In your AppDelegate, you don’t need to pass options. You can simply pass MessagingPushAPN.initialize(). But, in your NotificationServiceExtension, you’ll need to pass the cdpApiKey to initialize the push package. MessagingPushAPN.initializeForExtension(withConfig: MessagingPushConfigBuilder(cdpApiKey: "CDP_API_KEY").build()) Option Type Default Description region .US or .EU .US The region your Customer.io account resides in US or EU. autoFetchDeviceToken boolean true When true, the package automatically fetches the device token for push notifications. autoTrackPushEvents boolean true Automatically track opened and delivered metrics based on push notifications. showPushAppInForeground boolean true Show push notifications when the app is in the foreground. Used only if customer’s AppDelegate doesn’t implement UNUserNotificationCenterDelegate. MessagingInApp configuration options When you initialize the CioMessagingInApp package, you must pass both of these configuration options. Option Type Default Description siteId string The Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from. Region .US or .EU .US The region your Customer.io account resides in—US or EU. --- ## 1x -> 2.x URL: https://docs.customer.io/integrations/sdk/ios/3.x/whats-new/2.x-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 1.x to 2.x Singleton API is now enforced In version 1.x of the Customer.io iOS SDK, could use the SDK in 2 ways: Singleton API: CustomerIO.initialize(...) // initialize the singleton SDK instance Customer.shared.track(...) // use the singleton SDK instance Non-singleton API: let cio = CustomerIO(...) // initialize the non-singleton SDK instance cio.track(...) // use the non-singleton SDK instance In version 2.x, we removed the non-singleton API. To successfully migrate, you need to replace any code using a non-singleton with the singleton instance: // Replace the non-singleton instance: cio.track(...) messagingPush.application(...) // With `CustomerIO.shared` or `MessagingPush.shared`: CustomerIO.shared.track(...) MessagingPush.shared.application(...) If your app uses a technique like dependency injection, you can keep your code base as-is and simply replace code where you create new instances of the SDK: // For example, if you have code that accepts a CustomerIO dependency in the constructor (to easily allow mocking the Customer.io SDK): class Repository { let customerIO: CustomerIO init(customerIO: CustomerIO) { self.customerIO = customerIO } func acceptFriendRequest() { ... self.customerIO.track(...) ... } } // You can keep your Repository as-is, but you need to change where you create instances from: let repository = Repository(customerIO: CustomerIO(...)) repository.acceptFriendRequest() // To: let repository = Repository(customerIO: CustomerIO.shared) // Don't forget to initialize the SDK 😉 repository.acceptFriendRequest() Configuration of the SDK happens during initialization In version 1.x of the Customer.io iOS SDK, you configured the SDK through a .config function: CustomerIO.config { $0.autoTrackScreenViews = true } In version 2.x of the Customer.io iOS SDK, we moved the .config function into CustomerIO.initialize. You’ll need to move your configuration into the SDK initialization process to migrate: CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. SDK initialization: required parameters In version 1.x of the Customer.io SDK, the function CustomerIO.initialize contained optional parameters. We had to remove those and make all parameters required. To migrate to 2.x of the SDK, fill in the rest of the parameters in your initialize function: // v1.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // v2.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) {} // Optionally, if you want to configure settings of the SDK, do so in initialization. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. Rich push initialization If you have followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Because of the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your host app and in your Notification Service. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Cocoapods users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. This means that you need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. If you installed the Customer.io SDK using Swift Package Manager, this change does not effect you. We fixed a bug with custom attributes that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in v1 that converted keys in your custom data to snake_case. This bug is fixed in v2 of the iOS SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect our Android SDK.. // If you passed in custom attributes using camelCase keys: data = ["firstName": "Dana"] // The SDK v1 may have converted this data into: data = ["first_name": "Dana"] // Or, if you used a different format that was not snake_case: data = ["FIRSTNAME": "Dana"] // The SDK v1 may have converted this data into: data = ["f_irstname": "Dana"] You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Anywhere you call the Customer.io SDK and provide custom attributes like this: CustomerIO.shared.identify("dana@example.com", data: ["firstName": "Dana"]) // Consider sending duplicate data with snake_case CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. ]) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // We can remove the snake_case attribute and go back to just camelCase! ]) --- ## Changelog URL: https://docs.customer.io/integrations/sdk/ios/3.x/whats-new/changelog/ Check out release history for stable releases of iOS SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Get Started URL: https://docs.customer.io/integrations/sdk/ios/1.x/getting-started/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. SDK package products To minimize our SDK’s impact on your app’s size, we offer multiple, separate SDKs. You should only install the packages that you need for your project. You must install the Tracking package. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Product Required? Description Tracking ✅ identify people in Customer.io MessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) MessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) MessagingInApp Receive in-app notifications Install the SDK Follow Apple’s instructions to add https://github.com/customerio/customerio-ios.git as a dependency to your project in Xcode and select the individual package products that you want to install. We recommend that you set the Dependency Rule to Up to Next Major Version. While we encourage you to keep your app up to date with the latest SDK, major versions can include breaking changes or new features that require your attention. Install with CocoaPods We typically recommend that you install the SDK using Swift Package Manager (SPM). However, if your app uses CocoaPods, you can find and install our pods by appending CustomerIO/ to our packages—e.g. CustomerIO/Tracking. Package Product Required? Description CustomerIO/Tracking ✅ identify people in Customer.io CustomerIO/MessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) CustomerIO/MessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) CustomerIO/MessagingInApp Receive in-app notifications Initialize the SDK Before you can use the Customer.io SDK, you need to initialize it. Any calls that you make to the SDK before you initialize it are ignored. The SDK uses a Singleton. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. To get started, initialize the SDK. You’ll usually do this in the AppDelegate application(_ application: didFinishLaunchingWithOptions) function. import CioTracking class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // You can optionally provide a Region to set the Region for your Workspace: CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) return true } } When you want to use any of the SDK features, you use the shared instance of the class. CustomerIO.shared.track(...) MessagingPush.shared.application(...) Configuration options When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app or enable autoTrackScreenViews to automatically capture screen view events for your audience. Option Type Default Description disableCustomAttributeSnakeCasing boolean false New in 1.28 Prior to SDK version 1.2.8, we inadvertently snake-cased attributes (i.e. myAttr became my_attr). Set to true to preserve custom attributes exactly as you pass them to the SDK. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io autoTrackScreenViews boolean false If true, the SDK automatically sends screen events for every screen your audience visits. autoScreenViewBody strings When autoTrackScreenViews is true, use this to override the the body of automatic screen view events. See automatic screen tracking for more information. backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. CustomerIO.config { $0.autoTrackScreenViews = true } Attribute snake-casing In versions of the SDK prior to 1.2.8, functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—inadvertently converted custom data property names to snake_case. This bug is fixed in v2 of the iOS SDK. But, if you’re not ready to update your SDK implementation, you can use the disableCustomAttributeSnakeCasing configuration option in version 1.2.8 or later to make sure that the SDK respects your attribute names. CustomerIO.config { $0.disableCustomAttributeSnakeCasing = true } This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect our Android SDK.. // If you passed in custom attributes using camelCase keys: data = ["firstName": "Dana"] // The SDK < v1.2.8 may have converted this data into: data = ["first_name": "Dana"] // Or, if you used a different format that was not snake_case: data = ["FIRSTNAME": "Dana"] // The SDK < v1.2.8 may have converted this data into: data = ["f_irstname": "Dana"]  Check your data index to see if you were impacted The Data Index page shows a list of attributes and events in your workspace. You can check your data index to see if any of your data was misshaped by previous versions of the SDK. See our iOS 2.0 migration page for some strategies to deal with with misshaped attribute names. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] --- ## Identify people URL: https://docs.customer.io/integrations/sdk/ios/1.x/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You need the **Tracking** package to identify people. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. If you already registered a device token, identifying a person automatically associates the token with the identified person. You can register for a device token before or after you identify a person. See our Push Documentation for help registering device tokens. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (when updating people), depending on your workspace settings. body (Optional): Contains attributes that you want to add to, or update on, a person. https://customer.io/api/track/#operation/identify import CioTracking CustomerIO.shared.identify(identifier: "989388339", body: ["first_name": firstName]) // `body` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let body = ["first_name": "Dana", "last_name": "Green"] CustomerIO.shared.identify(identifier: "989388339", body: body) // 2. `Encodable` object: struct IdentifyRequestBody: Encodable { let firstName: String let lastName: String } CustomerIO.shared.identify(identifier: "989388339", body: IdentifyRequestBody(firstName: "Dana", lastName: "Green")) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with profileAttributes. CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] You only need to pass the attributes that you want to create or modify to profileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.shared.deviceAttributes = ["company" : "cio", "checklist" : "complete"] However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. CustomerIO.config { $0.autoTrackDeviceAttributes = false } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Future calls to the Customer.io SDK will be ignored until you identify a new person. CustomerIO.shared.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/ios/1.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. data (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import CioTracking CustomerIO.shared.track(name: "logged_in", data: ["ip": "127.0.0.1"]) // The `data` parameter can be optionally skipped CustomerIO.shared.track(name: "played_game") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["product": "socks", "price": "23.45"] CustomerIO.shared.track(name: "purchase", data: data) // 2. A custom `Encodable` type: struct Purchase: Encodable { let product: String let price: Double } CustomerIO.shared.track(name: "purchase", data: Purchase(product: "socks", price: 23.45)) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your app. You can turn on automatic screen tracking in CustomerIO.config. CustomerIO.config { $0.autoTrackScreenViews = true // Optional configuration where you can modify the `data` being sent for // automatic screenview events. $0.autoScreenViewBody = { return ["seconds_on_screen": SecondsOnScreenManager.getTimeInScreen()] } } For automatic screenview tracking, the SDK automatically names the screen as the class name of the UIViewController, minus ViewController. For example, if you have a class EditProfileViewController in your code base, the SDK will automatically send a screenview event with the screen name EditProfile. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can disable automatic screen tracking and send screen events manually. Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. import CioTracking // You can send an event with or without `data`. CustomerIO.shared.screen(name: "BaseballDailyScores") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["prev_screen": "homescreen", "seconds_in_app": "120"] CustomerIO.shared.screen(name: "BaseballDailyScores", data: data) // 2. A custom `Encodable` type: struct Screen: Encodable { let prevScreen: String let secondsInApp: Int } CustomerIO.shared.screen(name: "BaseballDailyScores", data: Screen(prevScreen: "homescreen", secondsInApp: 120)) --- ## Push notifications URL: https://docs.customer.io/integrations/sdk/ios/1.x/push/ Our iOS SDK supports push notifications over APN or FCM. Use this page to get started with either service. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style push fill:#B5FFEF,stroke:#007069 style register-token fill:#B5FFEF,stroke:#007069 Before you begin This page explains how to receive push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through your preferred service: Apple Push Notification Service (APNs) or Firebase Cloud Messaging (FCM). This process lets you receive basic push notifications in your app—a title and a message body. To send a more complicated push, complete the process on this page, and then see our rich push documentation. Install the push package Before you can receive push notifications, you need to install the push package supporting the push service you use—APNs or FCM. Use Swift Package Manager to install your push package. See our Getting Started page for installation instructions. APNs: install MessagingPushAPN FCM: install MessagingPushFCM Enable the Push Notifications capability in XCode. Register for push notifications To send a push notification, you have to register for a device token and identify the user, which associates the device token with a person in Customer.io. You can do these things in any order—it doesn’t matter whether you register for a token before or after you identify the current user. When you identify a person or stop identifying a person, the SDK automatically removes the device token from any previously identified profile and then associates any existing device token with the newly identified profile. This ensures that a device token is only registered to the currently identified profile in the SDK. This prevents you from sending duplicate messages or sending messages to the wrong person. After you initialize the SDK, register for remote push to receive a device token from your push service. Your code changes slightly depending on the push service you use. APNs APNs import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // It's a good practice to register for remote push when the app starts. // This asserts that the Customer.io SDK always has a valid device token. UIApplication.shared.registerForRemoteNotifications() return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } FCM FCM import CioMessagingPushFCM import CioTracking import Firebase import FirebaseMessaging class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // Set FCM messaging delegate Messaging.messaging().delegate = self // You should register for remote push when the app starts. // This asserts that the Customer.io SDK always has a valid device token. UIApplication.shared.registerForRemoteNotifications() return true } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Identify the person if you have not already. Even after you add a device token, you can’t use it until you associate it with a person. CustomerIO.shared.identify(identifier: "989388339", body: ["first_name": firstName]) When you identify a person, you should see their device token in your workspace. You can send a simple push notification to test your implementation. See Rich push if you want to send a push with images, action buttons, or deep links. Capture push metrics Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. If you already configured rich push notifications, the SDK will automatically track opened and delivered events for push notifications originating from Customer.io. Otherwise, you can: Record push metrics with UserNotifications. Extract delivery ID and Delivery Token parameters directly. Capture push metrics with UserNotifications If you’re using a version of iOS that supports UserNotifications, you can track metrics using our UNNotificationContent helper. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // This 1 line of code might be all that you need! MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If you use `UserNotifications` for more then Customer.io push notifications, you can check // if the SDK handled the push for you or not. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) if !handled { // Notification was *not* displayed by Customer.io. Handle the notification yourself. } }  UserNotifications has some inconsistencies when tracking delivered events. If delivered events are important to you, we recommend that you follow the setup instructions for rich push notifications, even if you do not plan on sending rich push notifications as rich push tracks delivered events more reliably. Extract delivery ID and token If you’re not using a version of iOS that supports UserNotifications, you should send the push metric manually by extracting the CIO-Delivery-ID and CIO-Delivery-Token parameters directly to track push metrics. guard let deliveryID: String = notificationContent.userInfo["CIO-Delivery-ID"] as? String, let deviceToken: String = notificationContent.userInfo["CIO-Delivery-Token"] as? String else { // Not a push notification delivered by Customer.io return } MessagingPush.shared.trackMetric(deliveryID: deliveryID, event: .delivered, deviceToken: deviceToken) Disable automatic push tracking Automatic push metric recording is enabled by default when you install the SDK. You can disable this behavior. CustomerIO.config { $0.autoTrackPushEvents = false } --- ## Rich push notifications URL: https://docs.customer.io/integrations/sdk/ios/1.x/rich-push/ With rich push, you can do more than just send a simple notification; you can send an image, open a deep link when someone taps your message, and more! This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style rich-push fill:#B5FFEF,stroke:#007069 Before you begin You should set up APN or FCM push notifications and make sure that you can send yourself a test push notification before you start implementing rich push. While rich push generally entails a number of features, our SDK only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch. Read below for tips on how to extend the functionality of the SDK with features we do not yet support. Set up rich push Add a Service App Extension to your project in Xcode. You should now see a new file added to your Xcode project. The file is probably named NotificationService and looks similar to this. import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { } override func serviceExtensionTimeWillExpire() { } } Modify this file by selecting the push package you want to import and calling the appropriate Customer.io functions. Your code changes if: Customer.io is your only push/rich push provider Customer.io is not your only provider You want to take advantage of push features outside the Customer.io, like action buttons; in this case, you’ll need to set your own completion handler. Customer.io push only Customer.io push only 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Multiple push services Multiple push services 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) // If you use a service other than Customer.io to send rich push, // you can check if the SDK handled the rich push for you. If it did not, you // know that the push was *not* sent by Customer.io and you can try another way. let handled = MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) if !handled { // Rich push was *not* sent by Customer.io. Handle the rich push in another way. } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Custom completion handler Custom completion handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) // If you need to add features, like showing action buttons in your push, // you can set your own completion handler. MessagingPush.shared.didReceive(request) { notificationContent in if let mutableContent = notificationContent.mutableCopy() as? UNMutableNotificationContent { // Modify the push notification like adding action buttons! } contentHandler(notificationContent) } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Your app can now display rich push notifications in your app, including images, etc. However, if you want to enable deep links, you should continue to the Deep links section below. Deep links After you set up rich push notifications you can enable deep links in rich push notifications. Modify your AppDelegate with the following information. This enables your app to launch a deep link URL when someone taps a notification. import CioMessagingPushAPN import CioTracking import Foundation import UIKit class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) // Must call this function in order for `UNUserNotificationCenterDelegate` functions // to be called. UNUserNotificationCenter.current().delegate = self // It's good practice to always register for remote push when the app starts. // This asserts that the Customer.io SDK always has a valid APN device token to use. UIApplication.shared.registerForRemoteNotifications() return true } } extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } // OPTIONAL: If you want your push UI to show even with the app in the foreground, override this function and call // the completion handler. @available(iOS 10.0, *) func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } } Setup deep linking in your app. There are two ways to do this, and you can do both if you choose. Universal Links: universal links are great if you want to open your mobile app instead of a web browser when your goes to a page on your website. However, universal links take more time to setup. Follow this guide to set up Universal Links in your app. App scheme: app scheme deep links are quick and easy to setup. However, they do not work if the mobile app is not installed, which is where universal links provide an advantage. To enable app scheme deep links. See the section below to enable app scheme deep links. Setup App Scheme Deep Links App scheme deep links only work if your audience has your mobile app installed. If you want to support cases where your audience might not already have your app, you can set up universal links. Open your Xcode project and go to your project’s settings. Select your app Target, click the Info tab, and then click URL Types > to create a new URL Type. Enter a unique value for your app for URL Schemes. Test Rich Push After you set up rich push, you should test your implementation. Use the payloads below to send a push in the Customer.io web app with a Custom Payload. In both of the test payloads below, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. APNs test payload APNs test payload { "CIO": { "push": { "link": "remote-habits://deep?message=hello&message2=world", "image": "https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg" } }, "aps": { "mutable-content": 1, "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" } } } FCM test payload FCM test payload { "message": { "apns": { "payload": { "CIO": { "push": { "link": "remote-habits://deep?message=hello&message2=world", "image": "https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg" } }, "aps": { "mutable-content": 1, "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" } } } } } } Rich push payloads To send a rich push in Customer.io, you need to use our Custom Payload editor, which takes a JSON structure. In the editor, you’ll select the type of device you want to send your message to: you can have separate payloads for Android and iOS. In our case, you’ll click iOS. The top level of the payload changes slightly depending on your push provider, APNS or FCM. Otherwise, your JSON is split into two major objects: an aps object, which contains the standard aspects of a push—the alert.title and alert.body of your message—and Apple’s push options. a CIO object containing the rich aspects of your message that the SDK will interpret. At present, it contains link and image strings. APNs payload APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. FCM payload FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/ios/1.x/in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. To set up in app messaging, install and initialize the Tracking and MessagingInApp packages. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging Use Swift Package Manager to install the MessagingInApp package. See Getting Started for installation instructions. Add the MessagingInApp module to your app and initialize it with your in-app organizationId. You’ll find your organizationId in Customer.io under Settings > Workspace Settings > In-App Settings. import CioMessagingInApp import CioTracking // Step 1: Initialise CustomerIO SDK CustomerIO.initialize(siteId: workspaceId, apiKey: apiKey, region: Region.US) // Optionally configure the CustomerIO SDK: CustomerIO.config { $0.logLevel = .debug $0.autoTrackScreenViews = true } // Setup In-app messaging MessagingInApp.shared.initialize(organizationId: organizationId) Now your app can receive in-app messages. Create a campaign and send your first in-app message to test your implementation! Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. You can add $0.autoTrackScreenViews = true to your CustomerIO.config to automatically track screens or you can track screens manually. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. The SDK automatically uses the class name of UIViewController, minus ViewController, as the name of each page. For example, if you wanted to display an in-app message on a class called EditProfileViewController, you would enter EditProfile as your page rule.  Make sure your screens use the same names across your apps If you have a screen called DashboardActivity in Android, and DashboardViewController in iOS, we’ll recognize Dashboard as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## Test support URL: https://docs.customer.io/integrations/sdk/ios/1.x/test-support/ The SDK makes it easy to write unit, integration, UI, or other types of automated tests in your code base. We designed our SDK with first-class support for automated testing, making it easy to inject dependencies and perform mocking in your code. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style test-support fill:#B5FFEF,stroke:#007069 Dependency injection Every SDK class inherits from a Swift protocol. Inherited protocols use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. If you want to inject a class in your project, it could look something like the example below. import CioTracking class ProfileRepository { private let cio: CustomerIOInstance init(cio: CustomerIOInstance) { self.cio = cio } // Now, you can call any of the `CustomerIO` class functions with `self.cio`! func loginUser(email: String, password: String, onComplete: @escaping (Result<Success, Error>) -> Void) { // Login the user to your system... // Then, identify the profile with Customer.io: self.cio.identify(identifier: email) } } // Inject an instance of the `CustomerIO` class to your class: let cio = CustomerIO(...) let repository = ProfileRepository(cio: cio) Mocking The Customer.io SDK comes bundled with mock classes ready for you to use. That’s right, we generated mocks for you! Mock classes follow the naming convention: <NameOfClass>Mock. For example, mock the CustomerIO class with CustomerIOMock. Here’s an example test class showing how you would test your ProfileRepository class. import Foundation import CioTracking import XCTest class ProfileRepositoryTest: XCTestCase { private var cioMock: CustomerIOMock! private var repository: ProfileRepository! override func setUp() { super.setUp() cioMock = CustomerIOMock() // Create a new instance of the mock in setUp() to reset the mock. repository = ProfileRepository(cio: cioMock) } func test_loginUser() { // Now, call your function under test: repository.loginUser(...) // You can access many properties of the mock class to assert the behavior of the mock. XCTAssertTrue(cioMock.mockCalled) XCTAssertEqual(cioMock.identifyBodyCallsCount, 1) XCTAssertEqual(cioMock.identifyBodyReceivedInvocations[0].identifier, expectedIdentifier) } } --- ## Changelog URL: https://docs.customer.io/integrations/sdk/ios/1.x/changelog/ Check out release history for stable releases of iOS SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Get Started URL: https://docs.customer.io/integrations/sdk/ios/2.x/getting-started/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. SDK package products To minimize our SDK’s impact on your app’s size, we offer multiple, separate SDKs. You should only install the packages that you need for your project. You must install the Tracking package. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Product Required? Description Tracking ✅ identify people in Customer.io MessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) MessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) MessagingInApp Receive in-app notifications Prerequisites To support the Customer.io SDK, you must: Set iOS 13 or later as your minimum deployment target in XCode Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Install the SDK Follow Apple’s instructions to add https://github.com/customerio/customerio-ios.git as a dependency to your project in Xcode and select the individual package products that you want to install. We recommend that you set the Dependency Rule to Up to Next Major Version. While we encourage you to keep your app up to date with the latest SDK, major versions can include breaking changes or new features that require your attention. Install with CocoaPods We typically recommend that you install the SDK using Swift Package Manager (SPM). However, if your app uses CocoaPods, you can find and install our pods by appending CustomerIO/ to our packages—e.g. CustomerIO/Tracking. Package Product Required? Description CustomerIO/Tracking ✅ identify people in Customer.io CustomerIO/MessagingPushAPN Receive push notifications over Apple’s Push Notification Service (APNs) CustomerIO/MessagingPushFCM Receive push notifications over Google Firebase Cloud Messaging (FCM) CustomerIO/MessagingInApp Receive in-app notifications Initialize the SDK Before you can use the Customer.io SDK, you need to initialize it. Any calls that you make to the SDK before you initialize it are ignored. The SDK uses a Singleton. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. To get started, initialize the SDK. You’ll usually do this in the AppDelegate application(_ application: didFinishLaunchingWithOptions) function. import CioTracking class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in } // The last parameter of `initialize()` is configuration. Ready for you to tweak if you wish. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackScreenViews = true } return true } } When you want to use any of the SDK features, you use the shared instance of the class. CustomerIO.shared.track(...) MessagingPush.shared.application(...) Configuration options You need to call configuration options when you initialize the SDK. The last parameter of initialize() contains your configuration options. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackScreenViews = true } When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app or enable autoTrackScreenViews to automatically capture screen view events for your audience. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io autoTrackScreenViews boolean false If true, the SDK automatically sends screen events for every screen your audience visits. autoScreenViewBody strings When autoTrackScreenViews is true, use this to override the the body of automatic screen view events. See automatic screen tracking for more information. backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] Using the SDK as a data source The SDK uses our Legacy Track API API, but it can also double as a source of data for other integrations without additional development work. To do this, we translate calls from the SDK to our newer Data Pipelines API format before we send them to your destinations. In general, we recommend that you upgrade your app to use a newer version of the SDK. Our newer versions rely on the Data Pipelines API, so you can take advantage of your mobile data without without us translating it for you. It can make it easier to trace data from your app to your destinations and troubleshoot issues as they arise. Our newer SDKs also support more features, like anonymous tracking. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/ios/2.x/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You need the **Tracking** package to identify people. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. If you already registered a device token, identifying a person automatically associates the token with the identified person. You can register for a device token before or after you identify a person. See our Push Documentation for help registering device tokens. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (when updating people), depending on your workspace settings. body (Optional): Contains attributes that you want to add to, or update on, a person. https://customer.io/api/track/#operation/identify import CioTracking CustomerIO.shared.identify(identifier: "989388339", body: ["first_name": firstName]) // `body` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let body = ["first_name": "Dana", "last_name": "Green"] CustomerIO.shared.identify(identifier: "989388339", body: body) // 2. `Encodable` object: struct IdentifyRequestBody: Encodable { let firstName: String let lastName: String } CustomerIO.shared.identify(identifier: "989388339", body: IdentifyRequestBody(firstName: "Dana", lastName: "Green")) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with profileAttributes. CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] You only need to pass the attributes that you want to create or modify to profileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.shared.deviceAttributes = ["company" : "cio", "checklist" : "complete"] However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. CustomerIO.initialize(...) { config in config.autoTrackDeviceAttributes = false } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Future calls to the Customer.io SDK will be ignored until you identify a new person. CustomerIO.shared.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/ios/2.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. data (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import CioTracking CustomerIO.shared.track(name: "logged_in", data: ["ip": "127.0.0.1"]) // The `data` parameter can be optionally skipped CustomerIO.shared.track(name: "played_game") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["product": "socks", "price": "23.45"] CustomerIO.shared.track(name: "purchase", data: data) // 2. A custom `Encodable` type: struct Purchase: Encodable { let product: String let price: Double } CustomerIO.shared.track(name: "purchase", data: Purchase(product: "socks", price: 23.45)) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your UIKit-based app. You can turn on automatic screen tracking in the SDK’s configuration. CustomerIO.initialize(...) { config in config.autoTrackScreenViews = true // Optional function to filter out certain view controllers from automatic screenview events. // Setting your own function will skip the SDK's filtering logic. config.filterAutoScreenViewEvents = { viewController in return true // return `true` to send screenview event for this view controller } // Optional configuration where you can modify the `data` being sent for // automatic screenview events. config.autoScreenViewBody = { return ["seconds_on_screen": SecondsOnScreenManager.getTimeInScreen()] } }  Apple framework views are filtered out by default (iOS SDK version >= 2.8.0) By default, the SDK filters all Views that belong to an Apple framework (SwiftUI, UIKit). You should expect Views from your app and any third-party frameworks to be sent as screenview events. You can override this default behavior by providing a custom filter function. For automatic screenview tracking, the SDK automatically names the screen as the class name of the UIViewController, minus ViewController. For example, if you have a class EditProfileViewController in your code base, the SDK will automatically send a screenview event with the screen name EditProfile. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can disable automatic screen tracking and send screen events manually. Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. import CioTracking // You can send an event with or without `data`. CustomerIO.shared.screen(name: "BaseballDailyScores") // `data` accepts [String: Any] or an `Encodable` object // 1. [String: Any]: let data = ["prev_screen": "homescreen", "seconds_in_app": "120"] CustomerIO.shared.screen(name: "BaseballDailyScores", data: data) // 2. A custom `Encodable` type: struct Screen: Encodable { let prevScreen: String let secondsInApp: Int } CustomerIO.shared.screen(name: "BaseballDailyScores", data: Screen(prevScreen: "homescreen", secondsInApp: 120)) --- ## Push notifications URL: https://docs.customer.io/integrations/sdk/ios/2.x/push/ Our iOS SDK supports push notifications over APN or FCM. Use this page to get started with either service. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style push fill:#B5FFEF,stroke:#007069 style register-token fill:#B5FFEF,stroke:#007069 Before you begin This page explains how to receive push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through your preferred service: Apple Push Notification Service (APNs) or Firebase Cloud Messaging (FCM). This process lets you receive basic push notifications in your app—a title and a message body. To send a more complicated push, complete the process on this page, and then see our rich push documentation. Install the push package Before you can receive push notifications, you need to install the push package supporting the push service you use—APNs or FCM. Use Swift Package Manager to install your push package. See our Getting Started page for installation instructions. APNs: install MessagingPushAPN FCM: install MessagingPushFCM Enable the Push Notifications capability in XCode. Register for push notifications The instructions in this section set you up to receive simple push notifications with a body and title. After you follow these instructions, you’ll need to do a bit more work to support rich push notifications. 🎉Updated in version 2.11.0 The SDK automatically handles push registration and push clicks for you. However, you’ll still need to identify users before you can send them push notifications.  Using version 2.10 or earlier? As of version 2.11, we automatically register device tokens and handle push clicks. Follow our upgrade guide to remove unnecessary code and increase compatibility with 3rd party SDKs in your app. After you initialize the SDK, register for remote push to receive a device token from your push service. Your code changes slightly depending on the push service you use. APNs APNs import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // Initialize Customer.io push features after you initialize the SDK: MessagingPushAPN.initialize { config in // Automatically register push device tokens to the Customer.io SDK config.autoFetchDeviceToken = true // When your app is in the foreground and a push is delivered, show the push config.showPushAppInForeground = true } return true } } FCM FCM import CioMessagingPushFCM import CioTracking import Firebase import FirebaseMessaging class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // Initialize Customer.io push features after you initialize the SDK: MessagingPushFCM.initialize { config in // Automatically register push device tokens to the Customer.io SDK config.autoFetchDeviceToken = true // When your app is in the foreground and a push is delivered, show the push config.showPushAppInForeground = true } return true } } Identify the person if you have not already. Even after you add a device token, you can’t use it until you associate it with a person. CustomerIO.shared.identify(identifier: "989388339", body: ["first_name": firstName]) When you identify a person, you should see their device token in your workspace. You can send a simple push notification to test your implementation. Note that when you identify a different person or stop identifying a person, the SDK automatically removes the device token from any previously identified profile. This ensures that a device token is only registered to the currently identified profile in the SDK and prevents you from sending duplicate messages messaging the wrong person. Set up rich push This process prepares your app to receive push notifications with images and links. Add a Service App Extension to your project in Xcode. You should now see a new file added to your Xcode project. The file is probably named NotificationService and looks similar to this. import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { } override func serviceExtensionTimeWillExpire() { } } Modify this file by selecting the push package you want to import and calling the appropriate Customer.io functions. Your code changes if: Customer.io is your only push/rich push provider Customer.io is not your only provider You want to take advantage of push features outside the Customer.io, like action buttons; in this case, you’ll need to set your own completion handler. Customer.io push only Customer.io push only 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. // The last parameter optionally allows you to configure the SDK. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } // For simple apps that only use Customer.io for sending rich push messages, // This 1 line of code is all that you need! MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Multiple push services Multiple push services 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. // The last parameter optionally allows you to configure the SDK. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } // If you use a service other than Customer.io to send rich push, // you can check if the SDK handled the rich push for you. If it did not, you // know that the push was *not* sent by Customer.io and you can try another way. let handled = MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) if !handled { // Rich push was *not* sent by Customer.io. Handle the rich push in another way. } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Custom completion handler Custom completion handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // Keep the import for your push provider—FCM or APN, and // remove the other import statement import CioMessagingPushFCM import CioMessagingPushAPN import UserNotifications import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Because of the behavior of Notification Service Extensions in iOS, you need to // initialize the Customer.io SDK in your host app and in your Notification Service. // The last parameter optionally allows you to configure the SDK. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } // If you need to add features, like showing action buttons in your push, // you can set your own completion handler. MessagingPush.shared.didReceive(request) { notificationContent in if let mutableContent = notificationContent.mutableCopy() as? UNMutableNotificationContent { // Modify the push notification like adding action buttons! } contentHandler(notificationContent) } } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Your app can now display rich push notifications in your app, including images, etc. However, if you want to enable deep links, you should continue to the Deep links section below. Deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Set up Universal Links To enable Universal Links in your iOS app, follow the instructions on the Apple documentation website. Be sure to complete all of the steps required including making modifications to your website to host a new file and making modifications to your mobile app’s code to handle the deep link.  Install iOS SDK 2.0.6 or later Earlier versions of the SDK had an issue causing both your app and browser to open when users tapped a universal link. Depending on how you set up your mobile app (SwiftUI, UIKit, watchOS, etc), you may need to handle deep links in multiple functions in your code. One of the functions you are required to have in your app is: class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard let universalLinkUrl = userActivity.webpageURL else { return false } // Parse `universalLinkUrl` object to perform the action you want in your app. // return true from this function if your app handled the deep link. // return false from this function if your app did not handle the deep link and you want sdk to open the URL in a browser. } }  Check universal links using your Notes app Try creating a note with a universal link and tapping the link to double-check that the link opens in your app and not in a browser window. This is an easy way to make sure that you’ve set up universal links correctly. If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Setup App Scheme Deep Links Open your Xcode project and go to your project’s settings. Select your app Target, click the Info tab, and then click URL Types > to create a new URL Type. Enter a unique value for your app for URL Schemes. Capture push metrics Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. If you already configured rich push notifications, the SDK will automatically track opened and delivered events for push notifications originating from Customer.io. Otherwise, you can: Record push metrics with UserNotifications. Extract delivery ID and Delivery Token parameters directly. Capture push metrics with UserNotifications If you’re using a version of iOS that supports UserNotifications, you can track metrics using our UNNotificationContent helper. func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // This 1 line of code might be all that you need! MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If you use `UserNotifications` for more then Customer.io push notifications, you can check // if the SDK handled the push for you or not. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) if !handled { // Notification was *not* displayed by Customer.io. Handle the notification yourself. } } Extract delivery ID and token If you’re not using a version of iOS that supports UserNotifications, you should send the push metric manually by extracting the CIO-Delivery-ID and CIO-Delivery-Token parameters directly to track push metrics. guard let deliveryID: String = notificationContent.userInfo["CIO-Delivery-ID"] as? String, let deviceToken: String = notificationContent.userInfo["CIO-Delivery-Token"] as? String else { // Not a push notification delivered by Customer.io return } MessagingPush.shared.trackMetric(deliveryID: deliveryID, event: .delivered, deviceToken: deviceToken) Disable automatic push tracking Automatic push metric recording is enabled by default when you install the SDK. You can disable this behavior in the SDK’s configuration. CustomerIO.initialize(...) { config in config.autoTrackPushEvents = false } Test your push implementation After you set up push notifications, you should send some test messages. You can send messages through the Customer.io push composer. If your app is set up to use keys outside the standard ones that our SDK supports, you’ll want to send a custom payload. The payloads below represent what your app can expect to receive from Customer.io. If you use a custom payload, you’ll need to use the format(s) below to make sure that the SDK receives your message properly. When testing, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. APNS payload APNS payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. FCM payload FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "sound": "default", "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Sound in push notifications When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/ios/2.x/in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. To set up in app messaging, install and initialize the Tracking and MessagingInApp packages. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging Use Swift Package Manager to install the MessagingInApp package. See Getting Started for installation instructions. Initializing your app with the MessagingInApp package sets up your app to receive in-app messages. import CioMessagingInApp import CioTracking CustomerIO.initialize(siteId: workspaceId, apiKey: apiKey, region: Region.US) {} MessagingInApp.initialize() // (Optional) setup an event listener to get notified when a message is shown, dismissed, or an action is taken. MessagingInApp.initialize(eventListener: self) extension YourClass: InAppEventListener { func messageShown(message: InAppMessage) {} func messageDismissed(message: InAppMessage) {} func errorWithMessage(message: InAppMessage) {} func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {} } Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. You can add $0.autoTrackScreenViews = true to your CustomerIO.config to automatically track screens or you can track screens manually. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. The SDK automatically uses the class name of UIViewController, minus ViewController, as the name of each page. For example, if you wanted to display an in-app message on a class called EditProfileViewController, you would enter EditProfile as your page rule.  Make sure your screens use the same names across your apps If you have a screen called DashboardActivity in Android, and DashboardViewController in iOS, we’ll recognize Dashboard as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. 1 2 3 4 5 6 7 8 9 10 11 12 13 import CioMessagingInApp import CioTracking CustomerIO.initialize(siteId: workspaceId, apiKey: apiKey, region: Region.US) MessagingInApp.initialize(eventListener: self) extension YourClass: InAppEventListener { func messageShown(message: InAppMessage) {} func messageDismissed(message: InAppMessage) {} func errorWithMessage(message: InAppMessage) {} func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {} } Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. MessagingInApp.shared.dismissMessage() --- ## Test support URL: https://docs.customer.io/integrations/sdk/ios/2.x/test-support/ The SDK makes it easy to write unit, integration, UI, or other types of automated tests in your code base. We designed our SDK with first-class support for automated testing, making it easy to inject dependencies and perform mocking in your code. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> register-token(Register Device Token) register-token -.-> push(Receive push) register-token -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/ios/getting-started/#install" click B href "/integrations/sdk/ios/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/ios/identify" click track-events href "/integrations/sdk/ios/track-events/" click register-token href "/integrations/sdk/ios/push" click push href "/integrations/sdk/ios/push" click rich-push href "/integrations/sdk/ios/rich-push" click in-app href "/integrations/sdk/ios/in-app" click test-support href "/integrations/sdk/ios/test-support" style test-support fill:#B5FFEF,stroke:#007069 Dependency injection Every SDK class inherits from a Swift protocol. Inherited protocols use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. If you want to inject a class in your project, it could look something like the example below. import CioTracking class ProfileRepository { private let cio: CustomerIOInstance init(cio: CustomerIOInstance) { self.cio = cio } // Now, you can call any of the `CustomerIO` class functions with `self.cio`! func loginUser(email: String, password: String, onComplete: @escaping (Result<Success, Error>) -> Void) { // Login the user to your system... // Then, identify the profile with Customer.io: self.cio.identify(identifier: email) } } // Inject an instance of the `CustomerIO` class to your class: let cio = CustomerIO(...) let repository = ProfileRepository(cio: cio) Mocking The Customer.io SDK comes bundled with mock classes ready for you to use. That’s right, we generated mocks for you! Mock classes follow the naming convention: <NameOfClass>Mock. For example, mock the CustomerIO class with CustomerIOMock. Here’s an example test class showing how you would test your ProfileRepository class. import Foundation import CioTracking import XCTest class ProfileRepositoryTest: XCTestCase { private var cioMock: CustomerIOMock! private var repository: ProfileRepository! override func setUp() { super.setUp() cioMock = CustomerIOMock() // Create a new instance of the mock in setUp() to reset the mock. repository = ProfileRepository(cio: cioMock) } func test_loginUser() { // Now, call your function under test: repository.loginUser(...) // You can access many properties of the mock class to assert the behavior of the mock. XCTAssertTrue(cioMock.mockCalled) XCTAssertEqual(cioMock.identifyBodyCallsCount, 1) XCTAssertEqual(cioMock.identifyBodyReceivedInvocations[0].identifier, expectedIdentifier) } } --- ## Update from 2.10 to 2.11 URL: https://docs.customer.io/integrations/sdk/ios/2.x/update-210-to-211/ This page explains how to update to version 2.11 of our native iOS SDK. While these changes aren't breaking—you don't have to implement these changes—they will simplify your integration, improve the reliability of your metrics, and improve deep link handling. Updating your integration also sets you up for success in future releases. Upgrade from 2.10 to 2.11+ As of version 2.11, the Customer.io SDK automatically registers push device tokens to identified people and automatically handles push clicks. These features let you simplify your SDK integration while also improving the reliability of device tokens and opened metrics. After you install version 2.11 or later: Open your AppDelegate file and review the highlighted code in the sample below. You can remove all of the highlighted code from your app. You might need to leave some of these lines in your app depending on your app’s configuration. See comments in the code sample to determine whether you should delete the code or not. APN APN import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // Do not delete line if: you use push device tokens for more then Customer.io UIApplication.shared.registerForRemoteNotifications() // Do not delete line if: your app receives push notifications from services other then Customer.io. // Or, you display local notifications and you need to handle them getting clicked. UNUserNotificationCenter.current().delegate = self return true } // If you deleted the line, `UIApplication.shared.registerForRemoteNotifications()`, you can delete this function. func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } // If you deleted the line, `UIApplication.shared.registerForRemoteNotifications()`, you can delete this function. func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } // If you deleted the line, `UNUserNotificationCenter.current().delegate = self`, you can delete this `extension` block. extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // Send Customer.io SDK click event to process. This enables features such as // push metrics and deep links. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } @available(iOS 10.0, *) func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } } FCM FCM import CioMessagingPushFCM import CioTracking class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { FirebaseApp.configure() CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // Do not delete these 2 lines if: you use push device tokens for more then Customer.io Messaging.messaging().delegate = self UIApplication.shared.registerForRemoteNotifications() // Do not delete line if: your app receives push notifications from services other then Customer.io. // Or, you display local notifications and you need to handle them getting clicked. UNUserNotificationCenter.current().delegate = self return true } } // If you deleted the line, `Messaging.messaging().delegate = self`, you can delete this `extension` block. extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } // If you deleted the line, `UNUserNotificationCenter.current().delegate = self`, you can delete this `extension` block. extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // Send Customer.io SDK click event to process. This enables features such as // push metrics and deep links. let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } @available(iOS 10.0, *) func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## Migrate from an earlier version URL: https://docs.customer.io/integrations/sdk/ios/2.x/migrate-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 1.x to 2.x Singleton API is now enforced In version 1.x of the Customer.io iOS SDK, could use the SDK in 2 ways: Singleton API: CustomerIO.initialize(...) // initialize the singleton SDK instance Customer.shared.track(...) // use the singleton SDK instance Non-singleton API: let cio = CustomerIO(...) // initialize the non-singleton SDK instance cio.track(...) // use the non-singleton SDK instance In version 2.x, we removed the non-singleton API. To successfully migrate, you need to replace any code using a non-singleton with the singleton instance: // Replace the non-singleton instance: cio.track(...) messagingPush.application(...) // With `CustomerIO.shared` or `MessagingPush.shared`: CustomerIO.shared.track(...) MessagingPush.shared.application(...) If your app uses a technique like dependency injection, you can keep your code base as-is and simply replace code where you create new instances of the SDK: // For example, if you have code that accepts a CustomerIO dependency in the constructor (to easily allow mocking the Customer.io SDK): class Repository { let customerIO: CustomerIO init(customerIO: CustomerIO) { self.customerIO = customerIO } func acceptFriendRequest() { ... self.customerIO.track(...) ... } } // You can keep your Repository as-is, but you need to change where you create instances from: let repository = Repository(customerIO: CustomerIO(...)) repository.acceptFriendRequest() // To: let repository = Repository(customerIO: CustomerIO.shared) // Don't forget to initialize the SDK 😉 repository.acceptFriendRequest() Configuration of the SDK happens during initialization In version 1.x of the Customer.io iOS SDK, you configured the SDK through a .config function: CustomerIO.config { $0.autoTrackScreenViews = true } In version 2.x of the Customer.io iOS SDK, we moved the .config function into CustomerIO.initialize. You’ll need to move your configuration into the SDK initialization process to migrate: CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. SDK initialization: required parameters In version 1.x of the Customer.io SDK, the function CustomerIO.initialize contained optional parameters. We had to remove those and make all parameters required. To migrate to 2.x of the SDK, fill in the rest of the parameters in your initialize function: // v1.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY") // v2.x CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) {} // Optionally, if you want to configure settings of the SDK, do so in initialization. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU) { config in config.autoTrackScreenViews = true } Visit the getting started doc to learn more about SDK configuration. Rich push initialization If you have followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Because of the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your host app and in your Notification Service. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Cocoapods users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. This means that you need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. If you installed the Customer.io SDK using Swift Package Manager, this change does not effect you. We fixed a bug with custom attributes that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in v1 that converted keys in your custom data to snake_case. This bug is fixed in v2 of the iOS SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect our Android SDK.. // If you passed in custom attributes using camelCase keys: data = ["firstName": "Dana"] // The SDK v1 may have converted this data into: data = ["first_name": "Dana"] // Or, if you used a different format that was not snake_case: data = ["FIRSTNAME": "Dana"] // The SDK v1 may have converted this data into: data = ["f_irstname": "Dana"] You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Anywhere you call the Customer.io SDK and provide custom attributes like this: CustomerIO.shared.identify("dana@example.com", data: ["firstName": "Dana"]) // Consider sending duplicate data with snake_case CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. ]) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.shared.identify("dana@example.com", data: [ "firstName": "Dana", // We can remove the snake_case attribute and go back to just camelCase! ]) --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/ios/2.x/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Try running CIO SDK Tools Our CIO SDK Tools library can help diagnose problems with your SDK implementation. This is a node package that you can run from inside or outside your app’s project folder. After you install it, you can run the doctor command to check your SDK configuration and get tips to fix problems. npx cio-sdk-tools@latest doctor /path/to/project Capture logs Enable debug logging in your app: Everywhere you call CustomerIO.initialize(), enable the debug log level. This includes in the Notification Service Extension that you setup for rich push.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. // During SDK initialization, enable debug logs: CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.logLevel = .debug } Open the Console app (already installed in MacOS). This is a built-in application you can use to view logs produced by the SDK. We recommend that you use Console instead of Xcode to view and capture logs from the SDK because Xcode may not show you all of the logs the SDK generates. In Console, click Action > Include Info messages and Action > Include Debug messages. These settings ensure that you’ll see log messages from the SDK. In Console, on the left, select the iOS device that runs your app with the Customer.io SDK. If you don’t see your device listed, plug in your iOS device into your Mac. Try to use a direct connection via the Apple cable; using a USB hub might prevent the device from showing up. Then, click Start streaming. You will see hundreds or even thousands of logs printed to you. Most of these log messages are not relevant. In the next steps, we’ll filter your log to find relevant messages. In the top right search bar, type “CIO” and press Enter. Click the dropdown and select Category. You will now only see messages sent from the SDK. In the top right, click Save to save this filter. The next time you open Console, just click that saved filter along the top of the screen to see Customer.io SDK logs. Click any of the log entries on the screen (or Edit > Select All), CMD + C, then CMD + P into a text editor on your computer. Save the file as a .txt. Send the file you just saved to our support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Deep links only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. You can learn more about setting up universal links here. You can easily test universal links using your Notes app. Try adding a link to a note and tap it. If it drives you to your app, then you’ve set things up correctly! If your links are opening Safari instead of your app, check this Apple document to troubleshoot. Universal Link opens in browser instead of app If you click on a push notification sent by Customer.io that contains a Universal Link deep link > click on the push notification > app opens for a moment > then the browser opens the URL, this could be a sign that something is wrong with your app’s Universal Link handling. The Customer.io SDK sends a request to your app’s app to give your app an opportunity to handle the Universal Link. If your app does not handle the Universal Link, the SDK will open the link in the browser instead. Let’s walk through some troubleshooting steps to try and fix this behavior so the browser does not open. In our deep links Universal Links guide, we show a function that is required to be added to your app: application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool. Add a print("Universal Links handle code called.") statement or Xcode breakpoint to verify that your code in this function does get called. If you click on a push notification and you do not see this print statement or breakpoint hit, verify that the deep link URL is a valid https URL and you have followed all of the Apple documentation linked in our Universal Links guide. If you do see your print statement or breakpoint hit, then your Universal Link URL is valid and is correctly attached to the push notification for the SDK to understand. Next, verify that your app returns true from the application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool function. If your app returns false, the SDK will open the Universal Link in the browser instead of the app. Lastly, check if there is another SDK interfering with the Customer.io SDK. In some cases, customers have reported instances where Universal Links, despite being correctly configured within your app, may unexpectedly open in a web browser. This can occur due to interactions with third-party SDKs that perform method swizzling inside your app. To address this, consider reviewing the documentation of other SDKs integrated into your app and disabling swizzling as needed. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/ios/2.x/changelog/ Check out release history for stable releases of iOS SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/android/quick-start-guide/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup Process Overview For native Android apps, our Android SDK sets you up to send push notifications and track user activity. You’ll need to add Customer.io SDK to your project, configure it with your API key, and set up push notification handling. Then, you’ll use the SDK to identify users, track their activity, and respond to incoming notifications. Install the SDK. Identify and Track Push Notifications In-App 1. Install the SDK If you haven’t already gotten a CDP API key, you’ll need to add a new Android integration to your Customer.io workspace. This “integration” represents your app in Customer.io and provides the CDP API key that you’ll use to initialize the SDK. See Get your CDP API key for details. Ensure that your build.gradle or settings.gradle includes the following repositories to resolve dependencies: android { repositories { google() mavenCentral() } ... } Add the following dependencies to your build.gradle file to install Customer.io Android SDK: dependencies { implementation "io.customer.android:datapipelines:4.17.0" // Required for push notifications only implementation "io.customer.android:messaging-push-fcm:4.17.0" // Required for in-app messages only implementation "io.customer.android:messaging-in-app:4.17.0" } We recommend initializing Customer.io SDK in your app’s Application class in the onCreate method. This ensures the SDK is accessible throughout your app using CustomerIO.instance() method. Use CustomerIOBuilder to add and configure the modules you want to enable: Kotlin Kotlin import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOConfigBuilder import io.customer.sdk.data.model.Region val builder = CustomerIOConfigBuilder(applicationContext, "<CDP_API_KEY>") // If you're in the EU, set Region.EU. Default is Region.US and optional. .region(Region.US) // Optional: Enable in-app messaging by adding siteId and Region .addCustomerIOModule( ModuleMessagingInApp(MessagingInAppModuleConfig.Builder("<SITE_ID>", Region.US).build()) ) // Optional: Enable support for push notifications .addCustomerIOModule(ModuleMessagingPushFCM()) // Completes setup and initializes the SDK CustomerIO.initialize(builder.build()) Java Java import io.customer.messaginginapp.MessagingInAppModuleConfig; import io.customer.messaginginapp.ModuleMessagingInApp; import io.customer.messagingpush.ModuleMessagingPushFCM; import io.customer.sdk.CustomerIO; import io.customer.sdk.CustomerIOConfigBuilder; import io.customer.sdk.data.model.Region; CustomerIOConfigBuilder builder = new CustomerIOConfigBuilder(this, "<CDP_API_KEY>") .region(Region.US.INSTANCE) // Optional: Enable in-app messaging by adding siteId and Region .addCustomerIOModule( // If you're in the EU, set Region.EU new ModuleMessagingInApp(new MessagingInAppModuleConfig.Builder("<SITE_ID>", Region.US.INSTANCE).build()) ) // Optional: Enable support for push notifications .addCustomerIOModule(new ModuleMessagingPushFCM()); // Completes setup and initializes the SDK CustomerIO.initialize(builder.build()); 2. Identify and Track Identify a user in your app using the CustomerIO.instance().identify method. You must identify a user before you can send push notifications and personalized in-app messages. Kotlin Kotlin import io.customer.sdk.CustomerIO fun identifyUserExample() { CustomerIO.instance().identify( userId = "android-test-user@example.com", traits = buildMap { put("firstName", "John") put("lastName", "Doe") put("email", "android-test-user@example.com") put("subscriptionStatus", "active") }, ) Log.d("[CustomerIO]", "User identified successfully") } Java Java import io.customer.sdk.CustomerIO; void identifyUserExample() { String userId = "android-test-user@example.com"; Map<String, Object> traits = new HashMap<>(); traits.put("firstName", "John"); traits.put("lastName", "Doe"); traits.put("email", "android-test-user@example.com"); traits.put("subscriptionStatus", "active"); CustomerIO.instance().identify(userId, traits); Log.d("[CustomerIO]", "User identified successfully"); } Track a custom event using the CustomerIO.instance().track method. Events help you trigger personalized campaigns and track user activity. Kotlin Kotlin import io.customer.sdk.CustomerIO fun trackCustomEventExample() { CustomerIO.instance().track( name = "purchased_item", properties = buildMap { put("product", "Premium Subscription") put("price", 99.99) put("currency", "USD") } ) Log.d("[CustomerIO]", "Custom event tracked successfully") } Java Java import io.customer.sdk.CustomerIO; void trackCustomEventExample() { String name = "purchased_item"; Map<String, Object> properties = new HashMap<>(); properties.put("product", "Premium Subscription"); properties.put("price", 99.99); properties.put("currency", "USD"); CustomerIO.instance().track(name, properties); Log.d("[CustomerIO]", "Custom event tracked successfully"); } Track screen views to automatically trigger in-app messages associated with specific screens. Kotlin Kotlin import io.customer.sdk.CustomerIO fun trackScreenViewExample() { CustomerIO.instance().screen( title = "ProductDetails", properties = buildMap { put("product_id", "12345") put("product_name", "Sample Product") put("category", "Electronics") } ) Log.d("[CustomerIO]", "Screen view tracked successfully") } Java Java import io.customer.sdk.CustomerIO; void trackScreenViewExample() { String title = "ProductDetails"; Map<String, Object> properties = new HashMap<>(); properties.put("product_id", "12345"); properties.put("product_name", "Sample Product"); properties.put("category", "Electronics"); CustomerIO.instance().screen(title, properties); Log.d("[CustomerIO]", "Screen view tracked successfully"); } 3. Push Notifications Set up your push notification credentials by uploading your Firebase Cloud Messaging (FCM) service account key (.json file) in Customer.io dashboard Add the push messaging dependency to your project: implementation "io.customer.android:messaging-push-fcm:4.17.0" Make sure your app includes required Firebase configurations (like the google-services plugin and the google-services.json file) as described in the FCM documentation. Initialize and include the push module in CustomerIOBuilder when setting up the SDK: Kotlin Kotlin import io.customer.messagingpush.ModuleMessagingPushFCM addCustomerIOModule(ModuleMessagingPushFCM()) Java Java import io.customer.messagingpush.ModuleMessagingPushFCM; builder.addCustomerIOModule(new ModuleMessagingPushFCM()); Request push notification permissions from the user following Google’s recommendations. For more details and customization options, see Push Notifications. 4. In-App To enable in-app messaging, add your site ID and Region. You’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Add the in-app messaging dependency to your project: implementation "io.customer.android:messaging-in-app:4.17.0" Initialize and include the in-app module in CustomerIOBuilder when setting up the SDK: Kotlin Kotlin import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp import io.customer.messaginginapp.type.InAppEventListener import io.customer.messaginginapp.type.InAppMessage import io.customer.sdk.data.model.Region addCustomerIOModule( ModuleMessagingInApp( MessagingInAppModuleConfig.Builder( siteId = <SITE_ID>, // If you're in the EU, set Region.EU region = Region.US ) // Optional: Set in-app message event listener .setEventListener(object : InAppEventListener { ... }) .build() ) ) Java Java import io.customer.messaginginapp.MessagingInAppModuleConfig; import io.customer.messaginginapp.ModuleMessagingInApp; import io.customer.messaginginapp.type.InAppEventListener; import io.customer.messaginginapp.type.InAppMessage; import io.customer.sdk.data.model.Region; builder.addCustomerIOModule( // If you're in the EU, set Region.EU new ModuleMessagingInApp(new MessagingInAppModuleConfig.Builder(<SITE_ID>, Region.US.INSTANCE) // Optional: Set in-app message event listener .setEventListener(new InAppEventListener() { ... }) .build())); For more details and customization options, see In-App Messaging. --- ## How it works URL: https://docs.customer.io/integrations/sdk/android/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum requirements To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/android/getting-started/auth/ To use the SDK, you'll need to get two kinds of keys: A CDP *API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. To get your SDK keys and send data to the right places, you’ll need to set up your app as a data inAn integration that feeds data into Customer.io. integration in Customer.io, and route it to your workspace. The SDK lets you route data to any number of destinations, but you must connect it to your workspace destination to send data, like the people you identify, the events you track, and so on, to Customer.io. If you haven’t already set up your app as an integration in Customer.io, do that first. API Keys you’ll need API Key: This key, shown in code samples as cdpApiKey, lets you send data to Customer.io. You’ll need it to initialize the SDK. You’ll get this key when you set up your mobile app as a data inAn integration that feeds data into Customer.io. integration in Customer.io. Site ID: This key tells the SDK which workspace your messages come from. You’ll use it to initialize the MessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your mobile app’s integration card in Customer.io. If you haven’t already set up your Android integration in Customer.io, you’ll need to do that first. Go to Integrations and click Add Integration. On the Overview tab, select your Android integration. If you don’t see your Android integration, you’ll need to set one up. Go to Settings and find your API Key. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should keep the siteId that you used in previous versions as the migrationSiteId in your config. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ).addCustomerIOModule( ModuleMessagingInApp( config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US // Replace with Region.EU if your Customer.io account is in the EU. ).setEventListener(InAppMessageEventListener()).build() ) ).addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build())  You’re not done yet You still need your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials to initialize the MessagingInApp package and to support people updating your app from a previous version of Customer.io SDK. See Get your Site ID below. Set up a new integration in Customer.io If you don’t already have a write key, you’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select Android. Enter a Name for your integration, like “My Android App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Now the Integrations page shows that your Android integration is connected to your workspace. You can also connect your Android integration to other services if you want to send your mobile data to other places outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your Site ID to initialize the MessagingInApp package and send in-app messages from your workspace. If you’re upgrading from a previous version, my can also set your Site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the MessagingInApp package. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ).migrationSiteId("your_site_id") .addCustomerIOModule( ModuleMessagingInApp( config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US // Replace with Region.EU if your Customer.io account is in the EU. ).setEventListener(InAppMessageEventListener()).build() ) ) .addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build()) Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/android/getting-started/packages-options/ The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use. SDK packages To minimize our SDK’s impact on your app’s size, we’ve split the SDK into packages. You can limit your install to the packages that you need for your project. But, in most cases, you’ll want to install all the packages to get the most value out of Customer.io. You must install the datapipelines package. It lets you identify people, which you must do before you can send them messages, etc. You’ll want to add the messaging-push-fcm and the messaging-in-app packages to send push notifications and in-app messages respectively. Package Product Required? Description datapipelines ✅ Identify people, track events, track anonymous activity messaging-push-fcm Receive push notifications over Google Firebase Cloud Messaging (FCM) messaging-in-app Receive in-app notifications location Enrich user profiles with accurate device location Configuration options You’ll call configuration options before you initialize the SDK. In most cases, you’ll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app—as shown in the example below. If you’re in our EU region, you must set Region.EU. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ).region(Region.US) .autoTrackDeviceAttributes(true) .autoTrackActivityScreens(false) .screenViewUse(ScreenView.All) .logLevel(CioLogLevel.DEBUG) .addCustomerIOModule( ModuleMessagingInApp( config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US ).setEventListener(InAppMessageEventListener()).build() ) ) .addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build()) Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region Region.EU or Region.US Region.US Because we default to the US region, you must set this to Region.EU if your account is in the EU region. apiHost string The domain you’ll proxy requests through. You’ll only need to set this (and cdnHost) if you’re proxying requests. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackActivityScreens boolean false If true, the SDK automatically sends screen events for every screen your audience visits. If you use Jetpack Compose you should set this to false and track screens manually. cdnHost string The domain you’ll fetch configuration settings from. You’ll only need to set this (and apiHost) if you’re proxying requests. logLevel string error Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. migrationSiteId string Required if you're updating from 2.x: the credential for previous versions of the SDK. This key is used to send remaining tasks to Customer.io when your audience updates your app. screenViewUse All or InApp All td style="text-align:left">ScreenView.All (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events Proxying requests By default, requests go through our domain at cdp.customer.io. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores. To proxy requests, you’ll need to set the apiHost and cdnHost properties in your SDKConfigBuilder. While these are separate settings, you should set them to the same URL. While you need to initialize the SDK with a cdpApiKey, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the cdpApiKey to a value representing basic authentication credentials that you handle on your own. See proxying requests for more information. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ) .region(Region.US) // Optional but recommended .apiHost("your-proxy.example.com") .cdnHost("your-proxy.example.com") .addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build()) --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/android/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with troubleshooting your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel(CiologLevel.ERROR). This can help you (or us) pinpoint problems.  Don’t use debug logging in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you don’t set the loglevel parameter (which defaults to CiologLevel.ERROR) in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Examine calls in the integrations tab Under Integrations, we’ll show you the calls that come in from your SDK and how we interpret those calls for each destination. You can use this information to pinpoiunt problems in your integration. If you have a problem, you may want to go to Integrations and check: That your Android integration is connected to your Customer.io workspace. If you don’t connect your integration to your workspace, you won’t be able to send messages, etc. Your integration’s Data In tab to make sure that your app sends the right data. The Data Out tab for any data outAn integration that sends data out of Customer.io. integrations to make sure that you’re sending the right data from your SDK to the destination. This includes Customer.io: your workspace is one of the places you’ll send data from your app! Check out our Integrations troubleshooting page for more help pinpointing issues in your integration. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Share your push or in-app payload: Knowing what images you used, the shape of your payload, and so on helps us reproduce the issue and figure out exactly what went wrong. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Capture logs Logs help us pinpoint the problem and find a solution. To capture logs from the Customer.io SDK: Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ).logLevel(CioLogLevel.DEBUG) CustomerIO.initialize(builder.build()) In Android Studio, build and run your app on a physical device or emulator. Select View > Tool Windows > Logcat. This shows you your device’s logs. Filter for CIO in the top to find log messages specific to the Customer.io SDK. Save your log and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Push notification issues Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/android/tracking/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: userId (required): The unique value representing a person—an ID or email address that represents a person in Customer.io (and your downstream destinations). traits (Optional): Contains attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to set for a person. The traits object accepts strings, enums, primitives (int, float, char, etc.), their boxed counterparts (Integer, Float, Character, etc.), arrays, collections, lists, sets, and maps. We also offer a Kotlin serialization library that can help make it easier to set keys and values for the traits object. CustomerIO.instance() .identify( userId = "USER_ID", traits = mapOf("first_name" to "firstName") ) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with profileAttributes. CustomerIO.instance().setProfileAttributes(mapOf("favorite_food" to "pizza")) You only need to pass the attributes that you want to set. For example, if you identify a new person with the attribute "first_name": "Dana", and then you call CustomerIO.instance().setProfileAttributes(mapOf("favorite_food" to "pizza")), the person’s first_name attribute will still be Dana. And Dana will now have a favorite_food attribute with the value pizza. Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.instance().setDeviceAttributes(mapOf("key" to "value")) However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to a person more broadly. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. // set before you build builder.autoTrackDeviceAttributes(false) Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Future calls to the SDK are anonymous CustomerIO.instance().clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/android/tracking/screen-events/ Screen events track the screens people view in your app. In addition to tracking the parts of your app people use, screen tracking is vital for supporting in-app messages. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your app. You can turn on automatic screen tracking by appending autoTrackActivityScreens(true) to CustomerIOBuilder. When automatically tracking screen events, we capture the name of the screen with the following priority from highest to lowest: We check if the current Activity has a label in the manifest file. If it does, the SDK will use the value for label. We get the class name of the Activity and use that value. The SDK will take whatever value it receives and will strip the word Activity from it. Example: If you have an Activity with the manifest label or class name ProfileActivity, the SDK will track the screen view with the name Profile. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key", ).autoTrackActivityScreens(true) CustomerIO.initialize(builder.build()) If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can send screen events manually. Manually track screen events Screen events use the .screen method. Like other events, you can add a map of properties object containing additional information about the screen event or the currently-identified person. CustomerIO.instance().screen( name = "baseballDailyScores", properties = mapOf("prevScreen" to "homescreen", "secondsInApp" to 120) ) ScreenView Settings Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.InApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key" ).region(Region.US) .screenViewUse(ScreenView.InApp) .addCustomerIOModule( ModuleMessagingInApp( config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US ).setEventListener(InAppMessageEventListener()).build() ) ) .addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build()) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/android/tracking/lifecycle-events/ By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. CustomerIO.instance().track( name = "Application Crashed", properties = mapOf("url" to "/page/in/app") ) Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the setTrackApplicationLifecycleEvents option to the SDK’s config builder. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key" ).trackApplicationLifecycleEvents(false) CustomerIO.initialize(builder.build()) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/android/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their screen view. This triggers the campaign you set up for people who visited the product page. You can return a person’s anonymous ID at ay time by calling CustomerIO.instance().anonymousId. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Track events URL: https://docs.customer.io/integrations/sdk/android/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track a custom event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.instance().track( name = "purchase", properties = mapOf("product" to "socks", "price" to "4.99") )  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.instance().track( name = "User Deleted ) --- ## Location tracking URL: https://docs.customer.io/integrations/sdk/android/tracking/location/ Real-time location tracking lets you update a person's profile with accurate coordinates so you can send geo-aware messages and segment users by location. How it works The Location module captures location (with user consent) from your app and attaches it to a person’s profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than IP-based geolocation. When you identify a person, the SDK includes the latest location in the identify call. The SDK also sends a Location Update event to the person’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update. The SDK does not request location permission on its own—your app must handle the permission flow. Install the location module To use location tracking, add the location dependency to your app’s build.gradle file. implementation 'io.customer.android:location:<version-here>' Initialize the SDK with the location module Add ModuleLocation when you initialize the SDK. The module takes a LocationModuleConfig where you set the tracking mode. Option Type Default Description trackingMode LocationTrackingMode MANUAL Controls how and when the SDK captures location. See tracking modes below. Tracking modes Mode Description MANUAL Your app controls when it caputres location. Call setLastKnownLocation() or requestLocationUpdate() to provide location. You should use this option when your app already has a location-tracking mechanism or you want full control over when you capture location data. ON_APP_START The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call setLastKnownLocation() or requestLocationUpdate() alongside automatic capture. Use this for hands-off location tracking with minimal battery impact. OFF Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime. Kotlin Kotlin val config = CustomerIOConfigBuilder(applicationContext, "your-cdp-api-key") .addCustomerIOModule( ModuleLocation( LocationModuleConfig.Builder() .setLocationTrackingMode(LocationTrackingMode.MANUAL) .build() ) ) .build() CustomerIO.initialize(config) Java Java CustomerIOConfigBuilder builder = new CustomerIOConfigBuilder(application, "your-cdp-api-key"); builder.addCustomerIOModule( new ModuleLocation( new LocationModuleConfig.Builder() .setLocationTrackingMode(LocationTrackingMode.MANUAL) .build() ) ); CustomerIO.initialize(builder.build()); Location APIs The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a Location Update event no more than once a day—and only if the person has moved a meaningful distance. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates. setLastKnownLocation Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io. Parameter Type Description latitude Double Latitude in degrees. Must be between -90 and 90. longitude Double Longitude in degrees. Must be between -180 and 180. You can also pass an Android Location object directly. Kotlin Kotlin // From coordinates ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194) // From an Android Location object ModuleLocation.instance().locationServices.setLastKnownLocation(androidLocation) Java Java // From coordinates ModuleLocation.instance().getLocationServices().setLastKnownLocation(37.7749, -122.4194); // From an Android Location object ModuleLocation.instance().getLocationServices().setLastKnownLocation(androidLocation); requestLocationUpdate Request a one-shot location from the SDK using Google’s Fused Location Provider. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user. If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes. Add the permission to your AndroidManifest.xml: <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- Optional: for more precise location --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> Request permission at runtime and call the SDK: Kotlin Kotlin val launcher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { ModuleLocation.instance().locationServices.requestLocationUpdate() } } launcher.launch(Manifest.permission.ACCESS_COARSE_LOCATION) Java Java ActivityResultLauncher<String> launcher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), granted -> { if (granted) { ModuleLocation.instance().getLocationServices().requestLocationUpdate(); } } ); launcher.launch(Manifest.permission.ACCESS_COARSE_LOCATION); Profile switch behavior When you call clearIdentify(), the SDK clears cached location data so that one person’s location doesn’t carry over to another person’s profile. The next person you identify starts with a clean slate. Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next identify() call includes it automatically. --- ## Push notifications URL: https://docs.customer.io/integrations/sdk/android/push/push/ Get started setting up push notifications for Android. Our Android SDK supports push notifications over FCM, including rich push messages with links and images. Before you begin This page explains how to receive rich push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through Firebase Cloud Messaging (FCM). How it works Before a device can receive a push notification, you must: Set up FCM. Set up push. Identify a person. When someone starts the app, they automatically generate a device token. Identifying the person associates the device token with the person in Customer.io, so that they can receive push notifications. Set up a campaign to send a push notification through the Customer.io composer. Set up push You must implement the Push Messaging SDK to use push notification features. implementation 'io.customer.android:messaging-push-fcm:4.17.0' Initialize the push module. The push module has an optional config object, explained below. See deep links for help configuring links. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key" ).autoTrackActivityScreens(true) .addCustomerIOModule( ModuleMessagingPushFCM() ) .region(Region.US) CustomerIO.initialize(builder.build()) The SDK adds a FirebaseMessagingService to the app manifest automatically, so you don’t have to perform additional setup to handle incoming push messages. However, if your application implements its own FirebaseMessagingService, make sure that when you call onMessageReceived and onNewToken methods, you also call CustomerIOFirebaseMessagingService.onMessageReceived and CustomerIOFirebaseMessagingService.onNewToken respectively. class FirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { val handled = CustomerIOFirebaseMessagingService.onMessageReceived(context, message) if (handled) { logger.breadcrumb(this, "Push notification has been handled", null) } } override fun onNewToken(token: String) { CustomerIOFirebaseMessagingService.onNewToken(context, token) } Push notifications launched from the SDK are currently posted to our default channel—[your app name] Channel. In the future, we plan to let you customize channels/categories so that users can subscribe and unsubscribe to content categories as necessary. Push module configuration ModuleMesagingPushFCM has an optional configuration object. In most cases, our default configuration works, but you can pass the configuration object to customize the way you handle push notifications and so on. Config option Default Description notificationCallback null A callback that notifies the client on push notification related actions. This lets you override the default behavior for push notifications. autoTrackPushEvents true Boolean: when true, the SDK automatically tracks push events like delivered and opened. pushClickBehavior ACTIVITY_RESTART Lets you customize the behavior when a user taps a push notification. See push click behavior. val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key", ).autoTrackActivityScreens(true) .addCustomerIOModule( ModuleMessagingPushFCM( moduleConfig = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) }.build() ) ) .region(Region.US) CustomerIO.initialize(builder.build()) Push click behavior The pushClickBehavior config lets you customize your application’s response when your audience taps a push notification. This includes going to specific deep links or launcher screens based on the notification payload. Note that the SDK tracks opened metrics for all click behaviors. builder.addCustomerIOModule( ModuleMessagingPushFCM( moduleConfig = MessagingPushModuleConfig.Builder().apply { setPushClickBehavior(PushClickBehavior.ACTIVITY_PREVENT_RESTART) }.build() ) ) The available options are: ACTIVITY_PREVENT_RESTART (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ACTIVITY_NO_FLAGS: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. RESET_TASK_STACK: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. Capture push metrics Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. By default, the messaging-push-fcm package automatically tracks opened and delivered for push notifications originating from Customer.io. Otherwise, you can track push metrics with the trackMetric method. CustomerIO.instance().trackMetric( deliveryID = deliveryId, deviceToken = deliveryToken, event = MetricEvent.delivered ) Customizing Push Notifications You can customize the icon and color of push notifications by updating your AndroidManifest as recommended by FCM. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> However, if you want more control over your notifications’ appearance and behavior, Customer.io SDK provides an option to override these settings on the app side. You can customize notification appearance by implementing CustomerIOPushNotificationCallback and overriding the onNotificationComposed method. class MainApplication : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key", ).autoTrackActivityScreens(true) .addCustomerIOModule( ModuleMessagingPushFCM( moduleConfig = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) }.build() ) ) CustomerIO.initialize(builder.build()) } override fun onNotificationComposed( payload: CustomerIOParsedPushPayload, builder: NotificationCompat.Builder ) { // Customize your notification here } } You cannot override PendingIntent for notifications. If you want to override the behavior when people tap your notifications, you can implement onNotificationClicked as described in our deep links documentation. The push notification icon You’ll set the icon that appears on normal push notifications as a part of your app manifest. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/android/push/deep-links/ Deep links let you send people who interact with your messages to links in your app. You should set up deep links to make sure that your push notifications are actionable, and take people to screens that matter to your audience. Our SDK supports redirects for notification links registered in Android by default. You can customize this behavior using CustomerIOPushNotificationCallback. To register a deep link, you must first add intent filters in your AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "remote-habits://settings” --> <data android:host="settings" android:scheme="remote-habits" /> </intent-filter> CustomerIOPushNotificationCallback—a URL handler feature provided by the SDK. When configuring your CustomerIO instance, you can set the callback to handle notification behavior. Note that the if statement (payload.deeplink.doesNotMatch()) in the example below is just an example of what you might do if you handle some links with our SDK and handle others yourself. class MainApplication1 : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key" ).autoTrackActivityScreens(true) .addCustomerIOModule( ModuleMessagingPushFCM( moduleConfig = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) }.build() ) ) CustomerIO.initialize(builder.build()) } override fun onNotificationClicked(payload: CustomerIOParsedPushPayload, context: Context): Unit? { // This if statement is an example of what you might do to handle // some links with our SDK and others yourself. if (payload.deepLink.doesNotMatch()) { // Return null so CustomerIO SDK can handle Notification Clicked return null } // Custom handling of Notification Clicked return Unit } }  When someone taps a push notification with a deep link, the SDK calls the CustomerIOPushNotificationCallback specified in CustomerIOBuilder object before it looks for default supported links. The onNotificationClicked function lets you override the SDK’s default click handler. If you return null, the SDK will handle the click normally, opening the link in your app or web browser. Otherwise, you can handle the click yourself using the payload and context parameters.  Don’t forget to capture metrics When you provide CustomerIOPushNotificationCallback, don’t forget to capture notification metrics, otherwise our dashboards won’t record conversions properly! --- ## Channel URL: https://docs.customer.io/integrations/sdk/android/push/push-notification-channel/ Learn how to customize your push notification channel in your app's manifest. 🎉New in v4.7.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io Android SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io Android SDK prior to 4.7.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io Android SDK v4.7.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v4.7.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## Push service certificates URL: https://docs.customer.io/integrations/sdk/android/push/push-certificates/ Before you can send push notifications, you'll need to add your Firebase Cloud Messaging credentials to Customer.io. This authorizes us to send push notifications to your Android app. Upload your push certificate If you don’t already have your FCM .JSON file, you’ll need to get it before you can finish this process and send push notifications. In Customer.io, go to Settings > Workspace Settings and click Settings next to Push. For iOS, click Enable, and select the Firebase Cloud Messaging (FCM) option. Get your .JSON file for FCM Before you can get a push certificate for Firebase Cloud Messaging, make sure that the FCM API is enabled for your project. You can check that here. Log into the Firebase Console for your project. Click in the sidebar and go to Project settings. Go to Service Accounts and click Generate New Private Key. Confirm your choice and download the credential file. --- ## Test your push implementation URL: https://docs.customer.io/integrations/sdk/android/push/test-push/ Before you send push notifications, you should test your implementation to make sure it works as expected. This is what the payload looks like on our end. If you’ve set up your app to use other data—custom keys outside the scope of our SDK—you can use our Custom Payload Editor; you’re welcome to place custom keys inside the message.data object, but you’ll need to do additional development to support keys beyond our standard title, body, link, and image. { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/android/in-app/in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. How it works An in-app message is a message that people see within the app. To set up in app messaging, install and initialize the tracking and messaging-in-app packages. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to implement screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Install the SDK and in-app module Make sure that you add update your repositories in the settings.gradle file to include the in-app SDK. dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // only needed for in-app messaging in SDK versions below 3.6.1 // maven { url 'https://maven.gist.build' } } } Implement the messaging-in-app package. implementation "io.customer.android:messaging-in-app:4.17.0" Initialize the SDK with the in-app module Simply initialize the SDK with the MessagingInApp module and your app will be able to receive in-app messages. Create a campaign and send your first in-app message to test your implementation! val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key" ).addCustomerIOModule(ModuleMessagingPushFCM()) .addCustomerIOModule( ModuleMessagingInApp( config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US) .setEventListener(object : InAppEventListener { override fun errorWithMessage(message: InAppMessage) {} override fun messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {} override fun messageDismissed(message: InAppMessage) {} override fun messageShown(message: InAppMessage) {} }) .build() ) ) .logLevel(CioLogLevel.DEBUG) // For fragment-based apps or Jetpack Compose, disable this and use manual screen tracking .autoTrackActivityScreens(true) .region(Region.US) CustomerIO.initialize(builder.build()) Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Enable screen tracking in the SDK. If you are using a view(xml)-based() UI with activities, you can enable automatic screen tracking by calling autoTrackActivityScreens(true) during SDK initialization. However, if your app is fragment-based or utilizes Jetpack Compose, we recommend utilizing manual screen tracking with the screen method. Provide page names to whomever sets up in-app messages in fly.customer.io. The SDK automatically uses label in your manifest file as the page/screen name. If your screens don’t have labels, you won’t be able to set up page rules and screenview events will have an empty event name. You should inform anybody creating in-app messages about page names if you want to set up page rules, to make sure that your messages appear on the right pages of your app. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. Anonymous messages As of version 4.12, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/android/in-app/in-app-event-listeners/ When people receive an in-app message, you'll listen for events to handle the message's lifecycle—like dismissing the event or taking a custom action. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your-cdp-api-key" ).addCustomerIOModule( ModuleMessagingInApp(config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US) .setEventListener(object : InAppEventListener { override fun messageShown(message: InAppMessage) { trackInAppEvent("messageShown", message) } override fun messageDismissed(message: InAppMessage) { trackInAppEvent("messageDismissed", message) } override fun errorWithMessage(message: InAppMessage) { trackInAppEvent("errorWithMessage", message) } override fun messageActionTaken( message: InAppMessage, actionValue: String, actionName: String ) { trackInAppEvent( "messageActionTaken", message, hashMapOf("action-value" to actionValue, "action-name" to actionName) ) } }) .build() ) ) CustomerIO.initialize(builder.build()) private fun trackInAppEvent( eventName: String, message: InAppMessage, arguments: Map<String, String>? = null ) { CustomerIO.instance().track( "in-app message action", HashMap<String, String>().apply { arguments?.let { putAll(it) } put("event-name", eventName) put("message-id", message.messageId) put("delivery-id", message.deliveryId ?: "NULL") } ) } Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.instance().inAppMessaging().dismissMessage() --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/android/in-app/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. Install dependencies Add the appropriate dependency to your build.gradle file based on your UI framework: dependencies { // For Android XML views implementation "io.customer.android:messaging-in-app:4.17.0" // For Jetpack Compose (includes messaging-in-app internally) implementation "io.customer.android:messaging-in-app-compose:4.17.0" }  If you’re using Jetpack Compose, you only need the messaging-in-app-compose dependency. It includes the standard in-app messaging functionality internally.  For more information about installing and configuring the Customer.io SDK, see our Quick Start Guide. 1. Add View to your app UI to support inline messages You’ll need to include a UI element in your app UI to render inline messages. Avoid setting a fixed height on this view as its height will automatically adjust when messages are loaded or interacted with.  We’ve set up examples in our sample apps that might help if you want to see a real-world implementation of this feature. Android XML Android XML Open your layout XML file and add InlineInAppMessageView to your layout as shown in the example below. Set up layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See view layout for more information. Set the elementId in your XML or programmatically in your activity/fragment. This ID is used in Customer.io UI to target this view when sending an in-app message. <io.customer.messaginginapp.ui.InlineInAppMessageView android:id="@+id/example_in_app_message" android:layout_width="match_parent" android:layout_height="wrap_content" app:elementId=<example-element-id> /> Or you can set the elementId directly in code as shown below: val inlineView = findViewById<InlineInAppMessageView>(R.id.example_in_app_message) inlineView.elementId = <example-element-id> Jetpack Compose Jetpack Compose Add the InlineInAppMessage to your UI as shown in the example below. Set layout modifiers to position the view. Avoid setting a fixed height, InlineInAppMessage will adjust its height automatically when messages load or are interacted with. See view layout for more information. Set the elementId in your composable. This ID is used in Customer.io UI to target this view when sending an in-app message. import io.customer.messaginginapp.compose.InlineInAppMessage InlineInAppMessage( elementId = <example-element-id>, modifier = Modifier.fillMaxWidth() ) Set up layout constraints for your message Inline message views automatically adjust their height at runtime when messages load or users interact with them. Use wrap_content for the view’s height. This lets the view resize itself dynamically. You’re responsible for setting layout constraints or modifiers to position your view correctly (like start, top, or end). You shouldn’t use a fixed height, as it might interfere with message rendering. If you’re using XML, Android Studio might show warnings if you don’t set a height. The wrap_content setting satisfies these warnings without breaking functionality. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. If the editor says that the inline display feature is Web/iOS only, don’t worry about that. We’re working on updating this UI. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow these steps to implement custom action buttons for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events There are two ways to listen for these click events in inline in-app messages. Register a delegate with your inline view: Android XML Android XML import io.customer.messaginginapp.type.InAppMessage import io.customer.messaginginapp.type.InlineMessageActionListener import io.customer.messaginginapp.ui.InlineInAppMessageView class InlineExamplesActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val inlineView = findViewById<InlineInAppMessageView>(R.id.example_in_app_message) inlineView.setActionListener(MyInlineMessageActionListener()) } } class MyInlineMessageActionListener : InlineMessageActionListener { override fun onActionClick(message: InAppMessage, actionValue: String, actionName: String) { // Perform some logic when people tap an action button. // Example code handling button tap: when (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. "enable-auto-renew" -> { // Perform the action to enable auto-renew enableAutoRenew(actionName) } // You can add more cases here for other actions else -> { // Handle unknown actions or do nothing print("Unknown action: $actionValue") } } } } Jetpack Compose Jetpack Compose import io.customer.messaginginapp.compose.InlineInAppMessage import io.customer.messaginginapp.type.InAppMessage import io.customer.messaginginapp.type.InlineMessageActionListener @Composable fun InlineInAppMessageExample() { InlineInAppMessage( elementId = <example-element-id>, modifier = Modifier .fillMaxWidth(), progressTint = Color(0xFF03DAC5), // Optional: set custom progress color if needed onAction = { message: InAppMessage, actionValue: String, actionName: String -> // Perform some logic when people tap an action button. // Example code handling button tap: when (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. "enable-auto-renew" -> { // Perform the action to enable auto-renew enableAutoRenew(actionName) } // You can add more cases here for other actions else -> { // Handle unknown actions or do nothing print("Unknown action: $actionValue") } } } ) } Register a global SDK event listener. When you register an event listener with the SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic if you want. Handle responses to messages (event listeners) Similar to modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionDelegate set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! --- ## Page rules URL: https://docs.customer.io/integrations/sdk/android/in-app/target-in-app-messages/ Sending people in-app messages often depends on the screens they visit in your app. You can set page rules when you create in-app messages. These rules determine the pages that your audience must visit in your app to see each message. Before you can take advantage of page rules, you need to: Track screens in your app. You can add autoTrackActivityScreens(true) to your CustomerIO configuration to automatically track screens or you can track screens manually. If you use Jetpack Compose you should turn off automatic screen tracking and track screens manually. Provide screen names to whomever sets up in-app messages in the Customer.io UI. If we don’t recognize the page that you set for a page rule, your audience will never see your message. The SDK automatically uses label in your manifest file as the page/screen name. If your screens don’t have labels, you won’t be able to set up page rules and screenview events will have an empty event name.  Make sure your screens use the same names across your apps If you have a screen called DashboardActivity in Android, and DashboardViewController in iOS, we’ll recognize Dashboard as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## Notification inbox URL: https://docs.customer.io/integrations/sdk/android/in-app/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io delivers inbox messages as JSON payloads, not fully-rendered messages. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message. Get the inbox instance You’ll access inbox functionality through the inbox() method on the in-app messaging module. val inbox = CustomerIO.instance().inAppMessaging().inbox() Inbox methods The inbox instance provides several methods to manage messages. Method Description getMessages(topic?) Suspend function to get messages from the inbox. Optionally filter by topic. Returns a list of messages. fetchMessages(callback) Fetch messages from the inbox with a callback. Returns a result with messages on success or error on failure. fetchMessages(topic, callback) Fetch messages filtered by topic with a callback. Returns a result with messages on success or error on failure. addChangeListener(listener) Add a listener to be notified when messages change. removeChangeListener(listener) Remove a previously added change listener. markMessageOpened(message) Mark a message as opened. markMessageUnopened(message) Mark a message as unopened. markMessageDeleted(message) Mark a message as deleted. trackMessageClicked(message, actionName?) Track a click on the message. The actionName parameter is optional. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Fetch messages // Using suspend function (recommended) val messages = notificationInbox.getMessages() // Fetch messages filtered by topic val promoMessages = notificationInbox.getMessages(topic = "promotions") // Using callback notificationInbox.fetchMessages { result -> runOnUiThread { result.onSuccess { messages -> // Update your UI with the messages updateInboxUI(messages) }.onFailure { error -> // Handle error Log.e("Inbox", "Failed to fetch messages", error) } } } // Using callback with topic filter notificationInbox.fetchMessages("promotions") { result -> runOnUiThread { result.onSuccess { messages -> // Update your UI with filtered messages updatePromotionsUI(messages) }.onFailure { error -> Log.e("Inbox", "Failed to fetch messages", error) } } } Listen for message updates // Create a change listener val notificationInboxChangeListener = object : NotificationInboxChangeListener { override fun onMessagesChanged(messages: List<InboxMessage>) { // Update your UI with the new messages updateInboxUI(messages) } } // Add the listener notificationInbox.addChangeListener(notificationInboxChangeListener) // Don't forget to remove the listener when you're done notificationInbox.removeChangeListener(notificationInboxChangeListener) Mark messages as opened or unopened // Mark a message as opened notificationInbox.markMessageOpened(message) // Mark a message as unopened notificationInbox.markMessageUnopened(message) Track message clicks // Track a click without an action name notificationInbox.trackMessageClicked(message) // Track a click with an action name notificationInbox.trackMessageClicked(message, "view_order") Delete messages // Mark a message as deleted notificationInbox.markMessageDeleted(message) Working with message properties You can access message properties to display custom content in your inbox: // Access message properties val title = message.properties["title"] as? String val body = message.properties["body"] as? String val link = message.properties["link"] as? String val imageUrl = message.properties["image"] as? String // Handle message action when user taps fun handleMessageTap(message: InboxMessage) { // Mark as opened notificationInbox.markMessageOpened(message) // Track the click notificationInbox.trackMessageClicked(message) // Open link if available val link = message.properties["link"] as? String if (link != null) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) startActivity(intent) } } --- ## 4.x -> 4.10 URL: https://docs.customer.io/integrations/sdk/android/whats-new/4.10-upgrade/ This page details changes from the previous major version of the SDK to this minor update, so you understand the development effort required to update your app and take advantage of the latest features. What changed? The changes in this update are mainly to align our APIs across different platforms. You should not experience changes to functionality or features. Changes to initialization CustomerIOBuilder is deprecated. You should use CustomerIOConfigBuilder instead. Before CustomerIOBuilder( applicationContext = this, // new credentials cdpApiKey = "your_cdp_api_key" migrationSiteId = "your_site_id" ).apply { // If you're in the EU, set Region.EU region(Region.US) addCustomerIOModule( ModuleMessagingInApp( // the in-app module now has its own configuration config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US ).setEventListener(InAppMessageEventListener()).build() ) ) addCustomerIOModule(ModuleMessagingPushFCM()) build() } After val builder = CustomerIOConfigBuilder( applicationContext = this, cdpApiKey = "your_cdp_api_key", ).region(Region.US) // Add migrationSiteId only if you had it before .migrationSiteId("your_site_id") .addCustomerIOModule( ModuleMessagingInApp( // the in-app module now has its own configuration config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US ).setEventListener(InAppMessageEventListener()).build() ) ) .addCustomerIOModule(ModuleMessagingPushFCM()) CustomerIO.initialize(builder.build()) Changes when you track users, events, and screens You’ll need to update the way you identify users and track events, including screen tracking. Identifying a user These variants of identify are deprecated: identify(userId: String, traits: T) where traits are a generic type identify(userId: String, traits: JsonObject) identify(userId: String, traits: Traits, serializationStrategy: SerializationStrategy<Traits>) where traits are a generic type You should use this instead: identify(userId: String, traits: Map<String, Any?>) CustomerIO.instance().identify(userId, mapOf("name" to "John Doe")) Tracking an event These variants of track are deprecated: track(name: String, properties: T) where traits are a generic type track(name: String, properties: JsonObject) track(name: String, properties: Traits, serializationStrategy: SerializationStrategy<Traits>) where traits are a generic type You should use this instead: track(name: String, properties: Map<String, Any?>) CustomerIO.instance().track("clicked_button", mapOf("button_name" to "Login")) Screen tracking These variants of screen are deprecated: screen(title: String, properties: T) where traits are a generic type screen(title: String, properties: JsonObject) screen(title: String, properties: Traits, serializationStrategy: SerializationStrategy<Traits>) where traits are a generic type You should use this instead: screen(title: String, properties: Map<String, Any?>) CustomerIO.instance().screen("Home", mapOf("login" to true)) Profile and device attribute changes You’ll need to update the way you set profile and device attributes. profileAttributes is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s profile attributes Setter is replaced with setProfileAttributes(attributes: Map<String, Any>) CustomerIO.instance().setProfileAttributes(mapOf("name" to "John Doe")) deviceAttributes is deprecated Getter has no replacement, the mobile SDK doesn’t expose the user’s device attributes Setter is replaced with setDeviceAttributes(attributes: Map<String, Any>) CustomerIO.instance().setDeviceAttributes(mapOf("device_id" to "1234567890")) --- ## 3.x -> 4.x URL: https://docs.customer.io/integrations/sdk/android/whats-new/4.x-upgrade/ This page details breaking changes from the previous major version of the SDK, so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. When you’re done, you’ll be able to use your app data’s in both Customer.io and other downstream destinations—like your analytics platform, data warehouse, or CRM. All that and you’ll be prepared to accept new features and improvements that we roll out in the future! Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new integration in Customer.io and get the CDP API Key you’ll use to initialize the SDK. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select Android. Enter a Name for your integration, like “My Android App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Now the Integrations page shows that your Android integration is connected to your workspace. You can also connect your Android integration to other services if you want to send your mobile data to other places outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Import datapipelines instead of tracking We’ve replaced the tracking package with datapipelines. You’ll need to update your import statements to reflect this change. // replace `tracking` with: implementation 'io.customer.android:datapipelines:4.17.0' 3. Update your initialization You’ll initialize the new version of the SDK and its packages with SDKConfigBuilder objects instead of a CustomerIOConfig. A few of the configuration options changed. In particular, CustomerIOBuilder replaces CustomerIO.Builder. cdpApiKey replaces apiKey: this is a new key that you got from Step 1 migrationSiteId replaces siteId: this is the same key you used in the previous version of the SDK. You need to include this property to send remaining traffic when people update your app. AutoTrackActivityScreens replaces autoTrackScreenViews: functionality is unchanged. The messagingInApp module now includes a site ID and region—these tell the SDK which workspace your in-app messages come from. CustomerIOBuilder( applicationContext = this, // new credentials cdpApiKey = "your_cdp_api_key" migrationSiteId = "your_site_id" ).apply { // If you're in the EU, set Region.EU region(Region.US) addCustomerIOModule( ModuleMessagingInApp( // the in-app module now has its own configuration config = MessagingInAppModuleConfig.Builder( siteId = "your_site_id", region = Region.US ).setEventListener(InAppMessageEventListener()).build() ) ) addCustomerIOModule(ModuleMessagingPushFCM()) build() } 4. Update your identify, track, and screen calls Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track: data becomes properties screen: name becomes title, and data becomes properties We’ve highlighted changes in the sample below. //identify: identifier becomes userId, body becomes traits CustomerIO.instance() .identify( userId = "USER_ID", traits = mapOf("first_name" to "firstName") ) // track: data becomes properties CustomerIO.instance().track( name = "purchase", properties = mapOf("product" to "socks", "price" to "4.99") ) // screen: name becomes title, data becomes properties CustomerIO.instance().screen( title = "purchase", properties = mapOf("product" to "socks", "price" to "4.99"), category: String = "" // optional ) Configuration Changes As a part of this release, we’ve changed a few configuration options. The MessagingInApp and MessagingPush modules also now take their own configuration options. datapipelines configuration options For the base SDK, you’ll use SDKConfigBuilder to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data to Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. AutoTrackActivityScreens boolean false Replaces autoTrackScreenViews; functionality is unchanged. We simply renamed the option to reflect support for UIKit and not SwiftUI. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). MessagingInApp configuration options When you initialize the MessagingInApp package, you must pass both of these configuration options. Option Type Default Description siteId string The Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from. Region .US or .EU .US The region your Customer.io account resides in—US or EU. --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/android/whats-new/3.x-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Android 12 changes the way the operating system resolves deep links. We’ve resolved the issue in our 3.x release, involving the following behavioral changes. If your app is open or you send a data notification with a non-app link: Android 12 or later: Your notification will launch the host app first, and then a matching app on top of it. Android 11 or earlier: Your notification will launch the matching app without launching the host app. By default, you You can now disable the ability to open links outside the app from your SDK configuration. CustomerIOPushNotificationCallback replaces CustomerIOUrlHandler The new CustomerIOPushNotificationCallback class handles deep links with Android 12 or later that otherwise would not have worked with the previous CustomerIOUrlHandler class. The ModuleMessagingPushFCM now has a config object that contains this notificationCallback and an optional redirectDeepLinksToOtherApps boolean (defaults to true). class MainApplication : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIOBuilder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) setRedirectDeepLinksToOtherApps(false) }.build() ) ) builder.build() } override fun createTaskStackFromPayload( context: Context, payload: CustomerIOParsedPushPayload ): TaskStackBuilder? { // return TaskStackBuilder of your choice if you plan to handle the deep link yourself // return null if you want CustomerIO SDK to do it for you TODO("Pass the link to your Deep link managers") } } Upgrade from 1.x to 2.x Remove .enqueue() The Android SDK 2.0 release introduces a queue system, making it easier to integrate with the SDK. All of the SDK functions that previously required a .enqueue() call, no longer do. Simply delete that code in your app to migrate to using the queue. // Before CustomerIO.instance().track(...).enqueue {...} // After CustomerIO.instance().track(...) This impacts the following functions: CustomerIO.instance().identify(...) CustomerIO.instance().track(...) CustomerIO.instance().screen(...) CustomerIO.instance().registerDeviceToken(...) CustomerIO.instance().deleteDeviceToken(...) CustomerIO.instance().trackMetric(...) If you use the optional FCM Push notification SDK, you’ll also benefit from the queue. // Before CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage, errorCallback = { ... }) // After CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage) // Before CustomerIOFirebaseMessagingService.onNewToken(token) { ... } // After CustomerIOFirebaseMessagingService.onNewToken(token) Initialize optional SDKs To keep your app size as small as possible, the Customer.io SDK is broken up into optional SDKs that you install only when you need them. In version 1.0, you only needed to install a dependency with Gradle to use an optional SDK. Version 2.0 introduces a breaking change that requires you to initialize optional SDKs after you install them via Gradle. For example, if you have the optional FCM Push notifications SDK installed, you need to add 1 new line to the CustomerIOBuilder: // Before CustomerIOBuilder() .build() // After CustomerIOBuilder() .addCustomerIOModule(ModuleMessagingPushFCM()) .build() The FCM Push notifications SDK is the currently only optional SDK module. When we release additional SDKs in the future, you can expect to initialize these SDKs as well. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/android/whats-new/changelog/ Check out release history for stable releases of android SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. Major versions may include breaking changes. See [our migration guide](/integrations/sdk/android/migrate-upgrade) for help updating your SDK integration to take advantage of new features and fixes. show --- ## Get Started URL: https://docs.customer.io/integrations/sdk/android/3.x/getting-started/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. SDK packages To minimize our impact on your app’s size, we offer multiple, separate SDKs. You should only install the SDKs that you need for your project. You must install the Tracking SDK. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Required? Description tracking ✅ identify people in Customer.io messaging-push-fcm Receive and interpret push notifications Prerequisites To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Install the SDK Before you add Customer.io dependencies, update your repositories in the settings.gradle file to include mavenCentral(). dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // only needed for in-app messaging in SDK versions below 3.6.1 // maven { url 'https://maven.gist.build' } } } Or, if you’re using an earlier project setup, make sure that you have added mavenCentral() as a repository in your root-level build.gradle file: allprojects { repositories { google() mavenCentral() // only needed for in-app messaging in SDK versions below 3.6.1 // maven { url 'https://maven.gist.build' } } } Install the dependencies that are relevant to your implementation. Replace version-here with the the latest version (available in our github repository). implementation 'io.customer.android:tracking:3.11.1' implementation 'io.customer.android:messaging-push-fcm:3.11.1' Initialize the SDK Before you can use the Customer.io SDK, you need to initialize it. CustomerIO is a singleton: you’ll create it once and re-use it across your application. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. You should initialize CustomerIO in the Application class, so that you can access that instance from any part of your application using the instance() method. class App : Application() { override fun onCreate() { super.onCreate() val customerIO = CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ).build() } } The Builder for CustomerIO exposes configuration options for features like region, and timeout. val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.setRegion(Region.EU) // set the request timeout for all the API requests sent from SDK builder.setRequestTimeout(8000L) builder.build() Configuration options When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app or enable autoTrackScreenViews to automatically capture screen view events for your audience. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackScreenViews boolean false If true, the SDK automatically sends screen events for every screen your audience visits. backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ) .autoTrackScreenViews(true) .build() The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] Using the SDK as a data-in integration The SDK uses our Legacy Track API API, but it can also double as a source of data for other integrations without additional development work. To do this, we translate calls from the SDK to our newer Data Pipelines API format before we send them to your destinations. In general, we recommend that you upgrade your app to use a newer version of the SDK. Our newer versions rely on the Data Pipelines API, so you can take advantage of your mobile data without without us translating it for you. It can make it easier to trace data from your app to your destinations and troubleshoot issues as they arise. Our newer SDKs also support more features, like anonymous tracking. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/android/3.x/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You must have have the Tracking SDK to use this feature. implementation 'io.customer.android:tracking:3.11.1' This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. CustomerIO.instance() .identify( identifier = "989388339", attributes = mapOf("first_name" to "firstName") ) An identify request takes the following parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (when updating people), depending on your workspace settings. attributes (Optional): Contains attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person; accepts strings, enums, primitives (int, float, char, etc.), their boxed counterparts (Integer, Float, Character, etc.), arrays, collections, lists, sets, and maps. Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can call identify() again to update their attributes on the server-side. Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.instance().deviceAttributes = mapOf("key" to "value") However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. // set before you build builder.autoTrackDeviceAttributes(false) Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Calls to the Customer.io SDK will be ignored until you identify a new person. CustomerIO.instance().clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/android/3.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. You can reference the data in your event to segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. members of your audience or as variables in your messages using 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}}.. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. attributes (Optional): The body of the event. You can reference attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.instance().track( name = "purchase", attributes = mapOf("product" to "socks", "price" to "4.99") ) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your app. You can turn on automatic screen tracking by appending autoTrackScreenViews(true) to CustomerIO.Builder. When automatically tracking screen events, we capture the name of the screen with the following priority from highest to lowest: We check if the current Activity has a label in the manifest file. If it does, the SDK will use the value for label. We get the class name of the Activity and use that value. The SDK will take whatever value it receives and will strip the word Activity from it. Example: If you have an Activity with the manifest label or class name ProfileActivity, the SDK will track the screen view with the name Profile. CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ) .autoTrackScreenViews(true) .build() If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can send screen events manually. Send your own screen events Screen events use the .screen method. Like other events, you can add a map of attributes object containing additional information about the screen event or the currently-identified person. CustomerIO.instance().screen( name = "baseballDailyScores", attributes = mapOf("prevScreen" to "homescreen", "secondsInApp" to "120") ) --- ## Push notifications URL: https://docs.customer.io/integrations/sdk/android/3.x/push/ Get started setting up push notifications for Android. Our Android SDK supports push notifications over FCM, including rich push messages with links and images. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style push fill:#B5FFEF,stroke:#007069 Before you begin This page explains how to receive rich push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through Firebase Cloud Messaging (FCM). How it works Before a device can receive a push notification, you must: Set up FCM. Set up push. Identify a person. When someone starts the app, they automatically generate a device token. Identifying the person associates the device token with the person in Customer.io, so that they can receive push notifications. Set up a campaign to send a push notification through the Customer.io composer. Set up push You must implement the Push Messaging SDK to use push notification features. implementation 'io.customer.android:messaging-push-fcm:3.11.1' Initialize the push module. The push module has an optional config object, explained below. See deep links for help configuring links. CustomerIO.Builder( siteId = "siteId", apiKey = "apiKey", appContext = application, ).apply { addCustomerIOModule( ModuleMessagingPushFCM() ) autoTrackScreenViews(true) setRequestTimeout(8000L) setRegion(Region.US) build() } The SDK adds a FirebaseMessagingService to the app manifest automatically, so you don’t have to perform additional setup to handle incoming push messages. However, if your application implements its own FirebaseMessagingService, make sure that when you call onMessageReceived and onNewToken methods, you also call CustomerIOFirebaseMessagingService.onMessageReceived and CustomerIOFirebaseMessagingService.onNewToken respectively. class FirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { val handled = CustomerIOFirebaseMessagingService.onMessageReceived(context, message) if (handled) { logger.breadcrumb(this, "Push notification has been handled", null) } } override fun onNewToken(token: String) { CustomerIOFirebaseMessagingService.onNewToken(context, token) } Push notifications launched from the SDK are currently posted to our default channel—[your app name] Channel. In the future, we plan to let you customize channels/categories so that users can subscribe and unsubscribe to content categories as necessary. Push module configuration ModuleMesagingPushFCM has an optional configuration object. The notificationCallback is A callback that notifies client on push notification related actions. For now, this callback is only useful for handling deep links, but we have plans to add more methods in future releases. It defaults to null. The redirectDeepLinksToOtherApps flag enables (true) or disables (false) the ability to open non app links outside the app. It is true by default. true: links not supported by your app will be opened in other matching apps. If a matching app isn’t found, the host app opens to default landing page. false: unsupported links open the host app to its default landing page. CustomerIO.Builder( siteId = "siteId", apiKey = "apiKey", appContext = application, ).apply { addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) setRedirectDeepLinksToOtherApps(false) }.build() ) ) setRegion(Region.US) build() } Push click behavior The pushClickBehavior config lets you customize your application’s response when your audience taps a push notification. This includes going to specific deep links or launcher screens based on the notification payload. Note that the SDK tracks opened metrics for all click behaviors. builder.addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setPushClickBehavior(PushClickBehavior.ACTIVITY_PREVENT_RESTART) }.build() ) ) The available options are: ACTIVITY_PREVENT_RESTART (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ACTIVITY_NO_FLAGS: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. RESET_TASK_STACK: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. Capture push metrics Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. By default, the messaging-push-fcm SDK automatically tracks opened and delivered for push notifications originating from Customer.io. Otherwise, you can track push metrics with the trackMetric method. CustomerIO.instance().trackMetric( deliveryID = deliveryId, deviceToken = deliveryToken, event = MetricEvent.delivered ) Deep links Deep links provide a way to link to a screen in your app. Our SDK supports redirects for notification links registered in Android by default. You can customize this behavior using CustomerIOPushNotificationCallback. To register a deep link, you must first add intent filters in your AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "remote-habits://settings” --> <data android:host="settings" android:scheme="remote-habits" /> </intent-filter> CustomerIOPushNotificationCallback—a URL handler feature provided by the SDK. When configuring your CustomerIO instance, you can set the callback to handle notification behavior. class MainApplication : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) setRedirectDeepLinksToOtherApps(true) }.build() ) ) builder.build() } override fun createTaskStackFromPayload( context: Context, payload: CustomerIOParsedPushPayload ): TaskStackBuilder? { // return TaskStackBuilder of your choice if you plan to handle the deep link yourself // and make sure to track notification metrics in the right place // // return null if you want CustomerIO SDK to do it for you TODO("Pass the link to your Deep link managers") } }  When someone taps a push notification with a deep link, the SDK calls the CustomerIOPushNotificationCallback specified in CustomerIO.Builder object before it looks for default supported links. The notification handler returns the TaskStackBuilder. If the handler is not set or returns null, the SDK tries to open the link in your app. If your app doesn’t support the link and redirectDeepLinksToOtherApps in the MessagingPushModuleConfig is true, the SDK will open the link in a web browser (if a web browser app is available). By default, redirectDeepLinksToOtherApps is true.  Don’t forget to capture metrics When you provide CustomerIOPushNotificationCallback, don’t forget to capture notification metrics, otherwise our dashboards might not show notification conversions properly. Customizing Push Notifications You can customize the icon and color of push notifications by updating your AndroidManifest as recommended by FCM. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> However, if you want more control over your notifications’ appearance and behavior, Customer.io SDK provides an option to override these settings on the app side. You can customize notification appearance by implementing CustomerIOPushNotificationCallback and overriding the onNotificationComposed method. class MainApplication : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) setRedirectDeepLinksToOtherApps(true) }.build() ) ) builder.build() } override fun onNotificationComposed( payload: CustomerIOParsedPushPayload, builder: NotificationCompat.Builder ) { // Customize your notification here } } Customer.io SDK does not let you override PendingIntent for notifications. If you want to override the behavior when people tap your notifications, you can implement createTaskStackFromPayload as described above in deep links. Test Rich Push After you set up your push implementation, you should test it. Simply set up a push notification that includes an image and link. This is what the payload looks like on our end. If you’ve set up your app to use other data—custom keys outside the scope of our SDK—you can use our Custom Payload Editor; you’re welcome to place custom keys inside the message.data object, but you’ll need to do additional development to support keys beyond our standard title, body, link, and image. { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/android/3.x/in-app/ Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. To set up in app messaging, install and initialize the tracking and messaging-in-app packages. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to implement screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Install the SDK and in-app module Make sure that you add update your repositories in the settings.gradle file to include the in-app SDK. dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // only needed for in-app messaging in SDK versions below 3.6.1 // maven { url 'https://maven.gist.build' } } } Implement the messaging-in-app package. implementation "io.customer.android:messaging-in-app:3.11.1" Initialize the SDK with the in-app module Simply initialize the SDK with the MessagingInApp module and your app will be able to receive in-app messages. Create a campaign and send your first in-app message to test your implementation! CustomerIO.Builder( siteId = "siteId", apiKey = "apiKey", appContext = application, ).apply { // Setup In-app messaging addCustomerIOModule(ModuleMessagingPushFCM()) addCustomerIOModule(ModuleMessagingInApp()) LogLevel(CioLogLevel.DEBUG) // For fragment-based apps or Jetpack Compose, disable this and use manual screen tracking autoTrackScreenViews(true) setRequestTimeout(8000L) setRegion(Region.US) build() } // `ModuleMessagingInApp` takes in optional configuration options. // One option is an event listener to get notified when a message is shown, dismissed, or an action is taken. ModuleMessagingInApp(config = MessagingInAppModuleConfig.Builder() .setEventListener(object : InAppEventListener { override fun errorWithMessage(message: InAppMessage) {} override fun messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {} override fun messageDismissed(message: InAppMessage) {} override fun messageShown(message: InAppMessage) {} }) .build() ) Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Enable screen tracking in the SDK. If you are using a view(xml)-based() UI with activities, you can enable automatic screen tracking by calling autoTrackScreenViews(true) during SDK initialization. However, if your app is fragment-based or utilizes Jetpack Compose, we recommend utilizing manual screen tracking with the screen method. Provide page names to whomever sets up in-app messages in fly.customer.io. The SDK automatically uses label in your manifest file as the page/screen name. If your screens don’t have labels, you won’t be able to set up page rules and screenview events will have an empty event name. You should inform anybody creating in-app messages about page names if you want to set up page rules, to make sure that your messages appear on the right pages of your app. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CustomerIO.Builder( siteId = "siteId", apiKey = "apiKey", appContext = application, ).apply { addCustomerIOModule(ModuleMessagingInApp()) build() } ModuleMessagingInApp(config = MessagingInAppModuleConfig.Builder() .setEventListener(object : InAppEventListener { override fun errorWithMessage(message: InAppMessage) {} override fun messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {} override fun messageDismissed(message: InAppMessage) {} override fun messageShown(message: InAppMessage) {} }) .build() ) Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.instance().inAppMessaging().dismissMessage() --- ## Test support URL: https://docs.customer.io/integrations/sdk/android/3.x/test-support/ The SDK makes it easy to write unit, integration, UI, or other types of automated tests in your code base. We designed our SDK with first-class support for automated testing, making it easy to inject dependencies and perform mocking in your code. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style test-support fill:#B5FFEF,stroke:#007069 Dependency injection Every SDK class inherits from an interface. Inherited interfaces use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. If you want to inject a class in your project, it could look something like the example below. class ProfileRepository(private val cio: CustomerIOInstance) { // Now, you can call any of the `CustomerIO` class functions with `this.cio`! suspend fun loginUser(email: String, password: String) { // Login the user to your system... // Then, identify the profile with Customer.io: cio.identify(email) } } val cio = CustomerIO.instance() val repository = ProfileRepository(cio) Mocking Every SDK class inherits from an interface. Inherited interfaces use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. Interfaces are really easy to mock with mocking frameworks such as Mockito. Here’s an example test class showing how you would test your ProfileRepository class. class ProfileRepositoryTest { private val cioMock: CustomerIOInstance = mock() private lateinit var repository: ProfileRepository @Before fun setUp() { repository = ProfileRepository(cioMock) } @Test fun test_loginUser(): Unit = runBlocking { val givenEmail = "example@example.com" val givenPassword = "123" // Now, call your function under test: repository.loginUser(givenEmail, givenPassword) // You can access many properties of the mock class to assert the behavior of the mock. verify(cioMock, times(1)).identify(givenEmail, givenPassword) } } --- ## Migrate from an earlier version URL: https://docs.customer.io/integrations/sdk/android/3.x/migrate-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Android 12 changes the way the operating system resolves deep links. We’ve resolved the issue in our 3.x release, involving the following behavioral changes. If your app is open or you send a data notification with a non-app link: Android 12 or later: Your notification will launch the host app first, and then a matching app on top of it. Android 11 or earlier: Your notification will launch the matching app without launching the host app. By default, you You can now disable the ability to open links outside the app from your SDK configuration. CustomerIOPushNotificationCallback replaces CustomerIOUrlHandler The new CustomerIOPushNotificationCallback class handles deep links with Android 12 or later that otherwise would not have worked with the previous CustomerIOUrlHandler class. The ModuleMessagingPushFCM now has a config object that contains this notificationCallback and an optional redirectDeepLinksToOtherApps boolean (defaults to true). class MainApplication : Application(), CustomerIOPushNotificationCallback { override fun onCreate() { super.onCreate() val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.addCustomerIOModule( ModuleMessagingPushFCM( config = MessagingPushModuleConfig.Builder().apply { setNotificationCallback(this) setRedirectDeepLinksToOtherApps(false) }.build() ) ) builder.build() } override fun createTaskStackFromPayload( context: Context, payload: CustomerIOParsedPushPayload ): TaskStackBuilder? { // return TaskStackBuilder of your choice if you plan to handle the deep link yourself // return null if you want CustomerIO SDK to do it for you TODO("Pass the link to your Deep link managers") } } Upgrade from 1.x to 2.x Remove .enqueue() The Android SDK 2.0 release introduces a queue system, making it easier to integrate with the SDK. All of the SDK functions that previously required a .enqueue() call, no longer do. Simply delete that code in your app to migrate to using the queue. // Before CustomerIO.instance().track(...).enqueue {...} // After CustomerIO.instance().track(...) This impacts the following functions: CustomerIO.instance().identify(...) CustomerIO.instance().track(...) CustomerIO.instance().screen(...) CustomerIO.instance().registerDeviceToken(...) CustomerIO.instance().deleteDeviceToken(...) CustomerIO.instance().trackMetric(...) If you use the optional FCM Push notification SDK, you’ll also benefit from the queue. // Before CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage, errorCallback = { ... }) // After CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage) // Before CustomerIOFirebaseMessagingService.onNewToken(token) { ... } // After CustomerIOFirebaseMessagingService.onNewToken(token) Initialize optional SDKs To keep your app size as small as possible, the Customer.io SDK is broken up into optional SDKs that you install only when you need them. In version 1.0, you only needed to install a dependency with Gradle to use an optional SDK. Version 2.0 introduces a breaking change that requires you to initialize optional SDKs after you install them via Gradle. For example, if you have the optional FCM Push notifications SDK installed, you need to add 1 new line to the CustomerIO.Builder: // Before CustomerIO.Builder() .build() // After CustomerIO.Builder() .addCustomerIOModule(ModuleMessagingPushFCM()) .build() The FCM Push notifications SDK is the currently only optional SDK module. When we release additional SDKs in the future, you can expect to initialize these SDKs as well. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/android/3.x/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Capture logs Logs help us pinpoint the problem and find a solution. To capture logs from the Customer.io SDK: Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.logLevel("debug") builder.build() In Android Studio, build and run your app on a physical device or emulator. Select View > Tool Windows > Logcat. This shows you your device’s logs. Filter for CIO in the top to find log messages specific to the Customer.io SDK. Save your log and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Push notification issues Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/android/3.x/changelog/ Check out release history for stable releases of android SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. Major versions may include breaking changes. See [our migration guide](/integrations/sdk/android/migrate-upgrade) for help updating your SDK integration to take advantage of new features and fixes. show --- ## Get Started URL: https://docs.customer.io/integrations/sdk/android/2.x/getting-started/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. SDK packages To minimize our impact on your app’s size, we offer multiple, separate SDKs. You should only install the SDKs that you need for your project. You must install the Tracking SDK. It lets you identify people, which you must do before you can send them messages, track their events, etc. Package Required? Description tracking ✅ identify people in Customer.io messaging-push-fcm Receive and interpret push notifications Install the SDK You’ll find detailed instructions to install our SDK in our Github repository. Before you add Customer.io dependencies, update your repositories in the settings.gradle file to include mavenCentral(): dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } Or, if you’re using an earlier project setup, make sure that you have added mavenCentral() as a repository in your root-level build.gradle file: allprojects { repositories { google() mavenCentral() } } Install the dependencies that are relevant to your implementation. Replace version-here with the the latest version (available in our github repository). implementation 'io.customer.android:tracking:<version-here>' implementation 'io.customer.android:messaging-push-fcm:<version-here>' Initialize the SDK Before you can use the Customer.io SDK, you need to initialize it. CustomerIO is a singleton: you’ll create it once and re-use it across your application. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. You should initialize CustomerIO in the Application class, so that you can access that instance from any part of your application using the instance() method. class App : Application() { override fun onCreate() { super.onCreate() val customerIO = CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ).build() } } The Builder for CustomerIO exposes configuration options for features like region, and timeout. val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.setRegion(Region.EU) // set the request timeout for all the API requests sent from SDK builder.setRequestTimeout(8000L) builder.build() Configuration options When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app or enable autoTrackScreenViews to automatically capture screen view events for your audience. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackScreenViews boolean false If true, the SDK automatically sends screen events for every screen your audience visits. backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ) .autoTrackScreenViews(true) .build() The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] --- ## Identify people URL: https://docs.customer.io/integrations/sdk/android/2.x/identify/ You need to identify a person using a mobile device before you can send them messages or track events for things they do in your app. You must have have the Tracking SDK to use this feature. implementation 'io.customer.android:tracking:<version-here>' This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. CustomerIO.instance() .identify( identifier = "989388339", attributes = mapOf("first_name" to "firstName") ) An identify request takes the following parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (when updating people), depending on your workspace settings. attributes (Optional): Contains attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person; accepts strings, enums, primitives (int, float, char, etc.), their boxed counterparts (Integer, Float, Character, etc.), arrays, collections, lists, sets, and maps. Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can call identify() again to update their attributes on the server-side. Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Custom device attributes When we collect device attributes, you can also set custom device attributes with the deviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. CustomerIO.instance().deviceAttributes = mapOf("key" to "value") However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. Disable automatic device attribute collection By default, the SDK automatically collects the device attributes defined above. You can change your config to prevent the SDK from automatically collecting these attributes. // set before you build builder.autoTrackDeviceAttributes(false) Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). // Calls to the Customer.io SDK will be ignored until you identify a new person. CustomerIO.instance().clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/android/2.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. You can reference the data in your event to segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. members of your audience or as variables in your messages using 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}}.. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. attributes (Optional): The body of the event. You can reference attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.instance().track( name = "purchase", attributes = mapOf("product" to "socks", "price" to "4.99") ) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your app. You can turn on automatic screen tracking by appending autoTrackScreenViews(true) to CustomerIO.Builder. When automatically tracking screen events, we capture the name of the screen with the following priority from highest to lowest: We check if the current Activity has a label in the manifest file. If it does, the SDK will use the value for label. We get the class name of the Activity and use that value. The SDK will take whatever value it receives and will strip the word Activity from it. Example: If you have an Activity with the manifest label or class name ProfileActivity, the SDK will track the screen view with the name Profile. CustomerIO.Builder( siteId = "your-site-id", apiKey = "your-api-key", appContext = this ) .autoTrackScreenViews(true) .build() If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can send screen events manually. Send your own screen events Screen events use the .screen method. Like other events, you can add a map of attributes object containing additional information about the screen event or the currently-identified person. CustomerIO.instance().screen( name = "baseballDailyScores", attributes = mapOf("prevScreen" to "homescreen", "secondsInApp" to "120") ) --- ## Push notifications URL: https://docs.customer.io/integrations/sdk/android/2.x/push/ Get started setting up push notifications for Android. Our Android SDK supports push notifications over FCM. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style push fill:#B5FFEF,stroke:#007069 Before you begin This page explains how to receive push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through Firebase Cloud Messaging (FCM). This process lets you receive basic push notifications in your app—a title and a message body. To send a notification with an image, link, etc, complete the process on this page, and then see our rich push documentation. How it works Before a device can receive a push notification, you must: Set up FCM. Set up push. Identify a person. When someone starts the app, they automatically generate a device token. Identifying the person associates the device token with the person in Customer.io, so that they can receive push notifications. Set up a campaign to send a push notification through the Customer.io composer. Set up push You must implement the Push Messaging SDK to use push notification features. implementation 'io.customer.android:messaging-push-fcm:<version-here>' Initialize the push module. CustomerIO.Builder( siteId = "siteId", apiKey = "apiKey", appContext = application, ).apply { addCustomerIOModule(ModuleMessagingPushFCM()) autoTrackScreenViews(true) setRequestTimeout(8000L) setRegion(Region.US) build() } The SDK adds a FirebaseMessagingService to the app manifest automatically, so you don’t have to perform additional setup to handle incoming push messages. If your application implements its own FirebaseMessagingService, make sure that when you call onMessageReceived and onNewToken methods, you also call CustomerIOFirebaseMessagingService.onMessageReceived and CustomerIOFirebaseMessagingService.onNewToken respectively. class FirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { val handled = CustomerIOFirebaseMessagingService.onMessageReceived(message) if (handled) { logger.breadcrumb(this, "Push notification has been handled", null) } } override fun onNewToken(token: String) { CustomerIOFirebaseMessagingService.onNewToken(token) } Push notifications launched from the SDK are currently posted to our default channel—[your app name] Channel. In the future, we plan to let you customize channels/categories so that users can subscribe and unsubscribe to content categories as necessary. Capture push metrics Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. By default, the messaging-push-fcm SDK automatically tracks opened and delivered for push notifications originating from Customer.io. Otherwise, you can track push metrics with the trackMetric method. CustomerIO.instance().trackMetric( deliveryID = deliveryId, deviceToken = deliveryToken, event = MetricEvent.delivered ) --- ## Rich push notifications URL: https://docs.customer.io/integrations/sdk/android/2.x/rich-push/ With rich push, you can do more than just send a simple notification; you can send an image, open a deep link when someone taps your message, and more! This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style rich-push fill:#B5FFEF,stroke:#007069 Set up rich push Rich push is included with the messaging-push-fcm SDK. For now, our rich push feature only supports images and deep links. We’ll add additional features here as we continue to develop our SDKs. Send a rich push To send a rich push through Customer.io, you need to use our Custom Payload editor, which takes a JSON structure. In the editor, you’ll select the type of device you want to send your message to: you can have separate payloads for Android and iOS. In our case, you’ll click Android. The following example shows a rich push with an image and a link. See the Deep links section below for help enabling deep links in your app. { "message": { "notification": { "image": "https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg", "title": "Hi there", "body": "How're you doing?" }, "data": { "link": "remote-habits://deep?message=hello&message2=world" } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. Deep links  If you use CustomerIOUrlHandler, set your SDK target to 11 Android 12 changes the way apps handle deep links when the app is in the background or closed. Our next version (3.x) will include updates to the CustomerIOUrlHandler to handle deep links gracefully. In the meantime, you should set your SDK target in your Android manifest to version 11 to ensure that your deep links work properly. Deep links provide a way to link to a screen in your app. There are two ways to set up deep links. If you use both methods, the CustomerIOUrlHandler takes precedence. Using intent filters in your AndroidManifest.xml file. Click here to learn more about intent filters. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "remote-habits://deep” --> <data android:host="deep" android:scheme="remote-habits" /> </intent-filter> Using CustomerIOUrlHandler—a URL handler feature provided by the SDK. When configuring your CustomerIO instance, attach a listener using the setCustomerIOUrlHandler method of CustomerIO.Builder. You should do this in the entry point of the application, which is usually the Application class. class MainApplication : Application(), CustomerIOUrlHandler { override fun onCreate() { super.onCreate() val builder = CustomerIO.Builder( siteId = "YOUR-SITE-ID", apiKey = "YOUR-API-KEY", appContext = this ) builder.setCustomerIOUrlHandler(this) builder.build() } override fun handleCustomerIOUrl(uri: Uri): Boolean { // return true if you plan to handle the deep link yourself // return false if you want CustomerIO SDK to do it for you TODO("Pass the link to your Deep link managers") } }  When someone taps a push notification with a deep link, the SDK calls the urlHandler specified in your CustomerIO.Builder object first. If the handler is not set or returns false, the SDK will open the link in a browser. Test Rich Push After you set up rich push, you should test your implementation. Use the payloads below to send a push in the Customer.io web app with a Custom Payload. In both of the test payloads below, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. Rich Push Payloads To send a rich push in Customer.io, you need to use our Custom Payload editor, which takes a JSON structure. In the editor, you’ll select the type of device you want to send your message to: you can have separate payloads for Android and iOS. In our case, you’ll click Android. The part of your payload interpreted by the Android SDK is contained in the message.data object. This object can contain other custom data for your app, but you’ll need to do additional development to support keys beyond title, body, link, and image—all of which are handled by the SDK. { "message": { "notification": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string" //https URL to an image you want to include in the notification }, "data": { "link": "string", //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Test support URL: https://docs.customer.io/integrations/sdk/android/2.x/test-support/ The SDK makes it easy to write unit, integration, UI, or other types of automated tests in your code base. We designed our SDK with first-class support for automated testing, making it easy to inject dependencies and perform mocking in your code. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> rich-push(Receive Rich Push) track-events --> test-support(Write tests) push --> test-support rich-push --> test-support identify -.-> in-app(Receive in-app) in-app --> test-support click getting-started href "/integrations/sdk/android/getting-started/#install" click B href "/integrations/sdk/android/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/android/identify" click track-events href "/integrations/sdk/android/track-events/" click register-token href "/integrations/sdk/android/push" click push href "/integrations/sdk/android/push" click rich-push href "/integrations/sdk/android/rich-push" click in-app href "/integrations/sdk/android/in-app" click test-support href "/integrations/sdk/android/test-support" style test-support fill:#B5FFEF,stroke:#007069 Dependency injection Every SDK class inherits from an interface. Inherited interfaces use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. If you want to inject a class in your project, it could look something like the example below. class ProfileRepository(private val cio: CustomerIOInstance) { // Now, you can call any of the `CustomerIO` class functions with `this.cio`! suspend fun loginUser(email: String, password: String) { // Login the user to your system... // Then, identify the profile with Customer.io: cio.identify(email) } } val cio = CustomerIO.instance() val repository = ProfileRepository(cio) Mocking Every SDK class inherits from an interface. Inherited interfaces use a consistent naming convention: <NameOfClass>Instance. For example, the CustomerIO class inherits the protocol CustomerIOInstance. Interfaces are really easy to mock with mocking frameworks such as Mockito. Here’s an example test class showing how you would test your ProfileRepository class. class ProfileRepositoryTest { private val cioMock: CustomerIOInstance = mock() private lateinit var repository: ProfileRepository @Before fun setUp() { repository = ProfileRepository(cioMock) } @Test fun test_loginUser(): Unit = runBlocking { val givenEmail = "example@example.com" val givenPassword = "123" // Now, call your function under test: repository.loginUser(givenEmail, givenPassword) // You can access many properties of the mock class to assert the behavior of the mock. verify(cioMock, times(1)).identify(givenEmail, givenPassword) } } --- ## Migrate from an earlier version URL: https://docs.customer.io/integrations/sdk/android/2.x/migrate-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 1.x to 2.x Remove .enqueue() The Android SDK 2.0 release introduces a queue system, making it easier to integrate with the SDK. All of the SDK functions that previously required a .enqueue() call, no longer do. Simply delete that code in your app to migrate to using the queue. // Before CustomerIO.instance().track(...).enqueue {...} // After CustomerIO.instance().track(...) This impacts the following functions: CustomerIO.instance().identify(...) CustomerIO.instance().track(...) CustomerIO.instance().screen(...) CustomerIO.instance().registerDeviceToken(...) CustomerIO.instance().deleteDeviceToken(...) CustomerIO.instance().trackMetric(...) If you use the optional FCM Push notification SDK, you’ll also benefit from the queue. // Before CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage, errorCallback = { ... }) // After CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage) // Before CustomerIOFirebaseMessagingService.onNewToken(token) { ... } // After CustomerIOFirebaseMessagingService.onNewToken(token) Initialize optional SDKs To keep your app size as small as possible, the Customer.io SDK is broken up into optional SDKs that you install only when you need them. In version 1.0, you only needed to install a dependency with Gradle to use an optional SDK. Version 2.0 introduces a breaking change that requires you to initialize optional SDKs after you install them via Gradle. For example, if you have the optional FCM Push notifications SDK installed, you need to add 1 new line to the CustomerIO.Builder: // Before CustomerIO.Builder() .build() // After CustomerIO.Builder() .addCustomerIOModule(ModuleMessagingPushFCM()) .build() The FCM Push notifications SDK is the currently only optional SDK module. When we release additional SDKs in the future, you can expect to initialize these SDKs as well. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/android/2.x/changelog/ Check out release history for stable releases of android SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. Major versions may include breaking changes. See [our migration guide](/integrations/sdk/android/migrate-upgrade) for help updating your SDK integration to take advantage of new features and fixes. show --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/react-native/quick-start-guide/ React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages. Install the SDK. Identify and Track Push Notifications In-App 1. Install the SDK You need to add a new React Native connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration. in Customer.io to get your CDP API key. See Get your CDP API key for details. Make sure you set up your React Native environment first. You must use React Native 0.79 or later. Open your terminal and go to your project folder. Install the customerio-reactnative package using NPM or Yarn: npm install customerio-reactnative # or yarn add customerio-reactnative Set up your project to support iOS and/or Android deployments: iOS iOS For iOS, install CocoaPods dependencies: pod install --repo-update --project-directory=ios Make sure your minimum iOS deployment target is set to 13.0 in both your Podfile and Xcode project settings. Android Android For Android, include the Google Services plugin by adding the following to your project-level android/build.gradle file: buildscript { repositories { google() // Google's Maven repository } dependencies { classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { google() // Google's Maven repository } } Add the plugin to your app-level android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and place it in android/app/google-services.json. Add your CDP API key and site ID to your configuration. CDP API Key: You’ll find this key in your React Native connection. Site ID: You’ll find this value in your workspace under Settings > Workspace Settings > API and webhook credentials. Initialize the SDK in your app. Add the following code to your main component or App.js file: import React, { useEffect } from 'react'; import { CioConfig, CioRegion, CioLogLevel, CustomerIO, PushClickBehaviorAndroid } from 'customerio-reactnative'; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', // Required region: CioRegion.US, // Replace with CioRegion.EU if your Customer.ioaccount is in the EU logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'YOUR_SITE_ID', // Required for in-app messaging } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []); Run your application to ensure everything is set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. import { CustomerIO } from "customerio-reactnative"; const identifyUserExample = async () => { await CustomerIO.identify({ userId: 'react-native-test-user@example.com', traits: { firstName: 'John', lastName: 'Doe', email: 'react-native-test-user@example.com', subscriptionStatus: 'active', }, }); console.log('User identified successfully'); }; Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. import { CustomerIO } from "customerio-reactnative"; const trackCustomEventExample = async () => { await CustomerIO.track('purchased_item', { product: 'Premium Subscription', price: 99.99, currency: 'USD' }); console.log('Custom event tracked successfully'); }; Track screen views to trigger in-app messages associated with specific screens. import { CustomerIO } from "customerio-reactnative"; const trackScreenViewExample = async () => { await CustomerIO.screen('ProductDetails'); console.log('Screen view tracked successfully'); }; 3. Push Notifications Set up your push notification credentials in Customer.io: iOS: Upload your Apple Push Notification certificate (.p8 file). Android: Upload your Firebase Cloud Messaging server key (.json format). Request push notification permissions from the user: import { CustomerIO, CioPushPermissionStatus } from "customerio-reactnative"; const requestPushPermissions = async () => { const permissionStatus = await CustomerIO.pushMessaging.showPromptForPushNotifications({ ios: { sound: true, badge: true } }); switch (permissionStatus) { case CioPushPermissionStatus.Granted: console.log('Push notifications enabled'); break; case CioPushPermissionStatus.Denied: case CioPushPermissionStatus.NotDetermined: console.log('Push notifications denied'); break; } }; For iOS: to ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes. For Android: add notification icon resources: Place a notification icon file named ic_notification.png in your drawable folders. Make sure your app’s AndroidManifest.xml has the proper FCM permissions. 4. In-App To enable in-app messaging, all you need to do is add the site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app. You can call the initialize method from your components or services: import { CioConfig, CustomerIO } from "customerio-reactnative"; import { useEffect } from "react"; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', inApp: { siteId: 'YOUR_SITE_ID', } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []);  Check out our code samples! You’ll find a complete, working sample app in our React Native SDK’s example directory. If you get stuck, you can refer to the sample app to see how everything fits together. --- ## How it works URL: https://docs.customer.io/integrations/sdk/react-native/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum requirements To support the Customer.io SDK, you must: Use React Native versions 0.79 and later. Current versions of XCode don’t support earlier versions of React Native. Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Add React Navigation to your app to support deep links and screen tracking. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/react-native/getting-started/auth/ To use the SDK, you'll need two kinds of API keys: A *CDP API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. These keys come from different places in Customer.io! CDP API Key: You’ll get this key when you set up your mobile app as a data-in integration in Customer.io. Site ID: This key tells the SDK which workspace your in-app messages come from. You’ll use it to support inApp messages. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your CDP API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your React Native entry under Integrations. If you don’t see your app on this page, you’ll need to add up a new integration. Go to Integrations. Go to the Connections tab and find your React Native connection. If you don’t have a React Native connection, you’ll need to set one up. Go to Settings and find your API Key. Copy this key into the CioConfig.CdpApiKey config option. import { CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Add a new integration If you don’t already have a write key, you’ll need to set up a new connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration.. The connection represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your site ID with the inApp option to support in-app messaging. And if you’re upgrading from a previous version of the SDK, you’ll also use your site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to Settings > Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the inApp package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/react-native/getting-started/packages-options/ The SDK consists of a few packages. You *must* use the `CioConfig` and `CustomerIO` packages to configure and initialize the React Native SDK. SDK packages The SDK consists of a few packages. You must use the CioConfig and CustomerIO packages to configure and initialize the SDK. Package Product Required? Description CustomerIO ✅ The main SDK package. Used to initialize the SDK and call the SDK’s methods. CioConfig ✅ Configure the SDK including in-app messaging support. CioRegion Used inside the CioConfig.region option to declare your region—EU or US. CioLogLevel Used inside the CioConfig.logLevel option to set the level of logs you can view from the SDK. CioLocationTrackingMode Used inside CioConfig.location to set the tracking mode. See location tracking for details. Configuration options You can determine global behaviors for the SDK in using CioConfig package. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. Note that the logLevel option requires the CioLogLevel package and the region option requires the CioRegion package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region CioRegion.EU or CioRegion.US CioRegion.US Requires the CioRegion package. You must set the region your account is in the EU (CioRegion.EU). apiHost string The domain you’ll proxy requests through. You’ll only need to set this (and cdnHost) if you’re proxying requests. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc cdnHost string The domain you’ll fetch configuration settings from. You’ll only need to set this (and apiHost) if you’re proxying requests. logLevel string error Requires the CioLogLevel package. Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. migrationSiteId string Required if you're updating from 3.x: the credential for previous versions of the SDK. This key lets the SDK send remaining tasks to Customer.io when your audience updates your app. screenViewUse All or InApp All ScreenView.All (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events inApp object Required for in-app support. This object takes a siteId property, determining the workspace your in-app messages come from. push object Takes a single option called PushClickBehaviorAndroid. This object and option controls how your app behaves when your Android audience taps push notifications. location object Enable location tracking. Takes a trackingMode option from the CioLocationTrackingMode package. Proxying requests By default, requests go through our domain at cdp.customer.io. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores. To proxy requests, you’ll need to set the apiHost and cdnHost properties in your SDKConfigBuilder. While these are separate settings, you should set them to the same URL. While you need to initialize the SDK with a cdpApiKey, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the cdpApiKey to a value representing basic authentication credentials that you handle on your own. See proxying requests for more information. import { CustomerIO, CioConfig, CioRegion, } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'your-cdp-api-key', region: CioRegion.US, inApp: { siteId: 'your-site-id', }, // Proxy requests through your own domain apiHost: 'proxy.example.com', cdnHost: 'proxy.example.com', } as CioConfig; CustomerIO.initialize(config); --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/react-native/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Capture logs Logs help us pinpoint the problem and find a solution. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import { CustomerIO, CioConfig, CioLogLevel } from 'customerio-reactnative'; const config: CioConfig = { CioApiKey: 'Your CDP API Key', logLevel: CioLogLevel.Debug, } CustomerIO.initialize(config) ; Build and run your app on a physical device or emulator. In the console, run: react-native log-ios react-native log-android Export your log to a text file and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Try updating iOS package dependencies This SDK uses our iOS push package. In some cases, we may make fixes in our iOS packages that fix downstream issues in, or expose new features to this SDK. You can update the version in your podfile and then run the following command to get the latest iOS packages. Our instructions above list out the full version of the iOS push package. If you want to automatically increment packages, you can remove the patch and minor build numbers (the second and third parts of the version number), and pod update will automatically fetch the latest package versions. However, please understand that fetching the latest versions can cause build issues if the latest iOS package doesn’t agree with code in your app! pod update --repo-update --project-directory=ios Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Error: Push notifications not working If push notifications don’t work, make sure that you’ve initialized the Customer.io SDK in your AppDelegate.swift file. You must initialize the SDK in the application(_:didFinishLaunchingWithOptions:) method. @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } } Deep linking to iOS when your app is killed There’s a known issue preventing deep links from working when your app is closed on iOS devices. When the app is in a closed state, the native click event fires before the app’s lifecycle begins. We recommend a workaround: Update didFinishLaunchingWithOptions in your AppDelegate.swift file with the code below. We extract the deep link from the push notification payload and add it to the launch options, ensuring that your React Native app receives the link when it starts. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) ... if var launchOptions = launchOptions, let remotePush = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [String: [String: [String: String]]], let link = remotePush["CIO"]?["push"]?["link"], let url = URL(string:link), launchOptions[UIApplication.LaunchOptionsKey.url] == nil { launchOptions[UIApplication.LaunchOptionsKey.url] = url } let appName = Bundle.main.displayName factory.startReactNative( withModuleName: appName, in: window, initialProperties: ["appName": appName], launchOptions: launchOptions ) ... } Compiler error: ‘X’ is unavailable in application extensions for iOS This error occasionally occurs when users add a notification extension to handle rich push messages. If you see this error, try the following steps: Add this code to the end of your Podfile: post_install do |installer| installer.pods_project.targets.each do |target| if target.name.start_with?('CustomerIO') puts "Modifying target #{target.name} with workaround" target.build_configurations.each do |config| puts "Setting build config settings for #{target.name}" config.build_settings['APPLICATION_EXTENSION_API_ONLY'] ||= 'NO' end end end end In the root directory of your app, run pod install --project-directory=ios. This command will apply the above workaround to your project. Try to compile your app again. If you still see the error message, it’s likely that the error you see is related to a different SDK that you use in your app and not the Customer.io SDK. We suggest that you contact the developers of the SDK that you see in the error message for help. If you don’t see an error message, send our technical support team a message with: The error message that you see when compiling your app. The contents of your ios/Podfile and ios/Podfile.lock files. The version of the React Native SDK that you are using. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/react-native/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood: "Pizza", favouriteDrink: "Mango Shake" }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify( userId: "new.person@example.com", traits: { first_name: "New", last_name: "Person" })  --- ## Track events URL: https://docs.customer.io/integrations/sdk/react-native/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import { CustomerIO } from "customerio-reactnative"; CustomerIO.track("event_name", { propertyName: propertyValue });  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.track("User Deleted) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/react-native/tracking/screen-events/ Screen events track the screens people view in your app. Beyond tracking the parts of your app people use, screen tracking is vital for in-app messages because they target specific screens. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using React Navigation for automatic screen tracking. This example requires @react-navigation/native and @react-navigation/native-stack to create a navigation container in App.js If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useRef } from 'react'; const Stack = createNativeStackNavigator(); export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} > <Stack.Navigator initialRouteName="FirstScreen"> <Stack.Screen name="FirstScreen" component={FirstScreen}/> <Stack.Screen name="SecondScreen" component={SecondScreen} options={{ title : "My App", headerStyle: { backgroundColor: '#F6F7F9', }, }}/> </Stack.Navigator> </NavigationContainer> ); }; Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.InApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory region: CioRegion.US, screenViewUse: ScreenView.All trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/react-native/tracking/lifecycle-events/ By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. CustomerIO.track("Application Crashed", { url: "/page/in/app" }); Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in the CioConfig object when you initialize the SDK. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, trackApplicationLifecycleEvents: false, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration } }; CustomerIO.initialize(config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/react-native/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their screen view. This triggers the campaign you set up for people who visited the product page. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Location tracking URL: https://docs.customer.io/integrations/sdk/react-native/tracking/location/ Real-time location tracking lets you update a person's profile with accurate coordinates so you can send geo-aware messages and segment users by location. How it works The Location module captures location (with user consent) from your app and attaches it to a person’s profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than IP-based geolocation. When you identify a person, the SDK includes the latest location in the identify call. The SDK also sends a Location Update event to the person’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update. The SDK does not request location permission on its own—your app must handle the permission flow. Install the location module The Location module requires native dependencies on both iOS and Android. Follow the platform-specific steps below. iOS Add the CioLocation package. If you use Swift Package Manager, add it the same way you added the other Customer.io packages. It’s part of the customerio-ios package. If you use CocoaPods, add the pod to your Podfile: pod 'CustomerIO/Location' Android Add the location dependency to your app’s build.gradle file: implementation 'io.customer.android:location:<version-here>' Initialize the SDK with the location module Add a location object to your CioConfig to enable the module. The trackingMode property controls how and when the SDK captures location. Option Type Default Description trackingMode CioLocationTrackingMode Manual Controls how and when the SDK captures location. See tracking modes below. Tracking modes Mode Description CioLocationTrackingMode.Manual Your app controls when it captures location. Call setLastKnownLocation() or requestLocationUpdate() to provide location. Use this when your app already has a location-tracking mechanism or you want full control over when you capture location data. CioLocationTrackingMode.OnAppStart The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call setLastKnownLocation() or requestLocationUpdate() alongside automatic capture. Use this for hands-off location tracking with minimal battery impact. CioLocationTrackingMode.Off Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime. import { CustomerIO, CioConfig, CioLocationTrackingMode } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'your-cdp-api-key', // ...other config options location: { trackingMode: CioLocationTrackingMode.Manual, }, }; CustomerIO.initialize(config); Location APIs The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a Location Update event no more than once a day—and only if the person has moved a meaningful distance since the last update. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates. setLastKnownLocation Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io. Parameter Type Description latitude number Latitude in degrees. Must be between -90 and 90. longitude number Longitude in degrees. Must be between -180 and 180. import { CustomerIO } from 'customerio-reactnative'; // Pass coordinates from your app's location provider CustomerIO.location.setLastKnownLocation(37.7749, -122.4194); requestLocationUpdate Request a one-shot location from the native platform’s location services. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user. If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes. Add the required key to your Info.plist: <key>NSLocationWhenInUseUsageDescription</key> <string>We use your location to personalize your experience.</string> Add the permission to your AndroidManifest.xml: <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- Optional: for more precise location --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> After your app requests and receives permission at runtime, call the SDK: import { CustomerIO } from 'customerio-reactnative'; // After location permission is granted CustomerIO.location.requestLocationUpdate();  We recommend using a library like react-native-permissions or expo-location to handle permission requests in your React Native app. Profile switch behavior When you call CustomerIO.clearIdentify(), the SDK clears cached location data so that one person’s location doesn’t carry over to another person’s profile. The next person you identify starts with a clean slate. Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next identify() call includes it automatically. --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/push/ Our React Native SDK supports push notifications over APN or FCM-including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How it works Under the hood, our React Native SDK takes advantage of our native Android and iOS SDKs. This helps us keep the React Native SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Add push notification capabilities in XCode. (iOS) Integrate push notifications: code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. Request, or check for, push notification permissions. If your app’s user doesn’t grant permission, notifications will not appear in the system tray. While push providers support a number of features in their payloads, our React Native package only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities, but there are still a few more steps to finish setting things up. Go to File > New > Target. Select Notification Service Extension and click Next. Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) and set the Language to Swift or Objective C based on the language you use for native iOS files. Click Finish. When presented with the dialog below, click Cancel. This helps Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Pick your push provider (APN or FCM) and the language your native files are written in to get started (Objective C or Swift). APN/Objective-CAPN/SwiftFCM/Objective-CFCM/Swift APN/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import Foundation import CioMessagingPushAPN @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // Initialize MessagingPushAPN module to automatically handle push notifications that originate from Customer.io MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Register device to receive push notifications with device token MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } Open your ios/AppDelegate.mm file and import your header file. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. If you’re not a native iOS developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import "SampleApp-Swift.h" Inside AppDelegate’s @implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to register a device to the current app user and handle push notifications. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RCTAppSetupPrepareApp(application, true); NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; #if RCT_NEW_ARCH_ENABLED _contextContainer = std::make_shared<facebook::react::ContextContainer const>(); _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>(); _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; #endif NSDictionary *initProps = [self prepareInitialProps]; UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); [application registerForRemoteNotifications]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [pnHandlerObj setupCustomerIOClickHandling]; [RNNotifications startMonitorNotifications]; return YES; } ... // Required to register device token. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Register device to receive push notifications with device token [pnHandlerObj application:application deviceToken:deviceToken]; } // Required for the registration error event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [pnHandlerObj application:application error:error]; } In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the code on the right and paste it into this new file (which we’ve called NotificationServicePushHandler.swift) file-replacing everything in the file and update Env.cdpApiKey with your CDP API key. import CioMessagingPushAPN import Foundation import UserNotifications @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Open your NotificationService.m file and copy the highlighted lines (beginning on line 2) into your file. The name of the header file on line 2 will depend on your extension’s name i.e. YourNotificationServiceExtensionName-Swift.h and is automatically created by Xcode. After this, you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import "NotificationService.h" #import "NotificationServiceExtension-Swift.h" @interface NotificationService () @end @implementation NotificationService // Create object of class NotificationServicePushHandler NotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[NotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. [nsHandlerObj serviceExtensionTimeWillExpire]; } @end APN/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios In your iOS subfolder, update your AppDelegate.swift file to use the Customer.io wrapper class that handles push notifications automatically. This approach replaces the need to manually implement push notification delegate methods. import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Optional: add if you need UNUserNotificationCenterDelegate methods UNUserNotificationCenter.current().delegate = self // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want more control over notifications when your app is in the foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushAPN import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import CioMessagingPushFCM import CioFirebaseWrapper import FirebaseMessaging import Foundation @objc public class MyAppPushNotificationsHandler: NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // Initialize MessagingPushFCM module to automatically handle your app's push notifications that originate from Customer.io MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) } @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Register device on receiving a device token (FCM) MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Open AppDelegate.h and add the FIRMessagingDelegate import statement. If you’re not a native Objective-C developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> @interface AppDelegate : RCTAppDelegate <FIRMessagingDelegate> @end Open your ios/AppDelegate.mm file and import your header file, as we’ve shown on line 2 and also import FirebaseCore as we’ve shown on line 5. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. #import "AppDelegate.h" #import <FCMSampleApp-Swift.h> #import <React/RCTLinkingManager.h> #import <React/RCTBundleURLProvider.h> #import <FirebaseCore.h> In your AppDelegate.mm file, create an object of your push notification handler. We called ours MyAppPushNotificationsHandler. @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler *pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to configure Firebase and handle tokens. We’ve highlighted the code here to show what you need to add. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"FCMSampleApp"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; // Configure Firebase [FIRApp configure]; // Set FCM messaging delegate [FIRMessaging messaging].delegate = self; // Use modifiedLaunchOptions for passing link to React Native bridge to sends users to the specified screen NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } [pnHandlerObj setupCustomerIOClickHandling]; return [super application:application didFinishLaunchingWithOptions:modifiedLaunchOptions]; } ... @end In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy this code into the new file and replace Env.cdpApiKey with your CDP API key. import CioMessagingPushFCM import CioFirebaseWrapper import Foundation import UserNotifications @objc public class MyAppNotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file-e.g. NotificationServiceExtension-Swift.h. You’ll also need to create an object class of MyAppNotificationServicePushHandler and call the functions in the code sample here. Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import <NotificationServiceExtension-Swift.h> #import "NotificationService.h" @interface NotificationService () @end @implementation NotificationService // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize { nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest*)request withContentHandler:(void (^)(UNNotificationContent* _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end FCM/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. When complete, you should see Pod installation complete! pod install --project-directory=ios Update your AppDelegate.swift file to handle push notifications. You can copy the code sample on the right, but you may need to update imports and other things to fit your app. import UIKit import CioMessagingPushFCM import CioFirebaseWrapper import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... // Configure Firebase FirebaseApp.configure() // Optional: Set FCM messaging delegate if you need it. The Customer.io SDK will automatically read FCM tokens Messaging.messaging().delegate = self // Initialize MessagingPushFCM module to automatically handle your app's push notifications that originate from Customer.io MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // This is required for FCM. The Customer.io SDK does not make changes to other SDKs Messaging.messaging().apnsToken = deviceToken } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want fine-grained control over presenting Notifications in foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } extension AppDelegate: MessagingDelegate { // Optional: add this method if you need access to `fcmToken` - Customer.io SDK will read this automatically func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushFCM import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) // Optional: set App Group ID for reliable push delivery tracking // .appGroupId("group.com.example.myapp.cio") .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Push icon (Android) You’ll set the icon that appears on normal push notifications as a part of your app manifest-android/app/src/main/AndroidManifest.xml. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user has not yet been asked to opt into notifications, the method will return NotDetermined (only for iOS). var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Optional: Remove POST_NOTIFICATIONS permission from Android apps By default, the SDK includes the POST_NOTIFICATIONS permission which is required by Android 13 to show notifications on Android device. However, if you do not want to include the permission because don’t use notifications, or for any other reason, you can remove the permission by adding the following line to your android/app/src/main/AndroidManifest.xml file: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/> Fetch the current device token You can fetch the currently stored device token using the CustomerIO.pushMessaging.getRegisteredDeviceToken() method. This method returns an APN/FCM token in a promise as a string. let token = await CustomerIO.pushMessaging.getRegisteredDeviceToken() if (token) { // Use the token as required in your app for example save in a state setDeviceToken(token); } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. Next steps App Groups for push tracking: Configure App Groups for reliable delivery tracking. Without App Groups, iOS can end the Notification Service Extension before the SDK finishes tracking delivery, causing lost metrics. --- ## App Groups for push tracking URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/app-groups/ Configure App Groups for reliable push delivery tracking. App Groups let the SDK recover delivery metrics that iOS might otherwise discard when it terminates the Notification Service Extension. App Groups are required for reliable push delivery tracking on iOS. Without this setup, delivery metrics may be lost if iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers these lost metrics on the next app launch. Before you begin Before you configure App Groups, make sure you’ve completed the following: Set up push notifications in your React Native app Added a Notification Service Extension as part of the push setup 1. Add the App Group capability in Xcode You need to add the App Groups capability to both your main app target and your Notification Service Extension target in Xcode. Automatic signing Automatic signing If you use automatic signing (the most common setup), this is the only step outside of code. Xcode registers the group and updates provisioning profiles automatically. In Xcode, select your main app target > Signing & Capabilities > + Capability > App Groups. Click + and enter your group identifier—for example, group.com.example.myapp.cio. Select your Notification Service Extension target > Signing & Capabilities > + Capability > App Groups. Select the same App Group you created in step 2. Both targets must reference the exact same App Group string. Manual signing Manual signing If you use manual signing, you need to register the group in the Apple Developer Portal and regenerate your provisioning profiles. Sign in to the Apple Developer Portal and go to Certificates, Identifiers & Profiles. Click Identifiers > + > App Groups. Enter your identifier. It must start with group.—for example, group.com.example.myapp.cio. Under Identifiers, select your main app’s App ID, enable App Groups, click Configure, and select your group. Repeat step 4 for your Notification Service Extension’s App ID. Regenerate provisioning profiles for both your main app and Notification Service Extension. Enabling App Groups invalidates your existing provisioning profiles.  You must regenerate your provisioning profiles in the Apple Developer Portal after enabling App Groups. You don’t need to regenerate certificates.  Already have an App Group? You can reuse an existing App Group by passing its identifier to .appGroupId(). There’s no conflict in having multiple App Groups on a target. 2. Pass the App Group ID in your SDK configuration After you add the App Group capability, you need to pass the App Group ID to the SDK in both your host app and Notification Service Extension. Both are required—App Groups work by sharing storage between the two targets, so the SDK needs the identifier in each place to read and write delivery metrics. The App Group ID must be identical in both places and must match the entitlements you set up in Xcode. Because the React Native SDK uses native iOS code for push initialization, you configure .appGroupId() in your Swift files—not in JavaScript or TypeScript. Host app initialization APNs APNs // In your AppDelegate.swift MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder() .appGroupId("group.com.example.myapp.cio") .build() ) FCM FCM // In your AppDelegate.swift MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .appGroupId("group.com.example.myapp.cio") .build() ) Notification Service Extension initialization APNs APNs MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .appGroupId("group.com.example.myapp.cio") .build() ) FCM FCM MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .appGroupId("group.com.example.myapp.cio") .build() )  App Group ID must match everywhere The .appGroupId() value must be identical in your host app initialization, your NSE initialization, and the App Group entitlements on both targets. A mismatch prevents the SDK from accessing shared storage. Fallback behavior If you omit .appGroupId(...), the SDK attempts to infer the identifier using group.{bundleId}.cio. This can work as a fallback, but we recommend explicitly passing the value to avoid configuration issues. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior The push.android.pushClickBehavior config option controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) The available options are: ActivityPreventRestart (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ActivityNoFlags: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. ResetTaskStack: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links After you set up push notifications you can enable deep links in rich push notifications. There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps.  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Open your AppDelegate.swift file and add the code below. Note that the Customer.io SDK automatically forwards deep links from Customer.io push notifications to the application(:open:options:) method. For push notifications from other providers, you still need to handle deep links manually in the userNotificationCenter(didReceive:withCompletionHandler:) method. import UIKit import CioMessagingPushAPN import React @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // Handle deep links return RCTLinkingManager.application(app, open: url, options: options) } } Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) Set up Universal Links Follow React Native’s documentation to implement Universal Links in your app. --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/multiple-push-providers/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If Customer.io is the only SDK that you use in your app to display push notifications, then you don’t need to do anything special to display push notifications. But, if you use another module in your app that can display push notifications like expo-notifications, react-native-push-notification, or rnfirebase, these modules can take over push handling by default and prevent your app from receiving push notifications from Customer.io. You can solve this problem using one (and only one) of the methods below, but we typically recommend the first option, because it doesn’t require you to write native code! Please note that the following methods will always return true for iOS. Option 1 (Recommended): Set Customer.io SDK to handle push clicks You can pass the payloads of other message services to Customer.io whenever a device receives a notification, and our SDK can process it for you. The SDK exposes the onMessageReceived method for this that takes two arguments: a message.data object containing the incoming notification payload a handleNotificationTrigger boolean indicating whether or not to trigger a notification. true (default) means that the Customer.io SDK will generate the notification and track associated metrics. false means that the SDK will process the notification to track metrics but will not generate a notification on the device. You’ll use the onMessageReceived like this: CustomerIO.pushMessaging.onMessageReceived(message).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); You can pass values in onMessageReceived by listening to notification events exposed by other SDKs. Make sure that you add listeners in the right places to process notifications that your app receives when it’s in the foreground and add background listeners that might be required by other SDK to process notifications that your app receives when it’s in background/killed state. If you always send rich push messages (with image and/or link), adding event listeners is enough. But if you send custom push payloads using the notification object or send simple push messages (with just a body and title), you may get duplicate notifications when your app is backgrounded because Firebase itself displays notifications sent using the notification object. To avoid this, You can pass false in handleNotificationTrigger to track metrics for simple and custom payload push notifications. To simplify this behavior, the SDK also exposes an onBackgroundMessageReceived method that automatically suppresses pushes with the notification object when your app is in background. If you use rnfirebase, you can setup listeners like this: Foreground Listener Foreground Listener To listen to messages in the foreground, set onMessage listener where appropriate: useEffect(() => { const unsubscribe = messaging().onMessage(async remoteMessage => { CustomerIO.pushMessaging.onMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); return unsubscribe; }, []); Background Listener Background Listener To listen to messages when app is in background/killed state, set setBackgroundMessageHandler in your index.js file messaging().setBackgroundMessageHandler(async remoteMessage => { CustomerIO.pushMessaging.onBackgroundMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); Option 2: Register Customer.io Messaging Service You can register Customer.io’s messaging service in your Manifest file so that we handle all notifications for your app. You can do this by adding the following code under the <application> tag in the AndroidManifest.xml file in your app’s android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  The Customer.io SDK will handle all your push notifications The code above hands all push notifications responsibility to our SDK, meaning: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. Manually track push metrics If you need to manually track push metrics when you use multiple push providers (like when you display notifications yourself or use another library), you can parse a push notification payload and send opened or delivered events to the SDK in relevant callbacks: CustomerIO.trackMetric({ deliveryID: deliveryID, deviceToken: deviceToken, event: MetricEvent.Opened, }); The trackMetric method requires the following parameters: deliveryID: The delivery ID extracted from the push notification payload deviceToken: The device token extracted from the push notification payload event: The metric event type, either MetricEvent.Opened or MetricEvent.Delivered --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. The SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. You don’t have to add any code to track opened push metrics or launch deep links.  Improve delivery metric reliability Configure App Groups to make sure delivery metrics aren’t lost when iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers any undelivered metrics on the next app launch.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. You can configure this behavior by adding the following configuration to the class that you created as a part of our push notification setup instructions in your AppDelegate.swift file. // In your AppDelegate.swift @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with foreground push display option MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().showPushAppInForeground(true).build() ) return true } } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually record push metrics using Javascript methods  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking After you set up push notifications, update your AppDelegate.swift file to disable automatic push notification tracking: // In your AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with auto-tracking disabled MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().autoTrackPushEvents(false).build() ) return true } --- ## Android channels URL: https://docs.customer.io/integrations/sdk/react-native/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v4.5.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io React-Native SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io React-Native SDK prior to 4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io React Native SDK v4.5.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/in-app-messages/set-up-in-app/ This page describes how to implement mobile in-app messages. How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set the inApp.siteId option in your CioConfig, and your app will be able to receive in-app messages. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key to generate them. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', } }; Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. Make sure your page rules match the casing of the title in your screen events. Anonymous messages As of version 4.11, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages You’ll need to include a UI element in your app UI to render inline messages. The view will automatically adjust its height when messages are loaded or interacted with.  We’ve set up examples in our sample apps that might help if you want to see a real-world implementation of this feature. Add the InlineInAppMessageView component to your React Native app: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { return ( <InlineInAppMessageView elementId="my-message" onActionClick={(message, actionValue, actionName) => { console.log('Action clicked:', { message, actionValue, actionName }); }} /> ); } View layout The InlineInAppMessageView automatically adjusts its height at runtime when messages load or users interact with them. You should avoid setting a fixed height on this component as it might interfere with message rendering. You’re responsible for setting layout styles to position your view correctly (width, margins, padding, and so on). The component will handle its own height dynamically. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. If the editor says that the inline display feature is Web/iOS only, don’t worry about that. We’re working on updating this UI. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow the steps below to implement custom actions for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events There are two ways to listen for these click events in inline in-app messages. Register a callback with your inline view: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { const handleActionClick = (message, actionValue, actionName) => { // Perform some logic when people tap an action button. // Example code handling button tap: switch (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionName); break; // You can add more cases here for other actions default: // Handle unknown actions or do nothing console.log("Unknown action:", actionValue); } }; return ( <InlineInAppMessageView elementId="my-message" onActionClick={handleActionClick} /> ); } Register a global SDK event listener. When you register an event listener with the SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic for inline and non-inline messages if you want. Handle responses to messages (event listeners) Like modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionClick callback set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/react-native/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging.registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## Notification inbox URL: https://docs.customer.io/integrations/sdk/react-native/in-app-messages/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io delivers inbox messages as JSON payloads, not fully-rendered messages. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message. Get the inbox instance You’ll access inbox functionality through the inbox() method on the in-app messaging module. const inbox = CustomerIO.inAppMessaging.inbox(); Inbox methods The inbox instance provides several methods to manage messages. Method Description getMessages() Fetch all messages from the inbox. Returns a Promise with the list of messages. getMessages(topic) Fetch messages filtered by topic. Returns a Promise with the filtered list of messages. subscribeToMessages(callbacks) Subscribe to inbox updates for all messages. Returns a subscription object. subscribeToMessages(callbacks, topic) Subscribe to inbox updates filtered by topic. Returns a subscription object. markMessageOpened(message) Mark a message as opened. markMessageUnopened(message) Mark a message as unopened. markMessageDeleted(message) Mark a message as deleted. trackMessageClicked(message) Track a click on the message without an action name. trackMessageClicked(message, actionName) Track a click on the message with an action name. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Fetch messages // Fetch all messages const messages = await inbox.getMessages(); // Fetch messages filtered by topic const promotions = await inbox.getMessages('promotions'); Subscribe to inbox updates // Subscribe to all messages const subscription = inbox.subscribeToMessages({ onMessagesChanged: (messages) => { console.log('Messages updated:', messages); // Update your UI with the messages setMessages(messages); } }); // Subscribe to messages filtered by topic const topicSubscription = inbox.subscribeToMessages({ onMessagesChanged: (messages) => { console.log('Topic messages:', messages); // Update your UI with filtered messages setTopicMessages(messages); } }, 'announcements'); // Don't forget to unsubscribe when you're done subscription.remove(); topicSubscription.remove(); Mark messages as opened or unopened // Mark a message as opened inbox.markMessageOpened(message); // Mark a message as unopened inbox.markMessageUnopened(message); Track message clicks // Track a click without an action name inbox.trackMessageClicked(message); // Track a click with an action name inbox.trackMessageClicked(message, 'view_offer'); Delete messages // Mark a message as deleted inbox.markMessageDeleted(message); Working with message properties You can access message properties to display custom content in your inbox: // Access message properties const { title, body, link, image } = message.properties; // Handle message action when user taps const handleMessagePress = (message) => { // Mark as opened inbox.markMessageOpened(message); // Track click inbox.trackMessageClicked(message); // Open link if available if (message.properties.link) { Linking.openURL(message.properties.link); } }; // Track with specific action name const handleActionButton = (message, actionType) => { inbox.markMessageOpened(message); inbox.trackMessageClicked(message, actionType); // Navigate based on action switch (actionType) { case 'view_offer': navigation.navigate('OfferDetails', { offerId: message.properties.offerId }); break; case 'view_order': navigation.navigate('OrderDetails', { orderId: message.properties.orderId }); break; } }; --- ## 6.x -> 6.4.0 URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/6.4.0-upgrade/ Version 6.4.0 adds App Groups support for more reliable push delivery metric tracking. This update is additive—existing integrations work without modification. What changed? Version 6.4.0 adds support for App Groups, which improves the reliability of push delivery metric tracking on iOS. This update is additive—your existing integration continues to work without changes. However, to take advantage of App Groups, you’ll need to update your Xcode project configuration and regenerate provisioning profiles if you use manual signing. Why App Groups? When you send a push notification, the SDK tracks delivery metrics in your Notification Service Extension (NSE). However, iOS can end the NSE before the tracking request completes, which means some delivery data may never reach Customer.io. App Groups provide shared storage between your main app and the NSE, so the SDK can save metrics and recover them on the next app launch. Upgrade process Update to version 6.4.0 or later of the Customer.io React Native SDK. Follow the App Groups setup instructions to configure your Xcode project and pass .appGroupId() to the SDK in your native iOS code. No other code changes are required. --- ## 5.x -> 6.0.0 URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/6.x-upgrade/ Version 6.0.0 of the Customer.io React Native SDK requires the React Native new architecture. What changed? Version 6.0.0 removes support for React Native’s legacy architecture. This aligns with React Native’s move to exclusively support their new architecture. You must migrate your app to use React Native’s new architecture to use this and future versions of the SDK. Do you need to update to this version? We recommend updating to the latest SDK version. However, if your app uses the old React Native architecture and you’re not ready to migrate, you can continue using version 5.x until you’re ready to adopt the new architecture. Prerequisites Before updating to SDK version 6.0.0, your app must use React Native’s new architecture. See React Native’s documentation for more information. Update process If your app already uses React Native’s new architecture, updating to Customer.io SDK version 6.0.0 is straightforward. There are no public API changes. 1. Migrate to the new React Native architecture If you haven’t migrated to React Native’s new architecture yet, see the React Native documentation for instructions. When your app is successfully running on the new architecture, you can update to Customer.io SDK version 6.0.0. 2. Update the SDK version Update your package.json to use version 6.0.0: npm install customerio-reactnative@6.0.0 # or yarn add customerio-reactnative@6.0.0 3. Install dependencies For a clean installation to ensure codegen works properly: # Clean install rm -rf node_modules npm install # iOS cd ios && rm -rf Pods Podfile.lock && pod install && cd .. # Rebuild your app npm run ios # or npm run android 4. Test your integration Since there are no public API changes, your existing Customer.io SDK calls will continue to work. However, you should test your app after updating to ensure everything works as expected. Troubleshooting If you encounter build errors after updating, perform a clean build: # Clean install rm -rf node_modules npm install # iOS cd ios && rm -rf Pods Podfile.lock && pod install && cd .. # Android cd android && ./gradlew clean && cd .. # Rebuild npm run ios npm run android If issues persist, ensure your app is properly configured for the React Native new architecture and that all dependencies support it.  Try our MCP server! Our MCP server includes an integration tool that can help you install and troubleshoot issues with our SDK, including problems with push and in-app notifications. See our MCP server documentation for more information. --- ## 4.x -> 5.0.0 URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/5.x-upgrade/ Version 5.x of the Customer.io React Native SDK introduces Firebase wrapper support for FCM users that improves Firebase compatibility and simplifies push notification setup. What changed? Version 5.x introduces a Firebase wrapper that improves compatibility with Firebase Cloud Messaging (FCM) and other Firebase services in your app. Do you need to update to this version? You need to update to this version if you use FCM (Firebase Cloud Messaging) for push notifications Update process Add the CioFirebaseWrapper import to your Swift files that use CioMessagingPushFCM. Add the Firebase wrapper import to your AppDelegate.swift file: import UIKit import CioMessagingPushFCM import CioFirebaseWrapper // Add this import for FCM users import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} Troubleshooting If you see build errors related to Firebase after upgrading: Clean your build: Run cd ios && pod install && cd .. then rebuild Check imports: Ensure you’ve added import CioFirebaseWrapper to all files that import CioMessagingPushFCM --- ## 4.x -> 4.3 URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/4.3-upgrade/ Version 4.3 of the Customer.io React Native SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 4.3 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your app depending on whether you send push notifications with APN or FCM and whether you use UIKit or SwiftUI. Update with APN (Apple Push Notification service) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } After (4.3) After (4.3) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper - handles all push methods automatically MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - CioAppDelegateWrapper handles everything } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Update with FCM (Firebase Cloud Messaging) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token } } After (4.3) After (4.3) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Initialize push FCM with wrapper - handles all push methods automatically MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - wrapper handles everything } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token - Customer.io SDK will also receive this automatically } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Important Notes Manual push handling methods are not required: the CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError didReceiveRemoteNotification All other push-related delegate methods The @main attribute - Must be on the wrapper class, not your AppDelegate. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushAPN.initialize() or MessagingPushFCM.initialize() If you encounter some unexpected behavior and want to test is it related to new Push Notification tracking system, just comment the following line and compare class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} with the original AppDelegate. --- ## 3.4x -> 4.x URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/4.x-upgrade/ This page provides steps to help you upgrade from react native 3.4 or later so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Update your initialization You’ll initialize the new version of the SDK and its packages with CioConfig objects instead of CustomerioConfig. While we’ve listed all the new configuration options, you’ll want to pay close attention to the following changes: CustomerIOEnv is no longer necessary. Region becomes CioRegion. siteId becomes migrationSiteId. You’ll initialize the SDK with initialize(config) instead of initialize(env, config). If you previously used the backgroundQueueMinNumberOfTasks or backgroundQueueSecondsDelay options, you should remove them from your configuration as well. These options are no longer supported, and may cause build errors if you use strict type checking. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) 3. Update your AppDelegate push notification handler In your MyAppPushNotificationsHandler.swift (or the associated file where you add a push notification handler in your main target), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import CioMessagingPushAPN // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } FCM FCM import Foundation import CioMessagingPushFCM import FirebaseMessaging // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: Region.US) { config in } // update this line to MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) } // Register device on receiving a device token (FCM) @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } 4. Update your NotificationService push notification handler In your NotificationServicePushHandler.swift (or the associated file where you add a push notification handler in NotificationServiceExtension), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import UserNotifications import CioMessagingPushAPN // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM FCM import Foundation import UserNotifications import CioMessagingPushFCM // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } 5. Update your identify call Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track and screen calls are structured the same as previous versions, but the data object is now called properties. We’ve highlighted changes in the sample below. //identify: identifier becomes userId, body becomes traits CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); //track: no significant change to method //in Customer.io data object renamed properties CustomerIO.track("track_event_name", { propertyName: propertyValue }); //screen: no significant change to method. //name becomes title, data object renamed properties CustomerIO.screen("screen_event_name", { propertyName: propertyValue }); Configuration Changes As a part of this release, we’ve changed a few configuration options when you initialize the SDK. You’ll use CioConfig to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data into Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). inApp object Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. push object Replaces the former enablePush option, providing a place to set push configuration options. For now, it only takes the android.pushClickBehavior setting. backgroundQueueMinNumberOfTasks removed This option is no longer available. backgroundQueueSecondsDelay removed This option is no longer available. --- ## 3.x -> 3.4 URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/update-to-3.4/ This page explains how to update your SDK install to latest versions that may not require a breaking change. While these changes aren't breaking—you don't _need_ to make these changes—they will simplify your integration, improve the reliability of your metrics, and improve deep link handling on iOS devices. Upgrade from 3.3 to 3.4+ As of version 3.4, the Customer.io SDK automatically registers push device tokens to identified people and handles push clicks. These features simplify your SDK integration while improving compatibility with apps that use multiple push SDKs. After you install a version of the SDK that is 3.4 or higher, follow these steps to upgrade.  Do you have a swift app? Skip ahead! If you’ve got a Swift app containing the AppDelegate.swift file, ignore the steps below and go to the Swift upgrade section. Open your push notification handler file (In our examples, we call this file MyAppPushNotificationsHandler.swift) and review all of the highlighted code below. We’ve highlighted the most relevant lines. import Foundation import CioMessagingPushAPN import UserNotifications // Delete this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} // Replace these 2 lines @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // With these 2 lines @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } // Delete these 2 lines: let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } // Delete this function: @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } } Open your AppDelegate.h file and review all of the highlighted code below. APN APN #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate @end FCM FCM #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate, UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate> @end Open your AppDelegate.m file and review all of the highlighted code below. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... // Replace this line [pnHandlerObj setupCustomerIOClickHandling:self]; // With this line: [pnHandlerObj setupCustomerIOClickHandling]; return YES; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { // Remove the line below: [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } Now that your app’s code has been simplified, follow the latest push notification setup documentation to enable these new features. Upgrade from 3.3 to 3.4+, for Swift Open your AppDelegate.swift file and review all of the highlighted code below. We’ve highlighted the most relevant lines. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Delete this line UIApplication.shared.registerForRemoteNotifications() return true } } // Delete this function func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } // Delete this function func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/update-to-3x/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Installing and updating our React Native SDK got easier. After you install the CustomerIO React Native SDK version 3.x, open your ios/Podfile and follow all 5 steps shown in this code block below: # 1. This line is required by the FCM SDK. If you encounter problems during 'pod install', add this line to your Podfile and try 'pod install' again. use_frameworks! :linkage => :static target 'YourApp' do # Note: 'YourApp' is unique to your app. This is here for example purposes, only. # 2. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me # 3. Add one of these new lines below: # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end target 'NotificationServiceExtension' do # 4. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me pod 'FirebaseMessaging' # Remove me, unless you need to specify a specific version pod 'Firebase' # Remove me, unless you need to specify a specific version. # 5. Add one of these new lines below: # ⚠️ Important: Notice these lines of code include "-richpush" in it making it unique to the host app target above. # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end After you modify your Podfile, run the command pod update --repo-update --project-directory=ios to make your changes to ios/Podfile go into effect. Upgrade from 1.x to 2.x Rich push initialization(iOS) If you followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Due to the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your Notification Service Extension. In the case that you use Objective-C, you must add the code snippet below into the Swift handler file that you created in NotificationService Extension. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Firebase users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. If you send messages to your iOS app using FCM, you’ll need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. pod 'Firebase' pod 'FirebaseMessaging' We fixed a bug in our iOS modules that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in our iOS v1 modules that converted keys in your custom data to snake_case. This bug is fixed in v2 of the SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect your Android users.. // If you passed in custom attributes using camelCase keys: data = {"firstName": "Dana"} // The SDK v1 may have converted this data into: data = {"first_name": "Dana"} // Or, if you used a different format that was not snake_case: data = {"FIRSTNAME": "Dana"} // The SDK v1 may have converted this data into: data = {"f_irstname": "Dana"} You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Call the Customer.io SDK and provide custom attributes like this: CustomerIO.identify("dana@example.com", {"first_name": "Dana"}) // Consider sending duplicate data with snake_case CustomerIO.identify("dana@example.com", { "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. }) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.identify("dana@example.com", { "firstName": "Dana" // We can remove the snake_case attribute and go back to just camelCase! }) --- ## Changelog URL: https://docs.customer.io/integrations/sdk/react-native/whats-new/changelog/ Check out release history our React Native SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. test --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/react-native/4.x/quick-start-guide/ React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages. Install the SDK. Identify and Track Push Notifications In-App 1. Install the SDK You need to add a new React Native connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration. in Customer.io to get your CDP API key. See Get your CDP API key for details. Make sure you set up your React Native environment first. You must use React Native 0.79 or later. Open your terminal and go to your project folder. Install the customerio-reactnative package using NPM or Yarn: npm install customerio-reactnative # or yarn add customerio-reactnative Set up your project to support iOS and/or Android deployments: iOS iOS For iOS, install CocoaPods dependencies: pod install --repo-update --project-directory=ios Make sure your minimum iOS deployment target is set to 13.0 in both your Podfile and Xcode project settings. Android Android For Android, include the Google Services plugin by adding the following to your project-level android/build.gradle file: buildscript { repositories { google() // Google's Maven repository } dependencies { classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { google() // Google's Maven repository } } Add the plugin to your app-level android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and place it in android/app/google-services.json. Add your CDP API key and site ID to your configuration. CDP API Key: You’ll find this key in your React Native connection. Site ID: You’ll find this value in your workspace under Settings > Workspace Settings > API and webhook credentials. Initialize the SDK in your app. Add the following code to your main component or App.js file: import React, { useEffect } from 'react'; import { CioConfig, CioRegion, CioLogLevel, CustomerIO, PushClickBehaviorAndroid } from 'customerio-reactnative'; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', // Required region: CioRegion.US, // Replace with CioRegion.EU if your Customer.ioaccount is in the EU logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'YOUR_SITE_ID', // Required for in-app messaging } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []); Run your application to ensure everything is set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. import { CustomerIO } from "customerio-reactnative"; const identifyUserExample = async () => { await CustomerIO.identify({ userId: 'react-native-test-user@example.com', traits: { firstName: 'John', lastName: 'Doe', email: 'react-native-test-user@example.com', subscriptionStatus: 'active', }, }); console.log('User identified successfully'); }; Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. import { CustomerIO } from "customerio-reactnative"; const trackCustomEventExample = async () => { await CustomerIO.track('purchased_item', { product: 'Premium Subscription', price: 99.99, currency: 'USD' }); console.log('Custom event tracked successfully'); }; Track screen views to trigger in-app messages associated with specific screens. import { CustomerIO } from "customerio-reactnative"; const trackScreenViewExample = async () => { await CustomerIO.screen('ProductDetails'); console.log('Screen view tracked successfully'); }; 3. Push Notifications Set up your push notification credentials in Customer.io: iOS: Upload your Apple Push Notification certificate (.p8 file). Android: Upload your Firebase Cloud Messaging server key (.json format). Request push notification permissions from the user: import { CustomerIO, CioPushPermissionStatus } from "customerio-reactnative"; const requestPushPermissions = async () => { const permissionStatus = await CustomerIO.pushMessaging.showPromptForPushNotifications({ ios: { sound: true, badge: true } }); switch (permissionStatus) { case CioPushPermissionStatus.Granted: console.log('Push notifications enabled'); break; case CioPushPermissionStatus.Denied: case CioPushPermissionStatus.NotDetermined: console.log('Push notifications denied'); break; } }; For iOS: to ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes. For Android: add notification icon resources: Place a notification icon file named ic_notification.png in your drawable folders. Make sure your app’s AndroidManifest.xml has the proper FCM permissions. 4. In-App To enable in-app messaging, all you need to do is add the site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app. You can call the initialize method from your components or services: import { CioConfig, CustomerIO } from "customerio-reactnative"; import { useEffect } from "react"; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', inApp: { siteId: 'YOUR_SITE_ID', } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []);  Check out our code samples! You’ll find a complete, working sample app in our React Native SDK’s example directory. If you get stuck, you can refer to the sample app to see how everything fits together. --- ## How it works URL: https://docs.customer.io/integrations/sdk/react-native/4.x/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum requirements To support the Customer.io SDK, you must: Use React Native versions 0.79 and later. Current versions of XCode don’t support earlier versions of React Native. Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Add React Navigation to your app to support deep links and screen tracking. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/react-native/4.x/getting-started/auth/ To use the SDK, you'll need two kinds of API keys: A *CDP API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. These keys come from different places in Customer.io! CDP API Key: You’ll get this key when you set up your mobile app as a data-in integration in Customer.io. Site ID: This key tells the SDK which workspace your in-app messages come from. You’ll use it to support inApp messages. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your CDP API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your React Native entry under Integrations. If you don’t see your app on this page, you’ll need to add up a new integration. Go to Integrations. Go to the Connections tab and find your React Native connection. If you don’t have a React Native connection, you’ll need to set one up. Go to Settings and find your API Key. Copy this key into the CioConfig.CdpApiKey config option. import { CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Add a new integration If you don’t already have a write key, you’ll need to set up a new connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration.. The connection represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your site ID with the inApp option to support in-app messaging. And if you’re upgrading from a previous version of the SDK, you’ll also use your site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to Settings > Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the inApp package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/react-native/4.x/getting-started/packages-options/ The SDK consists of a few packages. You *must* use the `CioConfig` and `CustomerIO` packages to configure and initialize the React Native SDK. SDK packages The SDK consists of a few packages. You must use the CioConfig and CustomerIO packages to configure and initialize the SDK. Package Product Required? Description CustomerIO ✅ The main SDK package. Used to initialize the SDK and call the SDK’s methods. CioConfig ✅ Configure the SDK including in-app messaging support. CioRegion Used inside the CioConfig.region option to declare your region—EU or US. CioLogLevel Used inside the CioConfig.logLevel option to set the level of logs you can view from the SDK. Configuration options You can determine global behaviors for the SDK in using CioConfig package. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. Note that the logLevel option requires the CioLogLevel package and the region option requires the CioRegion package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io migrationSiteId string Required if you're updating from 3.x: the credential for previous versions of the SDK. This key lets the SDK send remaining tasks to Customer.io when your audience updates your app. region CioRegion.EU or CioRegion.US CioRegion.US Requires the CioRegion package. You must set the region your account is in the EU (CioRegion.EU). autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc screenViewUse All or InApp All ScreenView.All (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events logLevel string error Requires the CioLogLevel package. Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. inApp object Required for in-app support. This object takes a siteId property, determining the workspace your in-app messages come from. push object Takes a single option called PushClickBehaviorAndroid. This object and option controls how your app behaves when your Android audience taps push notifications. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/react-native/4.x/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Capture logs Logs help us pinpoint the problem and find a solution. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import { CustomerIO, CioConfig, CioLogLevel } from 'customerio-reactnative'; const config: CioConfig = { CioApiKey: 'Your CDP API Key', logLevel: CioLogLevel.Debug, } CustomerIO.initialize(config) ; Build and run your app on a physical device or emulator. In the console, run: react-native log-ios react-native log-android Export your log to a text file and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Try updating iOS package dependencies This SDK uses our iOS push package. In some cases, we may make fixes in our iOS packages that fix downstream issues in, or expose new features to this SDK. You can update the version in your podfile and then run the following command to get the latest iOS packages. Our instructions above list out the full version of the iOS push package. If you want to automatically increment packages, you can remove the patch and minor build numbers (the second and third parts of the version number), and pod update will automatically fetch the latest package versions. However, please understand that fetching the latest versions can cause build issues if the latest iOS package doesn’t agree with code in your app! pod update --repo-update --project-directory=ios Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Error: Push notifications not working If push notifications don’t work, make sure that you’ve initialized the Customer.io SDK in your AppDelegate.swift file. You must initialize the SDK in the application(_:didFinishLaunchingWithOptions:) method. @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } } Deep linking to iOS when your app is killed There’s a known issue preventing deep links from working when your app is closed on iOS devices. When the app is in a closed state, the native click event fires before the app’s lifecycle begins. We recommend a workaround: Update didFinishLaunchingWithOptions in your AppDelegate.swift file with the code below. We extract the deep link from the push notification payload and add it to the launch options, ensuring that your React Native app receives the link when it starts. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) ... if var launchOptions = launchOptions, let remotePush = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [String: [String: [String: String]]], let link = remotePush["CIO"]?["push"]?["link"], let url = URL(string:link), launchOptions[UIApplication.LaunchOptionsKey.url] == nil { launchOptions[UIApplication.LaunchOptionsKey.url] = url } let appName = Bundle.main.displayName factory.startReactNative( withModuleName: appName, in: window, initialProperties: ["appName": appName], launchOptions: launchOptions ) ... } Compiler error: ‘X’ is unavailable in application extensions for iOS This error occasionally occurs when users add a notification extension to handle rich push messages. If you see this error, try the following steps: Add this code to the end of your Podfile: post_install do |installer| installer.pods_project.targets.each do |target| if target.name.start_with?('CustomerIO') puts "Modifying target #{target.name} with workaround" target.build_configurations.each do |config| puts "Setting build config settings for #{target.name}" config.build_settings['APPLICATION_EXTENSION_API_ONLY'] ||= 'NO' end end end end In the root directory of your app, run pod install --project-directory=ios. This command will apply the above workaround to your project. Try to compile your app again. If you still see the error message, it’s likely that the error you see is related to a different SDK that you use in your app and not the Customer.io SDK. We suggest that you contact the developers of the SDK that you see in the error message for help. If you don’t see an error message, send our technical support team a message with: The error message that you see when compiling your app. The contents of your ios/Podfile and ios/Podfile.lock files. The version of the React Native SDK that you are using. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/react-native/4.x/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood: "Pizza", favouriteDrink: "Mango Shake" }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify( userId: "new.person@example.com", traits: { first_name: "New", last_name: "Person" })  --- ## Track events URL: https://docs.customer.io/integrations/sdk/react-native/4.x/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import { CustomerIO } from "customerio-reactnative"; CustomerIO.track("event_name", { propertyName: propertyValue });  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.track("User Deleted) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/react-native/4.x/tracking/screen-events/ Screen events track the screens people view in your app. Beyond tracking the parts of your app people use, screen tracking is vital for in-app messages because they target specific screens. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using React Navigation for automatic screen tracking. This example requires @react-navigation/native and @react-navigation/native-stack to create a navigation container in App.js If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useRef } from 'react'; const Stack = createNativeStackNavigator(); export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} > <Stack.Navigator initialRouteName="FirstScreen"> <Stack.Screen name="FirstScreen" component={FirstScreen}/> <Stack.Screen name="SecondScreen" component={SecondScreen} options={{ title : "My App", headerStyle: { backgroundColor: '#F6F7F9', }, }}/> </Stack.Navigator> </NavigationContainer> ); }; Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.InApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory region: CioRegion.US, screenViewUse: ScreenView.All trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/react-native/4.x/tracking/lifecycle-events/ By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. CustomerIO.track("Application Crashed", { url: "/page/in/app" }); Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in the CioConfig object when you initialize the SDK. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, trackApplicationLifecycleEvents: false, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration } }; CustomerIO.initialize(config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/react-native/4.x/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their screen view. This triggers the campaign you set up for people who visited the product page. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/react-native/4.x/push-notifications/push/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How it works Under the hood, our React Native SDK takes advantage of our native Android and iOS SDKs. This helps us keep the React Native SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Add push notification capabilities in XCode. (iOS) Integrate push notifications: code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. Request, or check for, push notification permissions. If your app’s user doesn’t grant permission, notifications will not appear in the system tray. While push providers support a number of features in their payloads, our React Native package only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities, but there are still a few more steps to finish setting things up. Go to File > New > Target. Select Notification Service Extension and click Next. Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) and set the Language to Swift or Objective C based on the language you use for native iOS files. Click Finish. When presented with the dialog below, click Cancel. This helps Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Pick your push provider (APN or FCM) and the language your native files are written in to get started (Objective C or Swift). APN/Objective-CAPN/SwiftFCM/Objective-CFCM/Swift APN/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import Foundation import CioMessagingPushAPN @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // Initialize MessagingPushAPN module to automatically handle push notifications that originate from Customer.io MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Register device to receive push notifications with device token MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } Open your ios/AppDelegate.mm file and import your header file. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. If you’re not a native iOS developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import "SampleApp-Swift.h" Inside AppDelegate’s @implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to register a device to the current app user and handle push notifications. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RCTAppSetupPrepareApp(application, true); NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; #if RCT_NEW_ARCH_ENABLED _contextContainer = std::make_shared<facebook::react::ContextContainer const>(); _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>(); _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; #endif NSDictionary *initProps = [self prepareInitialProps]; UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); [application registerForRemoteNotifications]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [pnHandlerObj setupCustomerIOClickHandling]; [RNNotifications startMonitorNotifications]; return YES; } ... // Required to register device token. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Register device to receive push notifications with device token [pnHandlerObj application:application deviceToken:deviceToken]; } // Required for the registration error event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [pnHandlerObj application:application error:error]; } In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the code on the right and paste it into this new file (which we’ve called NotificationServicePushHandler.swift) file—replacing everything in the file and update Env.cdpApiKey with your CDP API key. import CioMessagingPushAPN import Foundation import UserNotifications @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Open your NotificationService.m file and copy the highlighted lines (beginning on line 2) into your file. The name of the header file on line 2 will depend on your extension’s name i.e. YourNotificationServiceExtensionName-Swift.h and is automatically created by Xcode. After this, you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import "NotificationService.h" #import "NotificationServiceExtension-Swift.h" @interface NotificationService () @end @implementation NotificationService // Create object of class NotificationServicePushHandler NotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[NotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. [nsHandlerObj serviceExtensionTimeWillExpire]; } @end APN/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios In your iOS subfolder, update your AppDelegate.swift file to use the Customer.io wrapper class that handles push notifications automatically. This approach replaces the need to manually implement push notification delegate methods. import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Optional: add if you need UNUserNotificationCenterDelegate methods UNUserNotificationCenter.current().delegate = self // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want more control over notifications when your app is in the foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushAPN import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import CioMessagingPushFCM import FirebaseMessaging import Foundation @objc public class MyAppPushNotificationsHandler: NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // Initialize MessagingPushFCM module to automatically handle your app’s push notifications that originate from Customer.io MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Register device on receiving a device token (FCM) MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Open AppDelegate.h and add the FIRMessagingDelegate import statement. If you’re not a native Objective-C developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> @interface AppDelegate : RCTAppDelegate <FIRMessagingDelegate> @end Open your ios/AppDelegate.mm file and import your header file, as we’ve shown on line 2 and also import FirebaseCore as we’ve shown on line 5. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. #import "AppDelegate.h" #import <FCMSampleApp-Swift.h> #import <React/RCTLinkingManager.h> #import <React/RCTBundleURLProvider.h> #import <FirebaseCore.h> In your AppDelegate.mm file, create an object of your push notification handler. We called ours MyAppPushNotificationsHandler. @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler *pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to configure Firebase and handle tokens. We’ve highlighted the code here to show what you need to add. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"FCMSampleApp"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; // Configure Firebase [FIRApp configure]; // Set FCM messaging delegate [FIRMessaging messaging].delegate = self; // Use modifiedLaunchOptions for passing link to React Native bridge to sends users to the specified screen NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } [pnHandlerObj setupCustomerIOClickHandling]; return [super application:application didFinishLaunchingWithOptions:modifiedLaunchOptions]; } ... @end In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy this code into the new file and replace Env.cdpApiKey with your CDP API key. import CioMessagingPushFCM import Foundation import UserNotifications @objc public class MyAppNotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file—e.g. NotificationServiceExtension-Swift.h. You’ll also need to create an object class of MyAppNotificationServicePushHandler and call the functions in the code sample here. Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import <NotificationServiceExtension-Swift.h> #import "NotificationService.h" @interface NotificationService () @end @implementation NotificationService // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize { nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest*)request withContentHandler:(void (^)(UNNotificationContent* _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end FCM/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. When complete, you should see Pod installation complete! pod install --project-directory=ios Update your AppDelegate.swift file to handle push notifications. You can copy the code sample on the right, but you may need to update imports and other things to fit your app. import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... // Configure Firebase FirebaseApp.configure() // Optional: Set FCM messaging delegate if you need it. The Customer.io SDK will automatically read FCM tokens Messaging.messaging().delegate = self // Initialize MessagingPushFCM module to automatically handle your app's push notifications that originate from Customer.io MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // This is required for FCM. The Customer.io SDK does not make changes to other SDKs Messaging.messaging().apnsToken = deviceToken } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want fine-grained control over presenting Notifications in foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } extension AppDelegate: MessagingDelegate { // Optional: add this method if you need access to `fcmToken` - Customer.io SDK will read this automatically func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushFCM import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Push icon (Android) You’ll set the icon that appears on normal push notifications as a part of your app manifest—android/app/src/main/AndroidManifest.xml. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user has not yet been asked to opt into notifications, the method will return NotDetermined (only for iOS). var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Optional: Remove POST_NOTIFICATIONS permission from Android apps By default, the SDK includes the POST_NOTIFICATIONS permission which is required by Android 13 to show notifications on Android device. However, if you do not want to include the permission because don’t use notifications, or for any other reason, you can remove the permission by adding the following line to your android/app/src/main/AndroidManifest.xml file: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/> Fetch the current device token You can fetch the currently stored device token using the CustomerIO.pushMessaging.getRegisteredDeviceToken() method. This method returns an APN/FCM token in a promise as a string. let token = await CustomerIO.pushMessaging.getRegisteredDeviceToken() if (token) { // Use the token as required in your app for example save in a state setDeviceToken(token); } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/react-native/4.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior The push.android.pushClickBehavior config option controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) The available options are: ActivityPreventRestart (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ActivityNoFlags: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. ResetTaskStack: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links After you set up push notifications you can enable deep links in rich push notifications. There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps.  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Open your AppDelegate.swift file and add the code below. Note that the Customer.io SDK automatically forwards deep links from Customer.io push notifications to the application(:open:options:) method. For push notifications from other providers, you still need to handle deep links manually in the userNotificationCenter(didReceive:withCompletionHandler:) method. import UIKit import CioMessagingPushAPN import React @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // Handle deep links return RCTLinkingManager.application(app, open: url, options: options) } } Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) Set up Universal Links Follow React Native’s documentation to implement Universal Links in your app. --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/react-native/4.x/push-notifications/multiple-push-providers/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If Customer.io is the only SDK that you use in your app to display push notifications, then you don’t need to do anything special to display push notifications. But, if you use another module in your app that can display push notifications like expo-notifications, react-native-push-notification, or rnfirebase, these modules can take over push handling by default and prevent your app from receiving push notifications from Customer.io. You can solve this problem using one (and only one) of the methods below, but we typically recommend the first option, because it doesn’t require you to write native code! Please note that the following methods will always return true for iOS. Option 1 (Recommended): Set Customer.io SDK to handle push clicks You can pass the payloads of other message services to Customer.io whenever a device receives a notification, and our SDK can process it for you. The SDK exposes the onMessageReceived method for this that takes two arguments: a message.data object containing the incoming notification payload a handleNotificationTrigger boolean indicating whether or not to trigger a notification. true (default) means that the Customer.io SDK will generate the notification and track associated metrics. false means that the SDK will process the notification to track metrics but will not generate a notification on the device. You’ll use the onMessageReceived like this: CustomerIO.pushMessaging.onMessageReceived(message).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); You can pass values in onMessageReceived by listening to notification events exposed by other SDKs. Make sure that you add listeners in the right places to process notifications that your app receives when it’s in the foreground and add background listeners that might be required by other SDK to process notifications that your app receives when it’s in background/killed state. If you always send rich push messages (with image and/or link), adding event listeners is enough. But if you send custom push payloads using the notification object or send simple push messages (with just a body and title), you may get duplicate notifications when your app is backgrounded because Firebase itself displays notifications sent using the notification object. To avoid this, You can pass false in handleNotificationTrigger to track metrics for simple and custom payload push notifications. To simplify this behavior, the SDK also exposes an onBackgroundMessageReceived method that automatically suppresses pushes with the notification object when your app is in background. If you use rnfirebase, you can setup listeners like this: Foreground Listener Foreground Listener To listen to messages in the foreground, set onMessage listener where appropriate: useEffect(() => { const unsubscribe = messaging().onMessage(async remoteMessage => { CustomerIO.pushMessaging.onMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); return unsubscribe; }, []); Background Listener Background Listener To listen to messages when app is in background/killed state, set setBackgroundMessageHandler in your index.js file messaging().setBackgroundMessageHandler(async remoteMessage => { CustomerIO.pushMessaging.onBackgroundMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); Option 2: Register Customer.io Messaging Service You can register Customer.io’s messaging service in your Manifest file so that we handle all notifications for your app. You can do this by adding the following code under the <application> tag in the AndroidManifest.xml file in your app’s android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  The Customer.io SDK will handle all your push notifications The code above hands all push notifications responsibility to our SDK, meaning: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/react-native/4.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. The SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. You don’t have to add any code to track opened push metrics or launch deep links.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. You can configure this behavior by adding the following configuration to the class that you created as a part of our push notification setup instructions in your AppDelegate.swift file. // In your AppDelegate.swift @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with foreground push display option MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().showPushAppInForeground(true).build() ) return true } } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually record push metrics using Javascript methods  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking After you set up push notifications, update your AppDelegate.swift file to disable automatic push notification tracking: // In your AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with auto-tracking disabled MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().autoTrackPushEvents(false).build() ) return true } --- ## Android channels URL: https://docs.customer.io/integrations/sdk/react-native/4.x/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v4.5.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io React-Native SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io React-Native SDK prior to 4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io React Native SDK v4.5.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/4.x/in-app-messages/set-up-in-app/ This page describes how to implement mobile in-app messages. How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set the inApp.siteId option in your CioConfig, and your app will be able to receive in-app messages. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key to generate them. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', } }; Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. Make sure your page rules match the casing of the title in your screen events. Anonymous messages As of version 4.11, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/4.x/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages You’ll need to include a UI element in your app UI to render inline messages. The view will automatically adjust its height when messages are loaded or interacted with.  We’ve set up examples in our sample apps that might help if you want to see a real-world implementation of this feature. Add the InlineInAppMessageView component to your React Native app: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { return ( <InlineInAppMessageView elementId="my-message" onActionClick={(message, actionValue, actionName) => { console.log('Action clicked:', { message, actionValue, actionName }); }} /> ); } View layout The InlineInAppMessageView automatically adjusts its height at runtime when messages load or users interact with them. You should avoid setting a fixed height on this component as it might interfere with message rendering. You’re responsible for setting layout styles to position your view correctly (width, margins, padding, and so on). The component will handle its own height dynamically. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. If the editor says that the inline display feature is Web/iOS only, don’t worry about that. We’re working on updating this UI. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow the steps below to implement custom actions for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events There are two ways to listen for these click events in inline in-app messages. Register a callback with your inline view: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { const handleActionClick = (message, actionValue, actionName) => { // Perform some logic when people tap an action button. // Example code handling button tap: switch (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionName); break; // You can add more cases here for other actions default: // Handle unknown actions or do nothing console.log("Unknown action:", actionValue); } }; return ( <InlineInAppMessageView elementId="my-message" onActionClick={handleActionClick} /> ); } Register a global SDK event listener. When you register an event listener with the SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic for inline and non-inline messages if you want. Handle responses to messages (event listeners) Like modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionClick callback set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/react-native/4.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging.registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## 4.x -> 4.3 URL: https://docs.customer.io/integrations/sdk/react-native/4.x/whats-new/4.3-upgrade/ Version 4.3 of the Customer.io React Native SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 4.3 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your app depending on whether you send push notifications with APN or FCM and whether you use UIKit or SwiftUI. Update with APN (Apple Push Notification service) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } After (4.3) After (4.3) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper - handles all push methods automatically MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - CioAppDelegateWrapper handles everything } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Update with FCM (Firebase Cloud Messaging) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token } } After (4.3) After (4.3) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Initialize push FCM with wrapper - handles all push methods automatically MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - wrapper handles everything } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token - Customer.io SDK will also receive this automatically } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Important Notes Manual push handling methods are not required: the CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError didReceiveRemoteNotification All other push-related delegate methods The @main attribute - Must be on the wrapper class, not your AppDelegate. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushAPN.initialize() or MessagingPushFCM.initialize() If you encounter some unexpected behavior and want to test is it related to new Push Notification tracking system, just comment the following line and compare class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} with the original AppDelegate. --- ## 3.4x -> 4.x URL: https://docs.customer.io/integrations/sdk/react-native/4.x/whats-new/4.x-upgrade/ This page provides steps to help you upgrade from react native 3.4 or later so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Update your initialization You’ll initialize the new version of the SDK and its packages with CioConfig objects instead of CustomerioConfig. While we’ve listed all the new configuration options, you’ll want to pay close attention to the following changes: CustomerIOEnv is no longer necessary. Region becomes CioRegion. siteId becomes migrationSiteId. You’ll initialize the SDK with initialize(config) instead of initialize(env, config). If you previously used the backgroundQueueMinNumberOfTasks or backgroundQueueSecondsDelay options, you should remove them from your configuration as well. These options are no longer supported, and may cause build errors if you use strict type checking. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) 3. Update your AppDelegate push notification handler In your MyAppPushNotificationsHandler.swift (or the associated file where you add a push notification handler in your main target), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import CioMessagingPushAPN // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } FCM FCM import Foundation import CioMessagingPushFCM import FirebaseMessaging // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: Region.US) { config in } // update this line to MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) } // Register device on receiving a device token (FCM) @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } 4. Update your NotificationService push notification handler In your NotificationServicePushHandler.swift (or the associated file where you add a push notification handler in NotificationServiceExtension), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import UserNotifications import CioMessagingPushAPN // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM FCM import Foundation import UserNotifications import CioMessagingPushFCM // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } 5. Update your identify call Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track and screen calls are structured the same as previous versions, but the data object is now called properties. We’ve highlighted changes in the sample below. //identify: identifier becomes userId, body becomes traits CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); //track: no significant change to method //in Customer.io data object renamed properties CustomerIO.track("track_event_name", { propertyName: propertyValue }); //screen: no significant change to method. //name becomes title, data object renamed properties CustomerIO.screen("screen_event_name", { propertyName: propertyValue }); Configuration Changes As a part of this release, we’ve changed a few configuration options when you initialize the SDK. You’ll use CioConfig to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data into Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). inApp object Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. push object Replaces the former enablePush option, providing a place to set push configuration options. For now, it only takes the android.pushClickBehavior setting. backgroundQueueMinNumberOfTasks removed This option is no longer available. backgroundQueueSecondsDelay removed This option is no longer available. --- ## 3.x -> 3.4 URL: https://docs.customer.io/integrations/sdk/react-native/4.x/whats-new/update-to-3.4/ This page explains how to update your SDK install to latest versions that may not require a breaking change. While these changes aren't breaking—you don't _need_ to make these changes—they will simplify your integration, improve the reliability of your metrics, and improve deep link handling on iOS devices. Upgrade from 3.3 to 3.4+ As of version 3.4, the Customer.io SDK automatically registers push device tokens to identified people and handles push clicks. These features simplify your SDK integration while improving compatibility with apps that use multiple push SDKs. After you install a version of the SDK that is 3.4 or higher, follow these steps to upgrade.  Do you have a swift app? Skip ahead! If you’ve got a Swift app containing the AppDelegate.swift file, ignore the steps below and go to the Swift upgrade section. Open your push notification handler file (In our examples, we call this file MyAppPushNotificationsHandler.swift) and review all of the highlighted code below. We’ve highlighted the most relevant lines. import Foundation import CioMessagingPushAPN import UserNotifications // Delete this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} // Replace these 2 lines @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // With these 2 lines @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } // Delete these 2 lines: let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } // Delete this function: @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } } Open your AppDelegate.h file and review all of the highlighted code below. APN APN #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate @end FCM FCM #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate, UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate> @end Open your AppDelegate.m file and review all of the highlighted code below. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... // Replace this line [pnHandlerObj setupCustomerIOClickHandling:self]; // With this line: [pnHandlerObj setupCustomerIOClickHandling]; return YES; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { // Remove the line below: [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } Now that your app’s code has been simplified, follow the latest push notification setup documentation to enable these new features. Upgrade from 3.3 to 3.4+, for Swift Open your AppDelegate.swift file and review all of the highlighted code below. We’ve highlighted the most relevant lines. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Delete this line UIApplication.shared.registerForRemoteNotifications() return true } } // Delete this function func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } // Delete this function func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/react-native/4.x/whats-new/update-to-3x/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Installing and updating our React Native SDK got easier. After you install the CustomerIO React Native SDK version 3.x, open your ios/Podfile and follow all 5 steps shown in this code block below: # 1. This line is required by the FCM SDK. If you encounter problems during 'pod install', add this line to your Podfile and try 'pod install' again. use_frameworks! :linkage => :static target 'YourApp' do # Note: 'YourApp' is unique to your app. This is here for example purposes, only. # 2. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me # 3. Add one of these new lines below: # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end target 'NotificationServiceExtension' do # 4. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me pod 'FirebaseMessaging' # Remove me, unless you need to specify a specific version pod 'Firebase' # Remove me, unless you need to specify a specific version. # 5. Add one of these new lines below: # ⚠️ Important: Notice these lines of code include "-richpush" in it making it unique to the host app target above. # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end After you modify your Podfile, run the command pod update --repo-update --project-directory=ios to make your changes to ios/Podfile go into effect. Upgrade from 1.x to 2.x Rich push initialization(iOS) If you followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Due to the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your Notification Service Extension. In the case that you use Objective-C, you must add the code snippet below into the Swift handler file that you created in NotificationService Extension. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Firebase users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. If you send messages to your iOS app using FCM, you’ll need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. pod 'Firebase' pod 'FirebaseMessaging' We fixed a bug in our iOS modules that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in our iOS v1 modules that converted keys in your custom data to snake_case. This bug is fixed in v2 of the SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect your Android users.. // If you passed in custom attributes using camelCase keys: data = {"firstName": "Dana"} // The SDK v1 may have converted this data into: data = {"first_name": "Dana"} // Or, if you used a different format that was not snake_case: data = {"FIRSTNAME": "Dana"} // The SDK v1 may have converted this data into: data = {"f_irstname": "Dana"} You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Call the Customer.io SDK and provide custom attributes like this: CustomerIO.identify("dana@example.com", {"first_name": "Dana"}) // Consider sending duplicate data with snake_case CustomerIO.identify("dana@example.com", { "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. }) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.identify("dana@example.com", { "firstName": "Dana" // We can remove the snake_case attribute and go back to just camelCase! }) --- ## Changelog URL: https://docs.customer.io/integrations/sdk/react-native/4.x/whats-new/changelog/ Check out release history our React Native SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. content --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/react-native/5.x/quick-start-guide/ React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview React Native lets you build native mobile apps with JavaScript. Our React Native SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages. Install the SDK. Identify and Track Push Notifications In-App 1. Install the SDK You need to add a new React Native connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration. in Customer.io to get your CDP API key. See Get your CDP API key for details. Make sure you set up your React Native environment first. You must use React Native 0.79 or later. Open your terminal and go to your project folder. Install the customerio-reactnative package using NPM or Yarn: npm install customerio-reactnative # or yarn add customerio-reactnative Set up your project to support iOS and/or Android deployments: iOS iOS For iOS, install CocoaPods dependencies: pod install --repo-update --project-directory=ios Make sure your minimum iOS deployment target is set to 13.0 in both your Podfile and Xcode project settings. Android Android For Android, include the Google Services plugin by adding the following to your project-level android/build.gradle file: buildscript { repositories { google() // Google's Maven repository } dependencies { classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { google() // Google's Maven repository } } Add the plugin to your app-level android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and place it in android/app/google-services.json. Add your CDP API key and site ID to your configuration. CDP API Key: You’ll find this key in your React Native connection. Site ID: You’ll find this value in your workspace under Settings > Workspace Settings > API and webhook credentials. Initialize the SDK in your app. Add the following code to your main component or App.js file: import React, { useEffect } from 'react'; import { CioConfig, CioRegion, CioLogLevel, CustomerIO, PushClickBehaviorAndroid } from 'customerio-reactnative'; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', // Required region: CioRegion.US, // Replace with CioRegion.EU if your Customer.ioaccount is in the EU logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'YOUR_SITE_ID', // Required for in-app messaging } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []); Run your application to ensure everything is set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. import { CustomerIO } from "customerio-reactnative"; const identifyUserExample = async () => { await CustomerIO.identify({ userId: 'react-native-test-user@example.com', traits: { firstName: 'John', lastName: 'Doe', email: 'react-native-test-user@example.com', subscriptionStatus: 'active', }, }); console.log('User identified successfully'); }; Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. import { CustomerIO } from "customerio-reactnative"; const trackCustomEventExample = async () => { await CustomerIO.track('purchased_item', { product: 'Premium Subscription', price: 99.99, currency: 'USD' }); console.log('Custom event tracked successfully'); }; Track screen views to trigger in-app messages associated with specific screens. import { CustomerIO } from "customerio-reactnative"; const trackScreenViewExample = async () => { await CustomerIO.screen('ProductDetails'); console.log('Screen view tracked successfully'); }; 3. Push Notifications Set up your push notification credentials in Customer.io: iOS: Upload your Apple Push Notification certificate (.p8 file). Android: Upload your Firebase Cloud Messaging server key (.json format). Request push notification permissions from the user: import { CustomerIO, CioPushPermissionStatus } from "customerio-reactnative"; const requestPushPermissions = async () => { const permissionStatus = await CustomerIO.pushMessaging.showPromptForPushNotifications({ ios: { sound: true, badge: true } }); switch (permissionStatus) { case CioPushPermissionStatus.Granted: console.log('Push notifications enabled'); break; case CioPushPermissionStatus.Denied: case CioPushPermissionStatus.NotDetermined: console.log('Push notifications denied'); break; } }; For iOS: to ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes. For Android: add notification icon resources: Place a notification icon file named ic_notification.png in your drawable folders. Make sure your app’s AndroidManifest.xml has the proper FCM permissions. 4. In-App To enable in-app messaging, all you need to do is add the site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app. You can call the initialize method from your components or services: import { CioConfig, CustomerIO } from "customerio-reactnative"; import { useEffect } from "react"; useEffect(() => { const initializeCustomerIO = async () => { const config: CioConfig = { cdpApiKey: 'YOUR_CDP_API_KEY', inApp: { siteId: 'YOUR_SITE_ID', } }; await CustomerIO.initialize(config); }; initializeCustomerIO(); }, []);  Check out our code samples! You’ll find a complete, working sample app in our React Native SDK’s example directory. If you get stuck, you can refer to the sample app to see how everything fits together. --- ## How it works URL: https://docs.customer.io/integrations/sdk/react-native/5.x/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum requirements To support the Customer.io SDK, you must: Use React Native versions 0.79 and later. Current versions of XCode don’t support earlier versions of React Native. Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Add React Navigation to your app to support deep links and screen tracking. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/react-native/5.x/getting-started/auth/ To use the SDK, you'll need two kinds of API keys: A *CDP API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. These keys come from different places in Customer.io! CDP API Key: You’ll get this key when you set up your mobile app as a data-in integration in Customer.io. Site ID: This key tells the SDK which workspace your in-app messages come from. You’ll use it to support inApp messages. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your CDP API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your React Native entry under Integrations. If you don’t see your app on this page, you’ll need to add up a new integration. Go to Integrations. Go to the Connections tab and find your React Native connection. If you don’t have a React Native connection, you’ll need to set one up. Go to Settings and find your API Key. Copy this key into the CioConfig.CdpApiKey config option. import { CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Add a new integration If you don’t already have a write key, you’ll need to set up a new connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration.. The connection represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your site ID with the inApp option to support in-app messaging. And if you’re upgrading from a previous version of the SDK, you’ll also use your site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to Settings > Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the inApp package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/react-native/5.x/getting-started/packages-options/ The SDK consists of a few packages. You *must* use the `CioConfig` and `CustomerIO` packages to configure and initialize the React Native SDK. SDK packages The SDK consists of a few packages. You must use the CioConfig and CustomerIO packages to configure and initialize the SDK. Package Product Required? Description CustomerIO ✅ The main SDK package. Used to initialize the SDK and call the SDK’s methods. CioConfig ✅ Configure the SDK including in-app messaging support. CioRegion Used inside the CioConfig.region option to declare your region—EU or US. CioLogLevel Used inside the CioConfig.logLevel option to set the level of logs you can view from the SDK. Configuration options You can determine global behaviors for the SDK in using CioConfig package. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. Note that the logLevel option requires the CioLogLevel package and the region option requires the CioRegion package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, // Replace with CioRegion.EU if your Customer.io account is in the EU. logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io migrationSiteId string Required if you're updating from 3.x: the credential for previous versions of the SDK. This key lets the SDK send remaining tasks to Customer.io when your audience updates your app. region CioRegion.EU or CioRegion.US CioRegion.US Requires the CioRegion package. You must set the region your account is in the EU (CioRegion.EU). autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc screenViewUse All or InApp All ScreenView.All (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events logLevel string error Requires the CioLogLevel package. Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. inApp object Required for in-app support. This object takes a siteId property, determining the workspace your in-app messages come from. push object Takes a single option called PushClickBehaviorAndroid. This object and option controls how your app behaves when your Android audience taps push notifications. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/react-native/5.x/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Capture logs Logs help us pinpoint the problem and find a solution. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import { CustomerIO, CioConfig, CioLogLevel } from 'customerio-reactnative'; const config: CioConfig = { CioApiKey: 'Your CDP API Key', logLevel: CioLogLevel.Debug, } CustomerIO.initialize(config) ; Build and run your app on a physical device or emulator. In the console, run: react-native log-ios react-native log-android Export your log to a text file and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Try updating iOS package dependencies This SDK uses our iOS push package. In some cases, we may make fixes in our iOS packages that fix downstream issues in, or expose new features to this SDK. You can update the version in your podfile and then run the following command to get the latest iOS packages. Our instructions above list out the full version of the iOS push package. If you want to automatically increment packages, you can remove the patch and minor build numbers (the second and third parts of the version number), and pod update will automatically fetch the latest package versions. However, please understand that fetching the latest versions can cause build issues if the latest iOS package doesn’t agree with code in your app! pod update --repo-update --project-directory=ios Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Error: Push notifications not working If push notifications don’t work, make sure that you’ve initialized the Customer.io SDK in your AppDelegate.swift file. You must initialize the SDK in the application(_:didFinishLaunchingWithOptions:) method. @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } } Deep linking to iOS when your app is killed There’s a known issue preventing deep links from working when your app is closed on iOS devices. When the app is in a closed state, the native click event fires before the app’s lifecycle begins. We recommend a workaround: Update didFinishLaunchingWithOptions in your AppDelegate.swift file with the code below. We extract the deep link from the push notification payload and add it to the launch options, ensuring that your React Native app receives the link when it starts. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) ... if var launchOptions = launchOptions, let remotePush = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [String: [String: [String: String]]], let link = remotePush["CIO"]?["push"]?["link"], let url = URL(string:link), launchOptions[UIApplication.LaunchOptionsKey.url] == nil { launchOptions[UIApplication.LaunchOptionsKey.url] = url } let appName = Bundle.main.displayName factory.startReactNative( withModuleName: appName, in: window, initialProperties: ["appName": appName], launchOptions: launchOptions ) ... } Compiler error: ‘X’ is unavailable in application extensions for iOS This error occasionally occurs when users add a notification extension to handle rich push messages. If you see this error, try the following steps: Add this code to the end of your Podfile: post_install do |installer| installer.pods_project.targets.each do |target| if target.name.start_with?('CustomerIO') puts "Modifying target #{target.name} with workaround" target.build_configurations.each do |config| puts "Setting build config settings for #{target.name}" config.build_settings['APPLICATION_EXTENSION_API_ONLY'] ||= 'NO' end end end end In the root directory of your app, run pod install --project-directory=ios. This command will apply the above workaround to your project. Try to compile your app again. If you still see the error message, it’s likely that the error you see is related to a different SDK that you use in your app and not the Customer.io SDK. We suggest that you contact the developers of the SDK that you see in the error message for help. If you don’t see an error message, send our technical support team a message with: The error message that you see when compiling your app. The contents of your ios/Podfile and ios/Podfile.lock files. The version of the React Native SDK that you are using. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/react-native/5.x/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood: "Pizza", favouriteDrink: "Mango Shake" }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify( userId: "new.person@example.com", traits: { first_name: "New", last_name: "Person" })  --- ## Track events URL: https://docs.customer.io/integrations/sdk/react-native/5.x/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import { CustomerIO } from "customerio-reactnative"; CustomerIO.track("event_name", { propertyName: propertyValue });  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.track("User Deleted) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/react-native/5.x/tracking/screen-events/ Screen events track the screens people view in your app. Beyond tracking the parts of your app people use, screen tracking is vital for in-app messages because they target specific screens. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using React Navigation for automatic screen tracking. This example requires @react-navigation/native and @react-navigation/native-stack to create a navigation container in App.js If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useRef } from 'react'; const Stack = createNativeStackNavigator(); export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} > <Stack.Navigator initialRouteName="FirstScreen"> <Stack.Screen name="FirstScreen" component={FirstScreen}/> <Stack.Screen name="SecondScreen" component={SecondScreen} options={{ title : "My App", headerStyle: { backgroundColor: '#F6F7F9', }, }}/> </Stack.Navigator> </NavigationContainer> ); }; Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.InApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory region: CioRegion.US, screenViewUse: ScreenView.All trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/react-native/5.x/tracking/lifecycle-events/ By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. CustomerIO.track("Application Crashed", { url: "/page/in/app" }); Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in the CioConfig object when you initialize the SDK. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, trackApplicationLifecycleEvents: false, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration } }; CustomerIO.initialize(config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/react-native/5.x/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their screen view. This triggers the campaign you set up for people who visited the product page. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/react-native/5.x/push-notifications/push/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How it works Under the hood, our React Native SDK takes advantage of our native Android and iOS SDKs. This helps us keep the React Native SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Add push notification capabilities in XCode. (iOS) Integrate push notifications: code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. Request, or check for, push notification permissions. If your app’s user doesn’t grant permission, notifications will not appear in the system tray. While push providers support a number of features in their payloads, our React Native package only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities, but there are still a few more steps to finish setting things up. Go to File > New > Target. Select Notification Service Extension and click Next. Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) and set the Language to Swift or Objective C based on the language you use for native iOS files. Click Finish. When presented with the dialog below, click Cancel. This helps Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Pick your push provider (APN or FCM) and the language your native files are written in to get started (Objective C or Swift). APN/Objective-CAPN/SwiftFCM/Objective-CFCM/Swift APN/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import Foundation import CioMessagingPushAPN @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // Initialize MessagingPushAPN module to automatically handle push notifications that originate from Customer.io MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Register device to receive push notifications with device token MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } Open your ios/AppDelegate.mm file and import your header file. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. If you’re not a native iOS developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import "SampleApp-Swift.h" Inside AppDelegate’s @implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to register a device to the current app user and handle push notifications. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RCTAppSetupPrepareApp(application, true); NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; #if RCT_NEW_ARCH_ENABLED _contextContainer = std::make_shared<facebook::react::ContextContainer const>(); _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>(); _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; #endif NSDictionary *initProps = [self prepareInitialProps]; UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); [application registerForRemoteNotifications]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [pnHandlerObj setupCustomerIOClickHandling]; [RNNotifications startMonitorNotifications]; return YES; } ... // Required to register device token. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Register device to receive push notifications with device token [pnHandlerObj application:application deviceToken:deviceToken]; } // Required for the registration error event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [pnHandlerObj application:application error:error]; } In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the code on the right and paste it into this new file (which we’ve called NotificationServicePushHandler.swift) file—replacing everything in the file and update Env.cdpApiKey with your CDP API key. import CioMessagingPushAPN import Foundation import UserNotifications @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Open your NotificationService.m file and copy the highlighted lines (beginning on line 2) into your file. The name of the header file on line 2 will depend on your extension’s name i.e. YourNotificationServiceExtensionName-Swift.h and is automatically created by Xcode. After this, you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import "NotificationService.h" #import "NotificationServiceExtension-Swift.h" @interface NotificationService () @end @implementation NotificationService // Create object of class NotificationServicePushHandler NotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[NotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. [nsHandlerObj serviceExtensionTimeWillExpire]; } @end APN/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. target 'SampleApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios In your iOS subfolder, update your AppDelegate.swift file to use the Customer.io wrapper class that handles push notifications automatically. This approach replaces the need to manually implement push notification delegate methods. import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... // Optional: add if you need UNUserNotificationCenterDelegate methods UNUserNotificationCenter.current().delegate = self // Initialize the Customer.io SDK for push notifications MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want more control over notifications when your app is in the foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushAPN import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM/Objective-C Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import CioMessagingPushFCM import CioFirebaseWrapper import FirebaseMessaging import Foundation @objc public class MyAppPushNotificationsHandler: NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // Initialize MessagingPushFCM module to automatically handle your app’s push notifications that originate from Customer.io MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Register device on receiving a device token (FCM) MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Open AppDelegate.h and add the FIRMessagingDelegate import statement. If you’re not a native Objective-C developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> @interface AppDelegate : RCTAppDelegate <FIRMessagingDelegate> @end Open your ios/AppDelegate.mm file and import your header file, as we’ve shown on line 2 and also import FirebaseCore as we’ve shown on line 5. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. #import "AppDelegate.h" #import <FCMSampleApp-Swift.h> #import <React/RCTLinkingManager.h> #import <React/RCTBundleURLProvider.h> #import <FirebaseCore.h> In your AppDelegate.mm file, create an object of your push notification handler. We called ours MyAppPushNotificationsHandler. @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler *pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to configure Firebase and handle tokens. We’ve highlighted the code here to show what you need to add. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"FCMSampleApp"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; // Configure Firebase [FIRApp configure]; // Set FCM messaging delegate [FIRMessaging messaging].delegate = self; // Use modifiedLaunchOptions for passing link to React Native bridge to sends users to the specified screen NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } [pnHandlerObj setupCustomerIOClickHandling]; return [super application:application didFinishLaunchingWithOptions:modifiedLaunchOptions]; } ... @end In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy this code into the new file and replace Env.cdpApiKey with your CDP API key. import CioMessagingPushFCM import CioFirebaseWrapper import Foundation import UserNotifications @objc public class MyAppNotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file—e.g. NotificationServiceExtension-Swift.h. You’ll also need to create an object class of MyAppNotificationServicePushHandler and call the functions in the code sample here. Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import <NotificationServiceExtension-Swift.h> #import "NotificationService.h" @interface NotificationService () @end @implementation NotificationService // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize { nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest*)request withContentHandler:(void (^)(UNNotificationContent* _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end FCM/Swift Open your ios/Podfile and add the Customer.io push dependency, highlighted here, to both your main target and NotificationServiceExtension target. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. When complete, you should see Pod installation complete! pod install --project-directory=ios Update your AppDelegate.swift file to handle push notifications. You can copy the code sample on the right, but you may need to update imports and other things to fit your app. import UIKit import CioMessagingPushFCM import CioFirebaseWrapper import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... // Configure Firebase FirebaseApp.configure() // Optional: Set FCM messaging delegate if you need it. The Customer.io SDK will automatically read FCM tokens Messaging.messaging().delegate = self // Initialize MessagingPushFCM module to automatically handle your app's push notifications that originate from Customer.io MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // This is required for FCM. The Customer.io SDK does not make changes to other SDKs Messaging.messaging().apnsToken = deviceToken } } extension AppDelegate: UNUserNotificationCenterDelegate { // Optional: add this method if you want fine-grained control over presenting Notifications in foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .list, .badge, .sound]) } } extension AppDelegate: MessagingDelegate { // Optional: add this method if you need access to `fcmToken` - Customer.io SDK will read this automatically func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import CioMessagingPushFCM import Foundation import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: Env.cdpApiKey) // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US //.region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { // Called just before the extension is terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. MessagingPush.shared.serviceExtensionTimeWillExpire() } } Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Push icon (Android) You’ll set the icon that appears on normal push notifications as a part of your app manifest—android/app/src/main/AndroidManifest.xml. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user has not yet been asked to opt into notifications, the method will return NotDetermined (only for iOS). var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Optional: Remove POST_NOTIFICATIONS permission from Android apps By default, the SDK includes the POST_NOTIFICATIONS permission which is required by Android 13 to show notifications on Android device. However, if you do not want to include the permission because don’t use notifications, or for any other reason, you can remove the permission by adding the following line to your android/app/src/main/AndroidManifest.xml file: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/> Fetch the current device token You can fetch the currently stored device token using the CustomerIO.pushMessaging.getRegisteredDeviceToken() method. This method returns an APN/FCM token in a promise as a string. let token = await CustomerIO.pushMessaging.getRegisteredDeviceToken() if (token) { // Use the token as required in your app for example save in a state setDeviceToken(token); } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/react-native/5.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior The push.android.pushClickBehavior config option controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) The available options are: ActivityPreventRestart (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ActivityNoFlags: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. ResetTaskStack: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links After you set up push notifications you can enable deep links in rich push notifications. There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps.  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Open your AppDelegate.swift file and add the code below. Note that the Customer.io SDK automatically forwards deep links from Customer.io push notifications to the application(:open:options:) method. For push notifications from other providers, you still need to handle deep links manually in the userNotificationCenter(didReceive:withCompletionHandler:) method. import UIKit import CioMessagingPushAPN import React @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // Handle deep links return RCTLinkingManager.application(app, open: url, options: options) } } Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) Set up Universal Links Follow React Native’s documentation to implement Universal Links in your app. --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/react-native/5.x/push-notifications/multiple-push-providers/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If Customer.io is the only SDK that you use in your app to display push notifications, then you don’t need to do anything special to display push notifications. But, if you use another module in your app that can display push notifications like expo-notifications, react-native-push-notification, or rnfirebase, these modules can take over push handling by default and prevent your app from receiving push notifications from Customer.io. You can solve this problem using one (and only one) of the methods below, but we typically recommend the first option, because it doesn’t require you to write native code! Please note that the following methods will always return true for iOS. Option 1 (Recommended): Set Customer.io SDK to handle push clicks You can pass the payloads of other message services to Customer.io whenever a device receives a notification, and our SDK can process it for you. The SDK exposes the onMessageReceived method for this that takes two arguments: a message.data object containing the incoming notification payload a handleNotificationTrigger boolean indicating whether or not to trigger a notification. true (default) means that the Customer.io SDK will generate the notification and track associated metrics. false means that the SDK will process the notification to track metrics but will not generate a notification on the device. You’ll use the onMessageReceived like this: CustomerIO.pushMessaging.onMessageReceived(message).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); You can pass values in onMessageReceived by listening to notification events exposed by other SDKs. Make sure that you add listeners in the right places to process notifications that your app receives when it’s in the foreground and add background listeners that might be required by other SDK to process notifications that your app receives when it’s in background/killed state. If you always send rich push messages (with image and/or link), adding event listeners is enough. But if you send custom push payloads using the notification object or send simple push messages (with just a body and title), you may get duplicate notifications when your app is backgrounded because Firebase itself displays notifications sent using the notification object. To avoid this, You can pass false in handleNotificationTrigger to track metrics for simple and custom payload push notifications. To simplify this behavior, the SDK also exposes an onBackgroundMessageReceived method that automatically suppresses pushes with the notification object when your app is in background. If you use rnfirebase, you can setup listeners like this: Foreground Listener Foreground Listener To listen to messages in the foreground, set onMessage listener where appropriate: useEffect(() => { const unsubscribe = messaging().onMessage(async remoteMessage => { CustomerIO.pushMessaging.onMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); return unsubscribe; }, []); Background Listener Background Listener To listen to messages when app is in background/killed state, set setBackgroundMessageHandler in your index.js file messaging().setBackgroundMessageHandler(async remoteMessage => { CustomerIO.pushMessaging.onBackgroundMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); Option 2: Register Customer.io Messaging Service You can register Customer.io’s messaging service in your Manifest file so that we handle all notifications for your app. You can do this by adding the following code under the <application> tag in the AndroidManifest.xml file in your app’s android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  The Customer.io SDK will handle all your push notifications The code above hands all push notifications responsibility to our SDK, meaning: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. Manually track push metrics If you need to manually track push metrics when you use multiple push providers (like when you display notifications yourself or use another library), you can parse a push notification payload and send opened or delivered events to the SDK in relevant callbacks: CustomerIO.trackMetric({ deliveryID: deliveryID, deviceToken: deviceToken, event: MetricEvent.Opened, }); The trackMetric method requires the following parameters: deliveryID: The delivery ID extracted from the push notification payload deviceToken: The device token extracted from the push notification payload event: The metric event type, either MetricEvent.Opened or MetricEvent.Delivered --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/react-native/5.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. The SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. You don’t have to add any code to track opened push metrics or launch deep links.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. You can configure this behavior by adding the following configuration to the class that you created as a part of our push notification setup instructions in your AppDelegate.swift file. // In your AppDelegate.swift @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with foreground push display option MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().showPushAppInForeground(true).build() ) return true } } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually record push metrics using Javascript methods  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking After you set up push notifications, update your AppDelegate.swift file to disable automatic push notification tracking: // In your AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with auto-tracking disabled MessagingPushAPN.initialize( withConfig: MessagingPushConfigBuilder().autoTrackPushEvents(false).build() ) return true } --- ## Android channels URL: https://docs.customer.io/integrations/sdk/react-native/5.x/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v4.5.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io React-Native SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io React-Native SDK prior to 4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io React Native SDK v4.5.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v4.5.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/5.x/in-app-messages/set-up-in-app/ This page describes how to implement mobile in-app messages. How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set the inApp.siteId option in your CioConfig, and your app will be able to receive in-app messages. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key to generate them. const config: CioConfig = { cdpApiKey: 'cdp_api_key', region: CioRegion.US, inApp: { siteId: 'site_id', } }; Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. Make sure your page rules match the casing of the title in your screen events. Anonymous messages As of version 4.11, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/5.x/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages You’ll need to include a UI element in your app UI to render inline messages. The view will automatically adjust its height when messages are loaded or interacted with.  We’ve set up examples in our sample apps that might help if you want to see a real-world implementation of this feature. Add the InlineInAppMessageView component to your React Native app: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { return ( <InlineInAppMessageView elementId="my-message" onActionClick={(message, actionValue, actionName) => { console.log('Action clicked:', { message, actionValue, actionName }); }} /> ); } View layout The InlineInAppMessageView automatically adjusts its height at runtime when messages load or users interact with them. You should avoid setting a fixed height on this component as it might interfere with message rendering. You’re responsible for setting layout styles to position your view correctly (width, margins, padding, and so on). The component will handle its own height dynamically. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. If the editor says that the inline display feature is Web/iOS only, don’t worry about that. We’re working on updating this UI. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow the steps below to implement custom actions for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events There are two ways to listen for these click events in inline in-app messages. Register a callback with your inline view: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { const handleActionClick = (message, actionValue, actionName) => { // Perform some logic when people tap an action button. // Example code handling button tap: switch (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionName); break; // You can add more cases here for other actions default: // Handle unknown actions or do nothing console.log("Unknown action:", actionValue); } }; return ( <InlineInAppMessageView elementId="my-message" onActionClick={handleActionClick} /> ); } Register a global SDK event listener. When you register an event listener with the SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic for inline and non-inline messages if you want. Handle responses to messages (event listeners) Like modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionClick callback set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/react-native/5.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging.registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## 4.x -> 5.0.0 URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/5.x-upgrade/ Version 5.x of the Customer.io React Native SDK introduces Firebase wrapper support for FCM users that improves Firebase compatibility and simplifies push notification setup. What changed? Version 5.x introduces a Firebase wrapper that improves compatibility with Firebase Cloud Messaging (FCM) and other Firebase services in your app. Do you need to update to this version? You need to update to this version if you use FCM (Firebase Cloud Messaging) for push notifications Update process Add the CioFirebaseWrapper import to your Swift files that use CioMessagingPushFCM. Add the Firebase wrapper import to your AppDelegate.swift file: import UIKit import CioMessagingPushFCM import CioFirebaseWrapper // Add this import for FCM users import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} Troubleshooting If you see build errors related to Firebase after upgrading: Clean your build: Run cd ios && pod install && cd .. then rebuild Check imports: Ensure you’ve added import CioFirebaseWrapper to all files that import CioMessagingPushFCM --- ## 4.x -> 4.3 URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/4.3-upgrade/ Version 4.3 of the Customer.io React Native SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 4.3 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your app depending on whether you send push notifications with APN or FCM and whether you use UIKit or SwiftUI. Update with APN (Apple Push Notification service) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } After (4.3) After (4.3) import UIKit import CioMessagingPushAPN import UserNotifications @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper - handles all push methods automatically MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - CioAppDelegateWrapper handles everything } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushAPN import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Update with FCM (Firebase Cloud Messaging) UIKit Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Register for push notifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // Manual push handling methods func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { MessagingPush.shared.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token } } After (4.3) After (4.3) import UIKit import CioMessagingPushFCM import UserNotifications import Firebase import FirebaseMessaging @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // Initialize push FCM with wrapper - handles all push methods automatically MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) // Register for push notifications // You can move this line to any part of your app. It's not critical to call it in this method. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { // Remove this, as Customer.io SDK handles this automatically DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } return true } // No manual push methods needed - wrapper handles everything } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { // Handle FCM token - Customer.io SDK will also receive this automatically } } SwiftUI If you’re using SwiftUI, you’ll need to use the @UIApplicationDelegateAdaptor instead of the @main attribute. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (4.x) Before (4.x) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { // Similar manual push handling as UIKit example above } After (4.3) After (4.3) import SwiftUI import CioMessagingPushFCM import UserNotifications @main struct MyApp: App { @UIApplicationDelegateAdaptor(CioAppDelegateWrapper<AppDelegate>.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize push with wrapper MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) return true } // No manual push methods needed } Important Notes Manual push handling methods are not required: the CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError didReceiveRemoteNotification All other push-related delegate methods The @main attribute - Must be on the wrapper class, not your AppDelegate. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushAPN.initialize() or MessagingPushFCM.initialize() If you encounter some unexpected behavior and want to test is it related to new Push Notification tracking system, just comment the following line and compare class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} with the original AppDelegate. --- ## 3.4x -> 4.x URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/4.x-upgrade/ This page provides steps to help you upgrade from react native 3.4 or later so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select React Native. Enter a Name for your integration, like “My React Native App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Click Complete Setup to finish setting up your integration. Remember, you can also connect your React Native app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Update your initialization You’ll initialize the new version of the SDK and its packages with CioConfig objects instead of CustomerioConfig. While we’ve listed all the new configuration options, you’ll want to pay close attention to the following changes: CustomerIOEnv is no longer necessary. Region becomes CioRegion. siteId becomes migrationSiteId. You’ll initialize the SDK with initialize(config) instead of initialize(env, config). If you previously used the backgroundQueueMinNumberOfTasks or backgroundQueueSecondsDelay options, you should remove them from your configuration as well. These options are no longer supported, and may cause build errors if you use strict type checking. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) 3. Update your AppDelegate push notification handler In your MyAppPushNotificationsHandler.swift (or the associated file where you add a push notification handler in your main target), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import CioMessagingPushAPN // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initialize(withConfig: MessagingPushConfigBuilder().build()) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } FCM FCM import Foundation import CioMessagingPushFCM import FirebaseMessaging // remove this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // remove this line CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: Region.US) { config in } // update this line to MessagingPushFCM.initialize(withConfig: MessagingPushConfigBuilder().build()) } // Register device on receiving a device token (FCM) @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } 4. Update your NotificationService push notification handler In your NotificationServicePushHandler.swift (or the associated file where you add a push notification handler in NotificationServiceExtension), you can remove the CioTracking module and the initialize method. If you write native code in Objective-C, you’ll also need to update your MessagingPushAPN or MessagingPushFCM initialization. We’ve highlighted the lines you’ll need to remove or modify in the code sample below. APN APN import Foundation import UserNotifications import CioMessagingPushAPN // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushAPN.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM FCM import Foundation import UserNotifications import CioMessagingPushFCM // remove this line import CioTracking @objc public class NotificationServicePushHandler: NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // remove this line CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } // update this line to MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "cdpApiKey") // Optional: specify region where your Customer.io account is located (.US or .EU). Default: US // .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } 5. Update your identify call Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track and screen calls are structured the same as previous versions, but the data object is now called properties. We’ve highlighted changes in the sample below. //identify: identifier becomes userId, body becomes traits CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); //track: no significant change to method //in Customer.io data object renamed properties CustomerIO.track("track_event_name", { propertyName: propertyValue }); //screen: no significant change to method. //name becomes title, data object renamed properties CustomerIO.screen("screen_event_name", { propertyName: propertyValue }); Configuration Changes As a part of this release, we’ve changed a few configuration options when you initialize the SDK. You’ll use CioConfig to set your configuration options. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data into Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). inApp object Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. push object Replaces the former enablePush option, providing a place to set push configuration options. For now, it only takes the android.pushClickBehavior setting. backgroundQueueMinNumberOfTasks removed This option is no longer available. backgroundQueueSecondsDelay removed This option is no longer available. --- ## 3.x -> 3.4 URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/update-to-3.4/ This page explains how to update your SDK install to latest versions that may not require a breaking change. While these changes aren't breaking—you don't _need_ to make these changes—they will simplify your integration, improve the reliability of your metrics, and improve deep link handling on iOS devices. Upgrade from 3.3 to 3.4+ As of version 3.4, the Customer.io SDK automatically registers push device tokens to identified people and handles push clicks. These features simplify your SDK integration while improving compatibility with apps that use multiple push SDKs. After you install a version of the SDK that is 3.4 or higher, follow these steps to upgrade.  Do you have a swift app? Skip ahead! If you’ve got a Swift app containing the AppDelegate.swift file, ignore the steps below and go to the Swift upgrade section. Open your push notification handler file (In our examples, we call this file MyAppPushNotificationsHandler.swift) and review all of the highlighted code below. We’ve highlighted the most relevant lines. import Foundation import CioMessagingPushAPN import UserNotifications // Delete this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} // Replace these 2 lines @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // With these 2 lines @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } // Delete these 2 lines: let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } // Delete this function: @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } } Open your AppDelegate.h file and review all of the highlighted code below. APN APN #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate @end FCM FCM #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate, UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate> @end Open your AppDelegate.m file and review all of the highlighted code below. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... // Replace this line [pnHandlerObj setupCustomerIOClickHandling:self]; // With this line: [pnHandlerObj setupCustomerIOClickHandling]; return YES; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { // Remove the line below: [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } Now that your app’s code has been simplified, follow the latest push notification setup documentation to enable these new features. Upgrade from 3.3 to 3.4+, for Swift Open your AppDelegate.swift file and review all of the highlighted code below. We’ve highlighted the most relevant lines. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Delete this line UIApplication.shared.registerForRemoteNotifications() return true } } // Delete this function func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } // Delete this function func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/update-to-3x/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Installing and updating our React Native SDK got easier. After you install the CustomerIO React Native SDK version 3.x, open your ios/Podfile and follow all 5 steps shown in this code block below: # 1. This line is required by the FCM SDK. If you encounter problems during 'pod install', add this line to your Podfile and try 'pod install' again. use_frameworks! :linkage => :static target 'YourApp' do # Note: 'YourApp' is unique to your app. This is here for example purposes, only. # 2. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me # 3. Add one of these new lines below: # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end target 'NotificationServiceExtension' do # 4. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me pod 'FirebaseMessaging' # Remove me, unless you need to specify a specific version pod 'Firebase' # Remove me, unless you need to specify a specific version. # 5. Add one of these new lines below: # ⚠️ Important: Notice these lines of code include "-richpush" in it making it unique to the host app target above. # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end After you modify your Podfile, run the command pod update --repo-update --project-directory=ios to make your changes to ios/Podfile go into effect. Upgrade from 1.x to 2.x Rich push initialization(iOS) If you followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Due to the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your Notification Service Extension. In the case that you use Objective-C, you must add the code snippet below into the Swift handler file that you created in NotificationService Extension. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Firebase users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. If you send messages to your iOS app using FCM, you’ll need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. pod 'Firebase' pod 'FirebaseMessaging' We fixed a bug in our iOS modules that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in our iOS v1 modules that converted keys in your custom data to snake_case. This bug is fixed in v2 of the SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect your Android users.. // If you passed in custom attributes using camelCase keys: data = {"firstName": "Dana"} // The SDK v1 may have converted this data into: data = {"first_name": "Dana"} // Or, if you used a different format that was not snake_case: data = {"FIRSTNAME": "Dana"} // The SDK v1 may have converted this data into: data = {"f_irstname": "Dana"} You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Call the Customer.io SDK and provide custom attributes like this: CustomerIO.identify("dana@example.com", {"first_name": "Dana"}) // Consider sending duplicate data with snake_case CustomerIO.identify("dana@example.com", { "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. }) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.identify("dana@example.com", { "firstName": "Dana" // We can remove the snake_case attribute and go back to just camelCase! }) --- ## Changelog URL: https://docs.customer.io/integrations/sdk/react-native/5.x/whats-new/changelog/ Check out release history our React Native SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. test --- ## Get Started URL: https://docs.customer.io/integrations/sdk/react-native/2.x/getting-started/ Before you can take advantage of our SDK, you need to install and initialize the SDK. This page also explains how the SDK prioritizes operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Prerequisites Before you get started with our React Native SDKs, you’ll need your Customer.io workspace Site ID and API Key. You’ll provide these credentials when you initialize the SDK. Because our React Native package relies on our native iOS and Android modules, you’ll need to set up both your React Native development environment and make sure that you’re set up to support both iOS and Android in your environment.  You no longer need your Organization ID If you enabled in-app support before January 26, 2023, you used your organization-id when configuring our SDKs so that you could send in-app messages. You can leave this code in your SDK configuration, but it’s no longer necessary; you can send in-app messages without it. React Native Set up your React Native environment Add React navigation to your project to support deep links and screen tracking iOS Setup XCode and set your deployment target to 13.0 or later Make sure that you’ve got XCode command line tools installed—xcode-select --install Get your Apple Push Certificate and enable push notifications for iOS in your Customer.io account You should have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Android Download and install Android Studio Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android Android Gradle plugin version 7.4 or later An Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33) Install the React Native SDK This process involves setup for both iOS and Android. For Android, we’ll guide you through the process to set up Firebase Cloud Messaging (FCM) in your app.  In-app messaging is disabled by default If you plan to send in-app messages, you need to set the enableInApp flag when you configure the SDK. Open your terminal and go to your project folder—cd <Root/path/to/your/app>. Install the customerio-reactnative package using NPM or Yarn: npm install customerio-reactnative yarn add customerio-reactnative If you’re using a React Native version earlier than 0.60, link the library manually with npx react-native link customerio-reactnative. Otherwise, go to the next step. In your terminal, run pod install --repo-update --project-directory=ios. This adds the required iOS dependencies to your project. When the process is complete , you’ll see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Make sure that your minimum deployment target is set to 13.0. You’ll have to do this in two places: Go to the ios subfolder and open your Podfile. Find the platform:ios line, and make sure that the version is set to 13.0 or later if it isn’t already. Open your project’s iOS directory in XCode, select the project under Targets, and set the Minimum Deployments target to 13.0 or later. Go to the Android subfolder and include google-services-plugin by adding the following lines to the project-level android/build.gradle file: buildscript { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } dependencies { // Add this line: classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } } Add the following line to android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and copy the file to android/app/google-services.json. Now you’re ready to initialize the SDK and use it in your app. Initialize the SDK After you install the SDK, you’ll need to initialize it in your app. To do this, you’ll add initialization code in your App.js file—or wherever you want to initialize the customerio-reactnative package. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. This makes the SDK available to use in your app. Note that you’ll still need to identify your app’s users before you can send them messages. import React, {useEffect} from 'react'; import { CustomerIO, CustomerIOEnv, Region } from 'customerio-reactnative'; const App = () => { useEffect(() => { const env = new CustomerIOEnv() env.siteId = "YourSiteId" env.apiKey = "YourAPIKey" // Region is optional, defaults to Region.US. // Use Region.EU for EU-based workspaces. env.region = Region.US CustomerIO.initialize(env) }, []) When you’re done, you may want to return to your main folder and run your application to make sure that everything’s set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android  Check out our sample app! We’ve provided examples that you can follow to implement our React Native SDK in your apps. Check it out! Configure the SDK You can determine global behaviors for the SDK in the CustomerIO.config object. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CustomerioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. import { CustomerIO, CustomerioConfig } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug data.autoTrackDeviceAttributes = true // In-app messages are optional and disabled by default // To enable in-app messages, set enableInApp to true data.enableInApp = true // `env` is the environment constant you used // to initialize the SDK in the previous section CustomerIO.initialize(env, data) When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. enableInApp boolean false Enables in-app messaging. See in-app messaging for more details. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. trackApiUrl string Do not change this setting. This points to our Track API. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] --- ## Identify people URL: https://docs.customer.io/integrations/sdk/react-native/2.x/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. body (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person. import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify("person@example.com", {"first_name": "Dana"}) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood : "Pizza", favouriteDrink : "Mango Shake", }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify("new.person@example.com", {"first_name": "New", "last_name": "Person"})  --- ## Track events URL: https://docs.customer.io/integrations/sdk/react-native/2.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. You can use events to trigger campaigns, add people to segments, etc. Your event-triggered campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. A data object (Optional): Additional information that you might want to reference in messages or use to segment your audience, etc. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.track("add-to-cart", {"product": "shoes", "price": "29.99"}) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using React Navigation for automatic screen tracking. This example requires @react-navigation/native and @react-navigation/native-stack to create a navigation container in App.js If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useRef } from 'react'; const Stack = createNativeStackNavigator(); export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} > <Stack.Navigator initialRouteName="FirstScreen"> <Stack.Screen name="FirstScreen" component={FirstScreen}/> <Stack.Screen name="SecondScreen" component={SecondScreen} options={{ title : "My App", headerStyle: { backgroundColor: '#F6F7F9', }, }} /> </Stack.Navigator> </NavigationContainer> ); } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/react-native/2.x/push-notifications/push/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style push fill:#B5FFEF,stroke:#007069 How it works Under the hood, our React Native SDK takes advantage of our native Android and iOS SDKs. This helps us keep the React Native SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Add push notification capabilities in XCode. (iOS) Integrate push notifications: code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. Request, or check for, push notification permissions. If your app’s user doesn’t grant permission, notifications will not appear in the system tray. While push providers support a number of features in their payloads, our React Native package only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities, but there are still a few more steps to finish setting things up. Go to File > New > Target. Select Notification Service Extension and click Next. Enter a product name, like NotificationServiceExtension (which we use in our examples on this page), and click Finish. When presented with the dialog below, click Cancel. This will help Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Pick your push provider (APN or FCM) and the language your native files are written in to get started (Objective C or Swift). APN/Objective-CAPN/SwiftFCM/Objective-CFCM/Swift APN/Objective-C Open the file ios/Podfile and make the following modifications: pod 'CustomerIO/MessagingPushAPN', '~> 2.14.2'  Want to automatically get the latest versions? The example above includes the full version number. If you remove the patch and/or minor version numbers, you’ll always get the latest minor release when you run pod update --repo-update --project-directory=ios. See Updating iOS Dependencies for information about updating your Podfile. Outside your main target, add the following line to the Podfile. pod 'CustomerIO/MessagingPushAPN', '~> 2.14.2' Open your terminal, go to your project path and run pod install --project-directory=ios. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. In our examples, we’ve named this file MyAppPushNotificationsHandler.swift but you should use a name that makes sense to you. Replace the file contents with the code below. We’re calling our class MyAppPushNotificationsHandler, but you might want to rename it to fit your app. import Foundation import CioMessagingPushAPN import UserNotifications import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } Open AppDelegate.h and add the following import statements and the UNUserNotificationCenterDelegate delegate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate> @end Open AppDelegate.m and import your project’s Objective-C header file. #import <MyAppProject-Swift.h> Inside AppDelegate’s implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; ...  If you get a compile-time error… See our troubleshooting section if you receive a message that reads Error: Initializer element is not a compile-time constant. Update AppDelegate.m to register a device to the current app user and handle push notifications. See comments in the sample below to understand what the code does! - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... [pnHandlerObj setupCustomerIOClickHandling:self]; return YES; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Register device to receive push notifications with device token [pnHandlerObj application:application deviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [pnHandlerObj application:application error:error]; } // Send push notification click events to the Customer.IO SDK for processing - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } // (Optional) Add the following code to show your push notifications even when your app is in the foreground. - (void)userNotificationCenter:(UNUserNotificationCenter* )center willPresentNotification:(UNNotification* )notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound); }  Is your app using other SDKs that work with push notifications? The code snippet above configures the Customer.io SDK to process all push notifications when they are clicked. If your app is using other SDKs that work with push notifications (such as expo-notifications, react-native-push-notification, or rnfirebase), you will need to follow these instructions to ensure that all SDKs work together. In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like MyAppNotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the content from the snippet below and replace the code in your MyAppNotificationServicePushHandler.swift file. You might change the name of the class to fit your project. Refer the comments in the snippet below for more information.  This code is based on our iOS 2.x SDK! If you’ve integrated based on an earlier version (your podfile targets a MessagingPushAPN version before 2.0), CustomerIO.initialize does not support the config object. It’s simply: CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) import Foundation import UserNotifications import CioMessagingPushAPN import CioTracking @objc public class MyAppNotificationServicePushHandler : NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You can configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackDeviceAttributes = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file—e.g. YourTargetName-Swift.h. #import <NotificationServiceExtension-Swift.h> Create an object of class MyAppNotificationServicePushHandler in your NotificationServiceExtension implementation and call the functions below. #import "NotificationService.h" #import <NotificationServiceExtension-Swift.h> @interface NotificationService () @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation NotificationServiceExtension // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. APN/Swift Open the file ios/Podfile and make the following modifications: pod 'CustomerIO/MessagingPushAPN', '~> 2.14.2'  Want to automatically get the latest versions? The example above includes the full version number. If you remove the patch and/or minor version numbers, you’ll always get the latest minor release when you run pod update --repo-update --project-directory=ios. See Updating iOS Dependencies for information about updating your Podfile. Add the following line to your Podfile. target 'NotificationServiceExtension' do pod 'CustomerIO/MessagingPushAPN', '~> 2.14.2' end Open your terminal, go to your project path and run pod install --project-directory=ios. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. In your iOS subfolder, open AppDelegate.swift and add a line inside the application function. We have an example in our sample app. This is a workaround for an issue preventing iOS devices from being added to the Podfile after being identified. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Important to call this code *before* you call registerForPushNotifications. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) Add the following code to your app.  This sample includes a work-around For now, it’s important that you call CustomerIO.initialize before registerForPushNotifications. This fixes an issue in which the SDK identifies the podfile, but doesn’t add a device to the podfile—preventing the user from receiving notifications. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { // This fixes an issue in which the React Native SDK identifies // a profile, but an iOS device isn't added to the profile. // Important to call this code before calling `registerForRemoteNotifications`. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // It's a good practice to register for remote push when the app starts. // This asserts that the Customer.io SDK always has a valid device token. UIApplication.shared.registerForRemoteNotifications() return true } } // PUSH NOTIFICATIONS func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. However, if you want to enable deep links, you should continue to the Deep links section below.  This code is based on our iOS 2.x SDK! If you’ve integrated based on an earlier version (your podfile targets a MessagingPushAPN version before 2.0), CustomerIO.initialize does not support the config object. It’s simply: CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) import UserNotifications import CioMessagingPushAPN import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You can configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackDeviceAttributes = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM/Objective-C Open the file ios/Podfile and make the following modifications: pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' pod 'Firebase' pod 'FirebaseMessaging' Add the following line outside your main target. # You may need to add this line, as required by FCM, to your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'NotificationServiceExtension' do pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' end Open your terminal, go to your project path and run pod install --project-directory=ios. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. In our examples, we’ve named this file MyAppPushNotificationsHandler.swift but you should use a name that makes sense to you. Replace the file contents with the code below. We’re calling our class MyAppPushNotificationsHandler, but you might want to rename it to fit your app. import Foundation import CioMessagingPushFCM import UserNotifications import FirebaseMessaging import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } // Register device on receiving a device token (FCM) @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } Open AppDelegate.h and add the following import statements, the UNUserNotificationCenterDelegate delegate, and the FIRMessagingDelegate delegate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> #import <FirebaseMessaging/FIRMessaging.h> @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate, FIRMessagingDelegate> @end In AppDelegate.m, import your project’s Objective-C Generated header file and FirebaseCore. #import <MyAppProject-Swift.h> #import <FirebaseCore.h> Inside AppDelegate’s implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; ...  If you get a compile-time error… See our troubleshooting section if you receive a message that reads Error: Initializer element is not a compile-time constant. Make the following updates to your AppDelegate.m file. See comments in the sample below to understand what the code does! - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Configure Firebase [FIRApp configure]; // Set FCM messaging delegate [FIRMessaging messaging].delegate = self; ... [pnHandlerObj setupCustomerIOClickHandling:self]; return YES; } - (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken { [pnHandlerObj didReceiveRegistrationToken:messaging fcmToken: fcmToken]; } // Send push notification click events to the Customer.IO SDK for processing - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } // (Optional) Add the following code to show your push notifications even when your app is in the foreground. - (void)userNotificationCenter:(UNUserNotificationCenter* )center willPresentNotification:(UNNotification* )notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound); }  Is your app using other SDKs that work with push notifications? The code snippet above configures the Customer.io SDK to process all push notifications when they are clicked. If your app is using other SDKs that work with push notifications (such as expo-notifications, react-native-push-notification, or rnfirebase), you will need to follow these instructions to ensure that all SDKs work together. In XCode, select your NotificationServiceExtension. Select File > New > File > Swift File and click Next. Enter a file name, like MyAppNotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the content from the snippet below and replace the code in your MyAppNotificationServicePushHandler.swift file. You might change the name of the class to fit your project. Refer the comments in the snippet below for more information.  This code is based on our iOS 2.x SDK! If you’ve integrated based on an earlier version (your podfile targets a MessagingPushAPN version before 2.0), CustomerIO.initialize does not support the config object. It’s simply: CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) import Foundation import UserNotifications import CioMessagingPushFCM import CioTracking @objc public class MyAppNotificationServicePushHandler : NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You may choose to configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackDeviceAttributes = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file—e.g. YourTargetName-Swift.h. #import <NotificationServiceExtension-Swift.h> Create an object of class MyAppNotificationServicePushHandler in your NotificationServiceExtension implementation and call the functions below. #import "NotificationService.h" #import <NotificationServiceExtension-Swift.h> @interface NotificationService () @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation NotificationServiceExtension // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. FCM/Swift Open the file ios/Podfile and make the following modifications: pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' pod 'Firebase' pod 'FirebaseMessaging' <div class="fly-panel bg-info border-info"> <div class="fly-panel-body"> <p class="callout-head text-info mrg-b-none mrg-t-none text--bold"><svg class="icon callout-icon"><use href="#pin" /></svg>&nbsp;Want to automatically get the latest versions?</p> <div class='text-info'>The example above includes the full version number. If you remove the patch and/or minor version numbers, you&rsquo;ll always get the latest minor release when you run <code>pod update --repo-update --project-directory=ios</code>. See <a href="#update-ios-dependencies">Updating iOS Dependencies</a> for information about updating your Podfile.</div> </div> </div> Add the following line to your Podfile. target 'NotificationServiceExtension' do pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' end Open your terminal, go to your project path, and run pod install --project-directory=ios. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Update your AppDelegate.swift file to handle push notifications.  This sample includes a work-around For now, it’s important that you call CustomerIO.initialize before registerForPushNotifications. This fixes an issue in which the SDK identifies the profile, but doesn’t add a device to the profile—preventing the user from receiving notifications. import CioMessagingPushFCM import CioTracking import Firebase import FirebaseMessaging class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { // This fixes an issue in which the React Native SDK identifies // a profile, but an iOS device isn't added to the profile. // Important to call this code before calling `registerForRemoteNotifications`. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // You should register for remote push when the app starts. UIApplication.shared.registerForRemoteNotifications() return true } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } (Optional) Add this function to AppDelegate.swift if you want to show your push notifications even when the app is in the foreground. func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. However, if you want to enable deep links, you should continue to the Deep links section below.  This code is based on our iOS 2.x SDK! If you’ve integrated based on an earlier version (your podfile targets a MessagingPushAPN version before 2.0), CustomerIO.initialize does not support the config object. It’s simply: CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) import UserNotifications import CioMessagingPushFCM import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You may choose to configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackPushEvents = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user has not yet been asked to opt into notifications, the method will return NotDetermined (only for iOS). var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now send push notifications to the user break; case "Denied": // App is not allowed to send push notifications to the user // You might need to explain users why your app needs permission to send notifications break; case "NotDetermined": // Only for iOS // Push permission status is not determined break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Optional: Remove POST_NOTIFICATIONS permission from Android apps By default, the SDK includes the POST_NOTIFICATIONS permission which is required by Android 13 to show notifications on Android device. However, if you do not want to include the permission because don’t use notifications, or for any other reason, you can remove the permission by adding the following line to your android/app/src/main/AndroidManifest.xml file: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/> Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/react-native/2.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are links that send a person from push notifications to pages in your app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android an iOS versions of your React Native app. Android: set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. iOS: set up deep links After you set up push notifications you can enable deep links in rich push notifications. There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps.  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Open your AppDelegate.m file and add this code. // Add this import statement at the top of the file #import <React/RCTLinkingManager.h> // Add this inside in AppDelegate implementation - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { return [RCTLinkingManager application:application openURL:url options:options]; } Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/react-native/2.x/push-notifications/multiple-push-providers/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If you use another push service alongside our SDK (like rnfirebase), it will take over push handling by default and prevent your app from receiving rich push notifications from Customer.io. You can solve this problem using one (and only one) of the methods below, but we typically recommend the first option, because it doesn’t require you to write native code! Option 1 (Recommended): Let Customer.io process notification payloads You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the onMessageReceived method for this that takes two arguments: a message.data object containing the incoming notification payload a handleNotificationTrigger boolean indicating whether or not to trigger a notification. A true value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A false value means that the SDK will only process the notification to track metrics but will not generate a notification on the device. You’ll use the onMessageReceived like this: CustomerIO.pushMessaging().onMessageReceived(message).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); You can pass values in onMessageReceived by listening to notification events exposed by other SDKs. Make sure that you add listeners in the right places to process notifications that your app receives when it’s in the foreground and add background listeners that might be required by other SDK to process notifications that your app receives when it’s in background/killed state. If you always send rich push messages (with image and/or link), adding event listeners is enough. But if you send custom push payloads using the notification object or send simple push messages (with just a body and title), you may get duplicate notifications when your app is backgrounded because Firebase itself displays notifications sent using the notification object. To avoid this, You can pass false in handleNotificationTrigger to track metrics for simple and custom payload push notifications. To simplify this behavior, the SDK also exposes an onBackgroundMessageReceived method that automatically suppresses pushes with the notification object when your app is in background. If you use rnfirebase, you can setup listeners like this: Foreground Listener Foreground Listener To listen to messages in the foreground, set onMessage listener where appropriate: useEffect(() => { const unsubscribe = messaging().onMessage(async remoteMessage => { CustomerIO.pushMessaging().onMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); return unsubscribe; }, []); Background Listener Background Listener To listen to messages when app is in background/killed state, set setBackgroundMessageHandler in your index.js file messaging().setBackgroundMessageHandler(async remoteMessage => { CustomerIO.pushMessaging().onBackgroundMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); Option 2: Register Customer.io Messaging Service You can register Customer.io’s messaging service in your Manifest file so that we handle all notifications for your app. You can do this by adding the following code under the <application> tag in the AndroidManifest.xml file in your app’s android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  The Customer.io SDK will handle all your push notifications The code above hands all push notifications responsibility to our SDK, meaning: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/react-native/2.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. How it works Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked.  Update to version 3 for Javascript functions The React Native SDK version 3.0 introduced support for tracking push metrics using Javascript methods, eliminating the need for adding any native code. It’s recommended you update to the latest version to take advantage of this feature. If you already configured push notifications and your app does not use other push notification modules such as expo-notifications or react-native-push-notification, the SDK automatically tracks opened and delivered events for push notifications originating from Customer.io. Otherwise, use one of the following methods to manually track push metrics: Record push metrics with native code. iOS: Record push metrics with native code If you’re using Objective-C, the example in our push setup process already contains the code required to handle push metrics—substituting AppDelegate for your push notification class. If you use Swift, add the following code to track push notifications in your app. extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, you need to handle it and call the // completion handler. If the SDK handles it, it calls the completion handler for you. if !handled { completionHandler() } } } If delivered events are important to you, we recommend that you follow the setup instructions for rich push notifications, even if you do not plan on sending rich push notifications as rich push tracks delivered events more reliably. Disabling automatic push tracking Automatic push metric recording is enabled by default when you install the SDK. You can disable this behavior by passing a configuration option when you initialize the SDK. You cannot disable automatic push tracking (or change other configuration settings) after you initialize the SDK. const cioConfigOptions = new CustomerioConfig() cioConfigOptions.autoTrackPushEvents = false const env = new CustomerIOEnv() env.siteId = Env.siteId env.apiKey = Env.apiKey CustomerIO.initialize(env, cioConfigOptions) --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/2.x/in-app-messages/set-up-in-app/ This page describes how to implement mobile in-app messages. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set enableInApp to true in your CustomerioConfig(), and your app will be able to receive in-app messages.  You no longer need your Organization ID If you enabled in-app support before January 26, 2023, you used your organization-id when configuring our SDKs so that you could send in-app messages. You can leave this code in your SDK configuration, but it’s no longer necessary; you can send in-app messages without it. const data = new CustomerioConfig() data.enableInApp = true Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/react-native/2.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also listen for in-app message events and handle responses yourself. And if you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging().registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging().dismissMessage(); --- ## Migrate from an earlier version URL: https://docs.customer.io/integrations/sdk/react-native/2.x/updates-and-troubleshooting/migrate-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 1.x to 2.x Rich push initialization(iOS) If you followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Due to the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your Notification Service Extension. In the case that you use Objective-C, you must add the code snippet below into the Swift handler file that you created in NotificationService Extension. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Firebase users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. If you send messages to your iOS app using FCM, you’ll need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. pod 'Firebase' pod 'FirebaseMessaging' We fixed a bug in our iOS modules that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in our iOS v1 modules that converted keys in your custom data to snake_case. This bug is fixed in v2 of the SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect your Android users.. // If you passed in custom attributes using camelCase keys: data = {"firstName": "Dana"} // The SDK v1 may have converted this data into: data = {"first_name": "Dana"} // Or, if you used a different format that was not snake_case: data = {"FIRSTNAME": "Dana"} // The SDK v1 may have converted this data into: data = {"f_irstname": "Dana"} You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Call the Customer.io SDK and provide custom attributes like this: CustomerIO.identify("dana@example.com", {"first_name": "Dana"}) // Consider sending duplicate data with snake_case CustomerIO.identify("dana@example.com", { "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. }) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.identify("dana@example.com", { "firstName": "Dana" // We can remove the snake_case attribute and go back to just camelCase! }) --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/react-native/2.x/updates-and-troubleshooting/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Capture logs Logs help us pinpoint the problem and find a solution. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import { CustomerIO, CustomerioConfig, CustomerIOEnv, Region } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug CustomerIO.initialize(env, data) ; Build and run your app on a physical device or emulator. In the console, run: react-native log-ios react-native log-android Export your log to a text file and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Push notification issues Try updating iOS package dependencies This SDK uses our iOS push package. In some cases, we may make fixes in our iOS packages that fix downstream issues in, or expose new features to this SDK. You can update the version in your podfile and then run the following command to get the latest iOS packages. Our instructions above list out the full version of the iOS push package. If you want to automatically increment packages, you can remove the patch and minor build numbers (the second and third parts of the version number), and pod update will automatically fetch the latest package versions. However, please understand that fetching the latest versions can cause build issues if the latest iOS package doesn’t agree with code in your app! pod update --repo-update --project-directory=ios Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Error: Initializer element is not a compile-time constant If you get this error while initializing the object in AppDelegate, go to your project’s ios directory, open AppDelegate.m and update the object of your amiappPushNotificationsHandler—or however you named your push handler—with the following code. @implementation AppDelegate PushNotificationsHandler* pnHandlerObj = nil; + (void)initialize { pnHandlerObj = [[amiappPushNotificationsHandler alloc] init]; } Deep linking to iOS when your app is closed There’s a known issue preventing deep links from working when your app is closed on iOS devices. When the app is closed, the native click event is fired before the react app’s lifecycle starts. We recommend a two-step workaround: Add an additional, custom parameter in your push payload, like react-deep-link in the example below. We can use this field in step 2. { "CIO": { "push": { "image": "[https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg](https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg)", "link": "amiapp://showtrial" } }, "react-deep-link": "amiapp://showtrial", "aps": { "mutable-content": 1, "sound": "default", "alert": { "title": "Voila! You just logged in as a user", "sound": "default", "body": "Thanks for using the app." } } } Update didFinishLaunchingWithOptions in your AppDelegate.m file with the code below. We use the variable modifiedLaunchOptions to create a bridge object in the last line of this code, which grabs the link and sends users to the specified screen. NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"react-deep-link"]) { NSString *initialURL = pushContent[@"react-deep-link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } // Replace `launchOptions` with `modifiedLaunchOptions` RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; ... // Use the `bridge` object in `RCTAppSetupDefaultRootView` UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); Compiler error: ‘X’ is unavailable in application extensions for iOS This error occasionally occurs when users add a notification extension to handle rich push messages. If you see this error, try the following steps: Add this code to the end of your Podfile: post_install do |installer| installer.pods_project.targets.each do |target| if target.name.start_with?('CustomerIO') puts "Modifying target #{target.name} with workaround" target.build_configurations.each do |config| puts "Setting build config settings for #{target.name}" config.build_settings['APPLICATION_EXTENSION_API_ONLY'] ||= 'NO' end end end end In the root directory of your app, run pod install --project-directory=ios. This command will apply the above workaround to your project. Try to compile your app again. If you still see the error message, it’s likely that the error you see is related to a different SDK that you use in your app and not the Customer.io SDK. We suggest that you contact the developers of the SDK that you see in the error message for help. If you don’t see an error message, send our technical support team a message with: The error message that you see when compiling your app. The contents of your ios/Podfile and ios/Podfile.lock files. The version of the React Native SDK that you are using. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/react-native/2.x/updates-and-troubleshooting/changelog/ Check out release history our React Native SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. test --- ## Get Started URL: https://docs.customer.io/integrations/sdk/react-native/3.x/getting-started/ Before you can take advantage of our SDK, you need to install and initialize the SDK. This page also explains how the SDK prioritizes operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Prerequisites Before you get started with our React Native SDKs, you’ll need your Customer.io workspace Site ID and API Key. You’ll provide these credentials when you initialize the SDK. Because our React Native package relies on our native iOS and Android modules, you’ll need to set up both your React Native development environment and make sure that you’re set up to support both iOS and Android in your environment. To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Add React Navigation to your app to support deep links and screen tracking. Before you begin: set up your development environment Before you get started, you’ll need to do the following things: Set up your React Native environment Add React navigation to your project to support deep links and screen tracking Set up your iOS environment: Setup XCode (using deployment target to 13.0 or later). Make sure that you have XCode command line tools installed xcode-select --install. Get your Apple Push Certificate and enable push notifications for iOS in your Customer.io account. Have an iOS 13 to test with. You cannot test push notifications in an emulator. Set up your Android environment: Download and install Android Studio Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android Make sure you use an appropriate version of the Android Gradle plugin. Have an Android device or emulator with Google Play Services enabled and a minimum OS version. Install the React Native SDK This process involves setup for both iOS and Android. For Android, the directions below will guide you through the setup process for both Firebase Cloud Messaging (FCM) and our in-app messaging module; both are required to use the React Native SDK.  In-app messaging is disabled by default If you plan to send in-app messages, you need to set the enableInApp flag when you configure the SDK. Open your terminal and go to your project folder—cd <Root/path/to/your/app>. Install the customerio-reactnative package using NPM or Yarn: npm install customerio-reactnative yarn add customerio-reactnative If you’re using a React Native version earlier than 0.60, link the library manually with npx react-native link customerio-reactnative. Otherwise, go to the next step. In your terminal, run pod install --repo-update --project-directory=ios. This adds the required iOS dependencies to your project. When the process is complete , you’ll see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Make sure that your minimum deployment target is set to 13.0. You’ll have to do this in two places: Go to the ios subfolder and open your Podfile. Find the platform:ios line, and make sure that the version is set to 13.0 or later if it isn’t already. Open your project’s iOS directory in XCode, select the project under Targets, and set the Minimum Deployments target to 13.0 or later. Go to the Android subfolder and include google-services-plugin by adding the following lines to the project-level android/build.gradle file: buildscript { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } dependencies { // Add this line: classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } } Add the following line to android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and copy the file to android/app/google-services.json. Now you’re ready to initialize the SDK and use it in your app. Initialize the SDK After you install the SDK, you’ll need to initialize it in your app. To do this, you’ll add initialization code in your App.js file—or wherever you want to initialize the customerio-reactnative package. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. This makes the SDK available to use in your app. Note that you’ll still need to identify your app’s users before you can send them messages. import React, {useEffect} from 'react'; import { CustomerIO, CustomerIOEnv, Region } from 'customerio-reactnative'; const App = () => { useEffect(() => { const env = new CustomerIOEnv() env.siteId = "YourSiteId" env.apiKey = "YourAPIKey" // Region is optional, defaults to Region.US. // Use Region.EU for EU-based workspaces. env.region = Region.US CustomerIO.initialize(env) }, []) When you’re done, you may want to return to your main folder and run your application to make sure that everything’s set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android  Check out our sample app! We’ve provided examples that you can follow to implement our React Native SDK in your apps. Check it out! Configure the SDK You can determine global behaviors for the SDK in the CustomerIO.config object. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CustomerioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. import { CustomerIO, CustomerioConfig } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug data.autoTrackDeviceAttributes = true // In-app messages are optional and disabled by default // To enable in-app messages, set enableInApp to true data.enableInApp = true // `env` is the environment constant you used // to initialize the SDK in the previous section CustomerIO.initialize(env, data) When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. enableInApp boolean false Enables in-app messaging. See in-app messaging for more details. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. trackApiUrl string Do not change this setting. This points to our Track API. Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] Using the SDK as a data source The SDK uses our Legacy Track API API, but it can also double as a source of data for other integrations without additional development work. To do this, we translate calls from the SDK to our newer Data Pipelines API format before we send them to your destinations. In general, we recommend that you upgrade your app to use a newer version of the SDK. Our newer versions rely on the Data Pipelines API, so you can take advantage of your mobile data without without us translating it for you. It can make it easier to trace data from your app to your destinations and troubleshoot issues as they arise. Our newer SDKs also support more features, like anonymous tracking. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/react-native/3.x/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. body (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person. import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify("person@example.com", {"first_name": "Dana"}) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood : "Pizza", favouriteDrink : "Mango Shake", }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify("new.person@example.com", {"first_name": "New", "last_name": "Person"})  --- ## Track events URL: https://docs.customer.io/integrations/sdk/react-native/3.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. You can use events to trigger campaigns, add people to segments, etc. Your event-triggered campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. A data object (Optional): Additional information that you might want to reference in messages or use to segment your audience, etc. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.track("add-to-cart", {"product": "shoes", "price": "29.99"}) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using React Navigation for automatic screen tracking. This example requires @react-navigation/native and @react-navigation/native-stack to create a navigation container in App.js If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useRef } from 'react'; const Stack = createNativeStackNavigator(); export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} > <Stack.Navigator initialRouteName="FirstScreen"> <Stack.Screen name="FirstScreen" component={FirstScreen}/> <Stack.Screen name="SecondScreen" component={SecondScreen} options={{ title : "My App", headerStyle: { backgroundColor: '#F6F7F9', }, }}/> </Stack.Navigator> </NavigationContainer> ); }; Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/react-native/3.x/push-notifications/push/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style push fill:#B5FFEF,stroke:#007069 How it works Under the hood, our React Native SDK takes advantage of our native Android and iOS SDKs. This helps us keep the React Native SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Add push notification capabilities in XCode. (iOS) Integrate push notifications: code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. Request, or check for, push notification permissions. If your app’s user doesn’t grant permission, notifications will not appear in the system tray. While push providers support a number of features in their payloads, our React Native package only supports deep links and images right now. If you want to include action buttons or other rich push features, you need to add your own custom code. When writing your own custom code, we recommend that you use our SDK as it is much easier to extend than writing your own code from scratch.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities, but there are still a few more steps to finish setting things up. Go to File > New > Target. Select Notification Service Extension and click Next. Enter a product name, like NotificationServiceExtension (which we use in our examples on this page), and click Finish. When presented with the dialog below, click Cancel. This helps Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app 🎉Updated in version 3.4.0  Using version 3.3 or earlier? If you implemented push notifications using a previous version of the SDK, you can remove a significant amount of code when you update. Follow our upgrade guide to remove unnecessary code and increase compatibility with 3rd party SDKs in your app. Pick your push provider (APN or FCM) and the language your native files are written in to get started (Objective C or Swift). APN/Objective-CAPN/SwiftFCM/Objective-CFCM/Swift APN/Objective-C Open your ios/Podfile and add a line to your main target and NotificationServiceExtension target. require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip platform :ios, '13.0' install! 'cocoapods', :deterministic_uuids => false require 'open-uri' IO.copy_stream(URI.open('https://raw.githubusercontent.com/customerio/customerio-ios/main/scripts/cocoapods_override_sdk.rb'), "/tmp/override_cio_sdk.rb") load "/tmp/override_cio_sdk.rb" target 'SampleApp' do config = use_native_modules! # Flags change depending on the env values. flags = get_default_flags() pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true, :fabric_enabled => flags[:fabric_enabled], # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) end end target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file and replace the siteId and apiKey (on the highlighted line) with your credentials. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import Foundation import CioMessagingPushAPN import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId, apiKey and region with your own values. CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } MessagingPushAPN.initialize(configOptions: nil) } @objc(application:deviceToken:) public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } @objc(application:error:) public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } Open your ios/AppDelegate.mm file and import your header file. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. If you’re not a native iOS developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import "SampleApp-Swift.h" Inside AppDelegate’s @implementation, create an object of MyAppPushNotificationsHandler (remember to substitute the name of your handler). @implementation AppDelegate MyAppPushNotificationsHandler* pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to register a device to the current app user and handle push notifications. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RCTAppSetupPrepareApp(application, true); NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; #if RCT_NEW_ARCH_ENABLED _contextContainer = std::make_shared<facebook::react::ContextContainer const>(); _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>(); _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; #endif NSDictionary *initProps = [self prepareInitialProps]; UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); [application registerForRemoteNotifications]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [pnHandlerObj setupCustomerIOClickHandling]; [RNNotifications startMonitorNotifications]; return YES; } ... // Required to register device token. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Register device to receive push notifications with device token [pnHandlerObj application:application deviceToken:deviceToken]; } // Required for the registrationError event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [pnHandlerObj application:application error:error]; } In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy the code on the right and paste it into this new file (which we’ve called NotificationServicePushHandler.swift) file—replacing everything in the file. import Foundation import UserNotifications import CioTracking import CioMessagingPushAPN @objc public class NotificationServicePushHandler : NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // Remember to modify the siteId, apiKey and region with your own values. CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Open your NotificationService.m file and copy the highlighted lines (beginning on line 2) into your file. The name of the header file on line 2 will depend on your extension’s name i.e. YourNotificationServiceExtensionName-Swift.h and is automatically created by Xcode. After this, you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import "NotificationService.h" #import "NotificationServiceExtension-Swift.h" @interface NotificationService () @end @implementation NotificationService // Create object of class NotificationServicePushHandler NotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize{ nsHandlerObj = [[NotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. [nsHandlerObj serviceExtensionTimeWillExpire]; } @end APN/Swift Open your ios/Podfile and add the highlighted lines to your main target and NotificationServiceExtension target. require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip platform :ios, '13.0' install! 'cocoapods', :deterministic_uuids => false require 'open-uri' IO.copy_stream(URI.open('https://raw.githubusercontent.com/customerio/customerio-ios/main/scripts/cocoapods_override_sdk.rb'), "/tmp/override_cio_sdk.rb") load "/tmp/override_cio_sdk.rb" target 'SampleApp' do config = use_native_modules! # Flags change depending on the env values. flags = get_default_flags() pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true, :fabric_enabled => flags[:fabric_enabled], # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) end end target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios In your iOS subfolder, open your AppDelegate.swift file and add this code to your app. This initializes the SDK and handles push notifications. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { // This fixes an issue in which the React Native SDK identifies // a profile, but an iOS device isn't added to the profile. // Important to call this code before calling `registerForRemoteNotifications`. // Remember to modify the siteId, apiKey and region with your own values. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // It's a good practice to register for remote push when the app starts. // This asserts that the Customer.io SDK always has a valid device token. UIApplication.shared.registerForRemoteNotifications() return true } // PUSH NOTIFICATIONS func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications.  This code is based on our iOS 2.x SDK If your podfile targets a messagingPushAPN version before 2.0, CustomerIO.initialize does not support the config object. import UserNotifications import CioMessagingPushAPN import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You can configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackDeviceAttributes = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } FCM/Objective-C Open your ios/Podfile and add the highlighted lines to your main target and NotificationServiceExtension target. # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip platform :ios, 13.0 prepare_react_native_project! use_modular_headers! # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` # ```js # module.exports = { # dependencies: { # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), # ``` flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym end target 'FCMSampleApp' do config = use_native_modules! # Flags change depending on the env values. flags = get_default_flags() # Pods required by Customer.io pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' use_react_native!( :path => config[:reactNativePath], # Hermes is now enabled by default. Disable by setting this flag to false. :hermes_enabled => flags[:hermes_enabled], :fabric_enabled => flags[:fabric_enabled], # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. # :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) __apply_Xcode_12_5_M1_post_install_workaround(installer) end end target 'NotificationServiceExtension' do pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. pod install --project-directory=ios Open ios/<YourAppName>.xcworkspace in Xcode, and add a new Swift file to your project. Copy the code here into your file and replace the siteId and apiKey with your credentials. We’re calling our file MyAppPushNotificationsHandler.swift and the associated class MyAppPushNotificationsHandler, but you might want to rename things to fit your app. import Foundation import CioMessagingPushFCM import FirebaseMessaging import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. CustomerIO.initialize(siteId: Env.siteId, apiKey: Env.apiKey, region: Region.US) { config in // Automatically register push device tokens to the Customer.io SDK config.autoTrackDeviceAttributes = true // This is convenient for internal testing. // It is optional. config.logLevel = .debug } MessagingPushFCM.initialize(configOptions: nil) } // Register device on receiving a device token (FCM) @objc(didReceiveRegistrationToken:fcmToken:) public func didReceiveRegistrationToken(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Open AppDelegate.h and add the FIRMessagingDelegate import statement. If you’re not a native Objective-C developer, the .h and .mm files represent interface and implementation respectively. It’s a convention of XCode to keep these files separate. #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> @interface AppDelegate : RCTAppDelegate <FIRMessagingDelegate> @end Open your ios/AppDelegate.mm file and import your header file, as we’ve shown on line 2 and also import FirebaseCore as we’ve shown on line 5. The name of the header file will depend on your app’s main target name i.e. YourMainTargetName-Swift.h and is auto-created by Xcode. #import "AppDelegate.h" #import <FCMSampleApp-Swift.h> #import <React/RCTLinkingManager.h> #import <React/RCTBundleURLProvider.h> #import <FirebaseCore.h> In AppDelegate.mm, create an object of your push notification handler. We called ours MyAppPushNotificationsHandler. @implementation AppDelegate // Create Object of class MyAppPushNotificationsHandler MyAppPushNotificationsHandler *pnHandlerObj = [[MyAppPushNotificationsHandler alloc] init]; Update AppDelegate.mm to configure Firebase and handle tokens. We’ve highlighted the code here to show what you need to add. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"FCMSampleApp"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; // Configure Firebase [FIRApp configure]; // Set FCM messaging delegate [FIRMessaging messaging].delegate = self; // Use modifiedLaunchOptions for passing link to React Native bridge to sends users to the specified screen NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } [pnHandlerObj setupCustomerIOClickHandling]; return [super application:application didFinishLaunchingWithOptions:modifiedLaunchOptions]; } ... @end In XCode, select your NotificationServiceExtension. Go to File > New > File > Swift File and click Next. Enter a file name, like NotificationServicePushHandler, and click Create. This adds a new swift file in your extension target. Copy this code into the new file. import Foundation import UserNotifications import CioTracking import CioMessagingPushAPN @objc public class NotificationServicePushHandler : NSObject { public override init() {} @objc(didReceive:withContentHandler:) public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // Remember to modify the siteId, apiKey and region with your own values. CustomerIO.initialize(siteId: "siteId", apiKey: "apiKey", region: .US) { config in } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } @objc(serviceExtensionTimeWillExpire) public func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } In your NotificationService.m file, import the auto-generated header file—e.g. NotificationServiceExtension-Swift.h. You’ll also need to create an object class of MyAppNotificationServicePushHandler and call the functions in the code sample here. Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. #import <NotificationServiceExtension-Swift.h> #import "NotificationService.h" @interface NotificationService () @end @implementation NotificationService // Create object of class MyAppNotificationServicePushHandler MyAppNotificationServicePushHandler* nsHandlerObj = nil; // Initialize the object + (void)initialize { nsHandlerObj = [[MyAppNotificationServicePushHandler alloc] init]; } - (void)didReceiveNotificationRequest:(UNNotificationRequest*)request withContentHandler:(void (^)(UNNotificationContent* _Nonnull))contentHandler { [nsHandlerObj didReceive:request withContentHandler:contentHandler]; } - (void)serviceExtensionTimeWillExpire { [nsHandlerObj serviceExtensionTimeWillExpire]; } @end FCM/Swift Open your ios/Podfile and make the changes shown on the right. # Note: You may need to add this line, as required by FCM, to the top of your Podfile if you encounter errors during 'pod install' use_frameworks! :linkage => :static target 'YourApp' do # Look for the main app target. # Make all file modifications after this line: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do # Notice the '-richpush' in the line below. This line of code is different from what you added for your main target. pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end Open your terminal, go to your project path and install the pods. When complete, you should see Pod installation complete! pod install --project-directory=ios Update your AppDelegate.swift file to handle push notifications. You can copy the code sample on the right, but you’ll need to replace the siteId and apiKey with your actual credentials. import CioMessagingPushFCM import CioTracking import Firebase import FirebaseMessaging class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { // This fixes an issue in which the React Native SDK identifies // a profile, but an iOS device isn't added to the profile. // Important to call this code before calling `registerForRemoteNotifications`. // Remember to modify the siteId, apiKey and region with your own values. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Configure Firebase FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // You should register for remote push when the app starts. UIApplication.shared.registerForRemoteNotifications() return true } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } // Optional: add this line if you want to show push // when app is in foreground. func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } Add a notification service extension to call the appropriate Customer.io functions. This lets your app display rich push notifications, including images, etc. See Deep Links if you want to support deep links from push notifications. import UserNotifications import CioMessagingPushFCM import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // You may choose to configure the SDK here // Update region to .EU for your EU-based workspace CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in config.autoTrackPushEvents = true config.logLevel = .info } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Push icon (Android) You’ll set the icon that appears on normal push notifications as a part of your app manifest—android/app/src/main/AndroidManifest.xml. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user has not yet been asked to opt into notifications, the method will return NotDetermined (only for iOS). var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Optional: Remove POST_NOTIFICATIONS permission from Android apps By default, the SDK includes the POST_NOTIFICATIONS permission which is required by Android 13 to show notifications on Android device. However, if you do not want to include the permission because don’t use notifications, or for any other reason, you can remove the permission by adding the following line to your android/app/src/main/AndroidManifest.xml file: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/> Fetch currently stored device token In customerio-reactnative versions 3.3.0 and later, you can fetch the currently stored device token using the CustomerIO.pushMessaging().getRegisteredDeviceToken() method. This method returns an APN/FCM token in a promise as a string. let token = await CustomerIO.pushMessaging().getRegisteredDeviceToken() if (token) { // Use the token as required in your app for example save in a state setDeviceToken(token); } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/react-native/3.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior The pushClickBehaviorAndroid config controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. const env = new CustomerIOEnv(); // setup env const config = new CustomerioConfig(); // setup other config options config.pushClickBehaviorAndroid = PushClickBehaviorAndroid.ACTIVITY_PREVENT_RESTART; CustomerIO.initialize(env, config); The available options are: ACTIVITY_PREVENT_RESTART (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ACTIVITY_NO_FLAGS: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. RESET_TASK_STACK: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links After you set up push notifications you can enable deep links in rich push notifications. There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps.  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Open your AppDelegate.m file and add this code. // Add this import statement at the top of the file #import <React/RCTLinkingManager.h> // Add this inside in AppDelegate implementation - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { return [RCTLinkingManager application:application openURL:url options:options]; } Now you’re ready to handle deep links. In your App.js file or anywhere you handle navigation, you’ll add code that looks like this. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['amiapp://'], config }; return ( <NavigationContainer linking={linking} > ... </NavigationController> ) Set up Universal Links Follow React Native’s documentation to implement Universal Links in your app. --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/react-native/3.x/push-notifications/multiple-push-providers/ Our React Native SDK supports push notifications over APN or FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If you use another module in your app that can display push notifications, like expo-notifications, react-native-push-notification, or rnfirebase, these modules may take over push handling by default and prevent your app from receiving push notifications from Customer.io. If Customer.io is the only SDK that you use in your app to display push notifications, then there is no more work that you need complete in your app. This document describes how to handle this situation if you use another module for push notifications on Android. You can solve this problem using one (and only one) of the methods below, but we typically recommend the first option, because it doesn’t require you to write native code! Please note that the following methods will always return true for iOS. Option 1 (Recommended): Set Customer.io SDK to handle push clicks You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the onMessageReceived method for this that takes two arguments: a message.data object containing the incoming notification payload a handleNotificationTrigger boolean indicating whether or not to trigger a notification. A true value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A false value means that the SDK will only process the notification to track metrics but will not generate a notification on the device. You’ll use the onMessageReceived like this: CustomerIO.pushMessaging().onMessageReceived(message).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); You can pass values in onMessageReceived by listening to notification events exposed by other SDKs. Make sure that you add listeners in the right places to process notifications that your app receives when it’s in the foreground and add background listeners that might be required by other SDK to process notifications that your app receives when it’s in background/killed state. If you always send rich push messages (with image and/or link), adding event listeners is enough. But if you send custom push payloads using the notification object or send simple push messages (with just a body and title), you may get duplicate notifications when your app is backgrounded because Firebase itself displays notifications sent using the notification object. To avoid this, You can pass false in handleNotificationTrigger to track metrics for simple and custom payload push notifications. To simplify this behavior, the SDK also exposes an onBackgroundMessageReceived method that automatically suppresses pushes with the notification object when your app is in background. If you use rnfirebase, you can setup listeners like this: Foreground Listener Foreground Listener To listen to messages in the foreground, set onMessage listener where appropriate: useEffect(() => { const unsubscribe = messaging().onMessage(async remoteMessage => { CustomerIO.pushMessaging().onMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); return unsubscribe; }, []); Background Listener Background Listener To listen to messages when app is in background/killed state, set setBackgroundMessageHandler in your index.js file messaging().setBackgroundMessageHandler(async remoteMessage => { CustomerIO.pushMessaging().onBackgroundMessageReceived(remoteMessage).then(handled => { // If true, the push was a Customer.io notification and handled by our SDK // Otherwise, `handled` is false }); }); Option 2: Register Customer.io Messaging Service You can register Customer.io’s messaging service in your Manifest file so that we handle all notifications for your app. You can do this by adding the following code under the <application> tag in the AndroidManifest.xml file in your app’s android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  The Customer.io SDK will handle all your push notifications The code above hands all push notifications responsibility to our SDK, meaning: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/react-native/3.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK.  Upgrade to a version after 3.4.0! If you’re on a version of our SDK before 3.4.0, you’ll need to manually handle push notifications. Beginning in 3.4.0, the SDK automatically handles push notifications from Customer.io and tracks opened and delivered metrics for you. We recommend that you upgrade to simplify your code! Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. Beginning in customerio-reactnative version 3.4.0, the SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. No more code is required for your app to track opened push metrics or launch deep links!  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Read the sections below to see how you can add (optional) custom handling to various push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, add the following highlighted line of code in your MyAppPushNotificationsHandler class that you created as a part of our push notification setup instructions: @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { ... MessagingPushAPN.initialize { config in config.showPushAppInForeground = true // `true` will display the push when app in foreground } } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually record push metrics using Javascript methods 🎉Updated in version 3.1.0  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking After you set up push notifications, update AppDelegate.mm to disable automatic push notification tracking: @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackPushEvents = false } } --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/react-native/3.x/in-app-messages/set-up-in-app/ This page describes how to implement mobile in-app messages. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/react-native/getting-started/#install" click B href "/integrations/sdk/react-native/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/react-native/identify" click track-events href "/integrations/sdk/react-native/track-events/" click register-token href "/integrations/sdk/react-native/push" click push href "/integrations/sdk/react-native/push" click rich-push href "/integrations/sdk/react-native/rich-push" click in-app href "/integrations/sdk/react-native/in-app" click test-support href "/integrations/sdk/react-native/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set enableInApp to true in your CustomerioConfig(), and your app will be able to receive in-app messages. const data = new CustomerioConfig() data.enableInApp = true Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/react-native/3.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging().registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging().dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## Migrate from an earlier version URL: https://docs.customer.io/integrations/sdk/react-native/3.x/updates-and-troubleshooting/migrate-upgrade/ This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features. Versioning We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively. Major: may include breaking changes, and generally introduces significant feature updates. Minor: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app. Patch: Increments represent minor fixes that should not require development effort. Upgrade from 2.x to 3.x Installing and updating our React Native SDK got easier. After you install the CustomerIO React Native SDK version 3.x, open your ios/Podfile and follow all 5 steps shown in this code block below: # 1. This line is required by the FCM SDK. If you encounter problems during 'pod install', add this line to your Podfile and try 'pod install' again. use_frameworks! :linkage => :static target 'YourApp' do # Note: 'YourApp' is unique to your app. This is here for example purposes, only. # 2. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me # 3. Add one of these new lines below: # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' end target 'NotificationServiceExtension' do # 4. Remove all 'pod CustomerIO...' lines (such as the example below). pod 'CustomerIO/MessagingPushAPN', '~> 2' # Remove me pod 'FirebaseMessaging' # Remove me, unless you need to specify a specific version pod 'Firebase' # Remove me, unless you need to specify a specific version. # 5. Add one of these new lines below: # ⚠️ Important: Notice these lines of code include "-richpush" in it making it unique to the host app target above. # If you use APN for your push notifications on iOS, install the APN pod: pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' # If you use FCM for your push notifications on iOS, install the FCM pod: pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' end After you modify your Podfile, run the command pod update --repo-update --project-directory=ios to make your changes to ios/Podfile go into effect. Upgrade from 1.x to 2.x Rich push initialization(iOS) If you followed our docs to setup rich push in your app, you should have a Notification Service Extension file in your code base. Due to the behavior of Notification Service Extensions in iOS, you need to initialize the Customer.io SDK in your Notification Service Extension. In the case that you use Objective-C, you must add the code snippet below into the Swift handler file that you created in NotificationService Extension. class NotificationService: UNNotificationServiceExtension { override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { // Make sure to initialize the SDK at the top of this function. CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US) { config in config.autoTrackPushEvents = true } ... } } See our docs for rich push to learn more about rich push setup, SDK initialization, and SDK configuration. Firebase users must manually install Firebase dependencies We removed all Firebase SDKs as dependencies from the CustomerIO/MessagingPushFCM Cocoapod. If you send messages to your iOS app using FCM, you’ll need to install the Firebase Cloud Messaging (FCM) dependencies in your Podfile on your own. pod 'Firebase' pod 'FirebaseMessaging' We fixed a bug in our iOS modules that may impact your data SDK functions that let you send custom data—trackEvent, screen, identify and deviceAttribute calls—may have been impacted by a bug in our iOS v1 modules that converted keys in your custom data to snake_case. This bug is fixed in v2 of the SDK. You will see your data in Customer.io exactly as you pass it to the SDK. This bug didn’t surface with all data; it did not affect you if you already snake-cased your data; and it did not affect your Android users.. // If you passed in custom attributes using camelCase keys: data = {"firstName": "Dana"} // The SDK v1 may have converted this data into: data = {"first_name": "Dana"} // Or, if you used a different format that was not snake_case: data = {"FIRSTNAME": "Dana"} // The SDK v1 may have converted this data into: data = {"f_irstname": "Dana"} You don’t need to do anything before you update. But we strongly recommend that you go to Data Index and audit your attributes and events to determine if the v1 SDK reshaped your data. Make sure that updating to the 2.x SDK won’t impact your segments, campaigns, etc by sending data in a different (but expected) format to Customer.io. If your data was affected, you can either: (Recommended) Update your attributes, segments, and other information stored in Customer.io to use your original data format. Set your app to continue using the snake-cased data passed by the 1.x SDK. Option 1 (Recommended): Update your data in Customer.io For Events: trackEvent and screen calls Unfortunately, you can’t modify past events sent by trackEvent or screen calls. But, before you move forward with the 2.0 SDK, you can can update your segments, campaigns, and other Customer.io assets to use your original, not-reshaped data format. For segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., you should use OR conditions with the bugged, snake-cased format and your preferred data format. This ensures that people enter your segments and campaigns whether they use your app with the 1.x or 2.x SDKs. For Attributes: identify, profileAttributes, and deviceAttribute calls If your customer data was inappropriately snake-cased by the v1 SDK, you can set up a campaign to apply correctly formatted attributes in Customer.io so you don’t need to update your app! If you update your data this way, you may still need to update segments and other assets to use the correct data shape. Create a segment of people possessing the affected, snake-cased attributes. Create a campaign using this segment as a trigger. In the workflow, add two a Create or Update Person actions. Configure the first action to set correctly formatted attributes using the values from your previously-misshaped attributes. Use liquid to identify the attributes in question. Use a liquid or JS if statement to set an attribute value if it exists, otherwise your campaign may experience errors. {% if customer.snake_case %}{{customer.snake_case}}{% endif %} Configure the second Create or Update Person action to remove the bugged, snake-case attributes from your audience. Make sure that your segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., filters, and other items that might be based on people’s attributes or device attributes are all set to use your preferred format. Option 2: Use snake-cased formats in your app // Call the Customer.io SDK and provide custom attributes like this: CustomerIO.identify("dana@example.com", {"first_name": "Dana"}) // Consider sending duplicate data with snake_case CustomerIO.identify("dana@example.com", { "firstName": "Dana", // Attribute used with v1 of the SDK that got converted to snake_case. Keeping it here as the bug has been fixed. "first_name": "Dana" // Adding this duplicate attribute for backwards compatibility with customers using old versions of your app. }) Then, after you have determined that all of your app’s customers have updated their app to a version of your app no longer using v1 of the Customer.io SDK, you can remove this duplication: CustomerIO.identify("dana@example.com", { "firstName": "Dana" // We can remove the snake_case attribute and go back to just camelCase! }) --- ## Update to version 3.4 URL: https://docs.customer.io/integrations/sdk/react-native/3.x/updates-and-troubleshooting/update-to-3.4/ This page explains how to update your SDK install to latest versions that may not require a breaking change. While these changes aren't breaking—you don't _need_ to make these changes—they will simplify your integration, improve the reliability of your metrics, and improve deep link handling on iOS devices. Upgrade from 3.3 to 3.4+ As of version 3.4, the Customer.io SDK automatically registers push device tokens to identified people and handles push clicks. These features simplify your SDK integration while improving compatibility with apps that use multiple push SDKs. After you install a version of the SDK that is 3.4 or higher, follow these steps to upgrade.  Do you have a swift app? Skip ahead! If you’ve got a Swift app containing the AppDelegate.swift file, ignore the steps below and go to the Swift upgrade section. Open your push notification handler file (In our examples, we call this file MyAppPushNotificationsHandler.swift) and review all of the highlighted code below. We’ve highlighted the most relevant lines. import Foundation import CioMessagingPushAPN import UserNotifications // Delete this line import CioTracking @objc public class MyAppPushNotificationsHandler : NSObject { public override init() {} // Replace these 2 lines @objc(setupCustomerIOClickHandling:) public func setupCustomerIOClickHandling(withNotificationDelegate notificationDelegate: UNUserNotificationCenterDelegate) { // With these 2 lines @objc(setupCustomerIOClickHandling) public func setupCustomerIOClickHandling() { // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: siteId, apiKey: apiKey, region: Region.US) { config in config.autoTrackDeviceAttributes = true } // Delete these 2 lines: let center = UNUserNotificationCenter.current() center.delegate = notificationDelegate } // Delete this function: @objc(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:) public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } } } Open your AppDelegate.h file and review all of the highlighted code below. APN APN #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate @end FCM FCM #import <RCTAppDelegate.h> #import <UIKit/UIKit.h> #import <FirebaseMessaging/FIRMessaging.h> #import <UserNotifications/UserNotifications.h> // Delete this line // Remove `UNUserNotificationCenterDelegate` from this line: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate, UNUserNotificationCenterDelegate> // After this change, the line will look like this: @interface AppDelegate: RCTAppDelegate<FIRMessagingDelegate> @end Open your AppDelegate.m file and review all of the highlighted code below. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... // Replace this line [pnHandlerObj setupCustomerIOClickHandling:self]; // With this line: [pnHandlerObj setupCustomerIOClickHandling]; return YES; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { // Remove the line below: [pnHandlerObj userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } Now that your app’s code has been simplified, follow the latest push notification setup documentation to enable these new features. Upgrade from 3.3 to 3.4+, for Swift Open your AppDelegate.swift file and review all of the highlighted code below. We’ve highlighted the most relevant lines. import CioTracking import CioMessagingPushAPN class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.US, configure: nil) // Delete this line UIApplication.shared.registerForRemoteNotifications() return true } } // Delete this function func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MessagingPush.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } // Delete this function func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/react-native/3.x/updates-and-troubleshooting/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Try running CIO SDK Tools Our CIO SDK Tools library can help diagnose problems with your SDK implementation. This is a node package that you can run from inside or outside your app’s project folder. After you install it, you can run the doctor command to check your SDK configuration and get tips to fix problems. npx cio-sdk-tools@latest doctor /path/to/project Capture logs Logs help us pinpoint the problem and find a solution. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import { CustomerIO, CustomerioConfig, CustomerIOEnv, Region } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug CustomerIO.initialize(env, data) ; Build and run your app on a physical device or emulator. In the console, run: react-native log-ios react-native log-android Export your log to a text file and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Try updating iOS package dependencies This SDK uses our iOS push package. In some cases, we may make fixes in our iOS packages that fix downstream issues in, or expose new features to this SDK. You can update the version in your podfile and then run the following command to get the latest iOS packages. Our instructions above list out the full version of the iOS push package. If you want to automatically increment packages, you can remove the patch and minor build numbers (the second and third parts of the version number), and pod update will automatically fetch the latest package versions. However, please understand that fetching the latest versions can cause build issues if the latest iOS package doesn’t agree with code in your app! pod update --repo-update --project-directory=ios Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Error: Initializer element is not a compile-time constant If you get this error while initializing the object in AppDelegate, go to your project’s ios directory, open AppDelegate.m and update the object of your amiappPushNotificationsHandler—or however you named your push handler—with the following code. @implementation AppDelegate PushNotificationsHandler* pnHandlerObj = nil; + (void)initialize { pnHandlerObj = [[amiappPushNotificationsHandler alloc] init]; } Deep linking to iOS when your app is killed There’s a known issue preventing deep links from working when your app is killed on iOS devices. When the app is in the killed state, the native click event is fired before the react app’s lifecycle starts. We recommend a workaround: Update didFinishLaunchingWithOptions in your AppDelegate.m file with the code below. We use the variable modifiedLaunchOptions to create a bridge object in the last line of this code, which grabs the link and sends users to the specified screen. NSMutableDictionary *modifiedLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *pushContent = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (pushContent[@"CIO"] && pushContent[@"CIO"][@"push"] && pushContent[@"CIO"][@"push"][@"link"]) { NSString *initialURL = pushContent[@"CIO"][@"push"][@"link"]; if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { modifiedLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; } } } // Replace launchOptions with modifiedLaunchOptions RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:modifiedLaunchOptions]; ... // Use the bridge object in RCTAppSetupDefaultRootView UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"SampleApp", initProps, true); Compiler error: ‘X’ is unavailable in application extensions for iOS This error occasionally occurs when users add a notification extension to handle rich push messages. If you see this error, try the following steps: Add this code to the end of your Podfile: post_install do |installer| installer.pods_project.targets.each do |target| if target.name.start_with?('CustomerIO') puts "Modifying target #{target.name} with workaround" target.build_configurations.each do |config| puts "Setting build config settings for #{target.name}" config.build_settings['APPLICATION_EXTENSION_API_ONLY'] ||= 'NO' end end end end In the root directory of your app, run pod install --project-directory=ios. This command will apply the above workaround to your project. Try to compile your app again. If you still see the error message, it’s likely that the error you see is related to a different SDK that you use in your app and not the Customer.io SDK. We suggest that you contact the developers of the SDK that you see in the error message for help. If you don’t see an error message, send our technical support team a message with: The error message that you see when compiling your app. The contents of your ios/Podfile and ios/Podfile.lock files. The version of the React Native SDK that you are using. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/react-native/3.x/updates-and-troubleshooting/changelog/ Check out release history our React Native SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. test --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/expo/quick-start-guide/ Expo provides a managed framework to help you build React Native mobile apps. Our Expo plugin relies on our React Native SDK. When you run the prebuild, we'll generate the native files you'll need to integrate Customer.io push notifications in your app. Then you'll add calls to your app to `identify` people, `track` their activity, and listen for notifications.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview Expo provides a managed framework to help you build React Native mobile apps. Our Expo plugin relies on our React Native SDK. When you run the prebuild, we’ll generate the native files you’ll need to integrate Customer.io push notifications in your app. Then you’ll add calls to your app to identify people, track their activity, and listen for notifications. Install the SDK. Identify and Track Push Notifications In-App 1. Install the SDK If you haven’t already gotten a CDP API key, you’ll need to add a new Expo integration to your Customer.io workspace. This “integration” represents your app in Customer.io and provides you the CDP API key that you’ll use to initialize your app. See Get your CDP API key for details. Install the Customer.io Expo plugin and React Native SDK. npx expo install customerio-expo-plugin customerio-reactnative Configure your CDP API key and site ID. CDP API Key: You’ll find this key in the Expo integration that you created in step 1 of this section. Site ID: You’ll find this value in your integrations under Customer.io API: Track. Initialize React Native SDK Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) Note: Auto-initialization is supported in Expo 53+ only. For older Expo versions, use manual initialization. Add React Native SDK configuration to your app.json file: { "expo": { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP API KEY>", "siteId": "<SITE ID>", // Optionally, you can enable in-app messaging by adding site ID "region": "us" // use "eu" if your workspace is in EU region } } ] ] } } See Configuration Options for more details. Manual initialization Manual initialization Add your CDP API key and site ID to your .env file. EXPO_PUBLIC_CDP_API_KEY=<CDP API KEY> EXPO_PUBLIC_SITE_ID=<SITE ID> Ensure the SDK is initialized in your app. You can call the initialize method from your components or services. We recommend initializing the SDK in the useEffect hook of your main component or layout file. import { CioConfig, CustomerIO } from "customerio-reactnative"; useEffect(() => { const initializeCustomerIO = () => { const config: CioConfig = { cdpApiKey: process.env.EXPO_PUBLIC_CDP_API_KEY, region: "us", // use "eu" if your workspace is in EU region // Optionally, you can enable in-app messaging by adding the site ID inApp: { siteId: process.env.EXPO_PUBLIC_SITE_ID, } }; CustomerIO.initialize(config); }; initializeCustomerIO(); }, []); You must also add push notification options to your app.json file to build the native files, even if you don’t use Customer.io push notifications in your app. See Push Notifications for more information, and then run the prebuild command. npx expo prebuild Run your application using npx expo run:ios or npx expo run:android instead of using the dev server via npx expo start. You need to use these commands so that the plugin can build native files that let your app receive push notifications and in-app messages. 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. import { CustomerIO } from "customerio-reactnative"; const identifyUserExample = () => { CustomerIO.identify({ userId: 'expo-test-user@example.com', traits: { firstName: 'John', lastName: 'Doe', email: 'expo-test-user@example.com', subscriptionStatus: 'active', }, }); console.log('User identified successfully'); }; Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. import { CustomerIO } from "customerio-reactnative"; const trackCustomEventExample = () => { CustomerIO.track('purchased_item', { product: 'Premium Subscription', price: 99.99, currency: 'USD' }); console.log('Custom event tracked successfully'); }; Track screen views to automatically trigger in-app messages associated with specific screens. import { CustomerIO } from "customerio-reactnative"; const trackScreenViewExample = () => { CustomerIO.screen('ProductDetails'); console.log('Screen view tracked successfully'); }; 3. Push Notifications Configure push notifications for iOS and Android. Update your app.json file with the following configuration: Auto-initialization Auto-initialization { "expo": { ...Other options "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP API KEY>", "region": "us" // us or eu }, "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "pushNotification": { "useRichPush": true } } } ] ] } } Manual initialization Manual initialization { "expo": { ...Other options "plugins": [ [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "pushNotification": { "useRichPush": true, "env": { "cdpApiKey": "<CDP API KEY>", "region": "us" // us or eu } } } } ] ] } } Upload your push service credentials in Customer.io under Settings > Workspace Settings > Push. iOS: upload your Apple Push Notification certificate (.p8 file). Android: upload your server key json file (.json format). See Push Notifications for detailed configuration options. 4. In-App To enable in-app messaging, all you need to do is add the site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app: Auto-initialization Auto-initialization Add siteId to your app.json config: { "expo": { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP API KEY>", "siteId": "<SITE ID>", "region": "us" } } ] ] } } Manual initialization Manual initialization Add siteId to your environment variables and initialization config. You can call the initialize method from your components or services: EXPO_PUBLIC_SITE_ID=<SITE ID> import { CioConfig, CustomerIO } from "customerio-reactnative"; import { useEffect } from "react"; useEffect(() => { const initializeCustomerIO = () => { const config: CioConfig = { cdpApiKey: process.env.EXPO_PUBLIC_CDP_API_KEY, inApp: { siteId: process.env.EXPO_PUBLIC_SITE_ID, } }; CustomerIO.initialize(config); }; initializeCustomerIO(); }, []); See In-App Messages for more details. --- ## How it works URL: https://docs.customer.io/integrations/sdk/expo/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum requirements To support the Customer.io SDK, you must: Use a version of Expo from 45 to (and including) 53. Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/expo/getting-started/auth/ To use the SDK, you'll need two kinds of API keys: A *CDP API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from. These keys come from different places in Customer.io! CDP API Key: You’ll get this key when you set up your mobile app as a data-in integration in Customer.io. Site ID: This key tells the SDK which workspace your in-app messages come from. You’ll use it to support inApp messages. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the migrationSiteId. Get your CDP API Key You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your Expo entry under Integrations. If you don’t see your app on this page, you’ll need to add a new integration. Go to Integrations. Go to the Connections tab and find the entry for your Expo app. If you don’t see Expo in the list of connections, you’ll need to add a new integration. Go to Settings and find your API Key. Copy this key and use it in your app configuration. Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) Add the key to the config object in your app.json file: { "expo": { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP_API_KEY>", "region": "us", // use "eu" if in EU region "siteId": "<SITE_ID>", // Required for in-app messaging "migrationSiteId": "<OLD_SITE_ID>" // Required if migrating from an earlier version }, "android": { "googleServicesFile": "./files/google-services.json", "setHighPriorityPushHandler": true }, "ios": { "pushNotification": { "useRichPush": true } } } ] ] } } Manual initialization Manual initialization Add the key to the customerio-expo-plugin object in your app.json file and your app’s code: In app.json: { "expo": { "plugins": [ [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json", "setHighPriorityPushHandler": true }, "ios": { "pushNotification": { "useRichPush": true, "env": { "cdpApiKey": "<CDP_API_KEY>", "region": "us" } } } } ] ] } } In your app’s code: import { CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, inApp: { siteId: '<your_site_id>', }, }; CustomerIO.initialize(config); }, []); }; Add a new integration If you don’t already have a write key, you’ll need to set up a new connectionRepresents an integration between Customer.io and another service or app under Data & Integrations > Integrations. A connection in Customer.io provides you with API keys and settings for your integration.. The connection represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click the Directory tab. Find and select the Expo integration. Enter a Name for the integration, like “My Expo App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. You’ll use it to initialize the SDK in your Expo app. (Optional) Set up your Expo app and click Test Connection. If you tested your connection first, click Complete Setup. Otherwise, click Save & Complete Later. Get your Site ID You’ll use your site ID with the inApp option to support in-app messaging. And if you’re upgrading from a previous version of the SDK, you’ll also use your site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to Settings > Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. Use this Site ID to enable in-app messaging: Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) Add the Site ID to your app.json configuration: { "expo": { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP_API_KEY>", "region": "us", "siteId": "<SITE_ID>" } } ] ] } } Manual initialization Manual initialization Use the Site ID in your app’s code to initialize the inApp package: import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, }; CustomerIO.initialize(config) }, []) } Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Packages and Configuration Options URL: https://docs.customer.io/integrations/sdk/expo/getting-started/packages-options/ The SDK consists of a few packages. You *must* use the `CioConfig` and `CustomerIO` packages to configure and initialize the React Native SDK. How it works With our Expo plugin, you can configure the SDK in two ways: Auto-initialization (Expo 53+): Configure everything in your app.json file using the config object. The SDK will automatically initialize without requiring CustomerIO.initialize() in your app code. Manual initialization: Configure native settings in app.json and SDK runtime settings in your React Native app code using CustomerIO.initialize(). Both approaches require configuration in your app.json or app.config.js files that affect the native files we generate when you run the prebuild. Configuring the Expo Plugin To use the Expo plugin, you’ll need to add a customerio-expo-plugin entry to your app.json or app.config.js files. Before you set up your app: If you’re in our EU region, set the region to eu. To support Customer.io’s latest features, you’ll also want to set your iOS deployment target to 15.1 or higher. If you send notifications to iOS users through Firebase Cloud Messaging, you’ll need to: Set expo-build-properties.ios.useFrameworks to static. Set customerio-expo-plugin.ios.pushNotification.provider to fcm. Set customerio-expo-plugin.ios.pushNotification.googleServicesFile to the location of your GoogleService-Info.plist file. If you use @react-native-firebase with the Customer.io SDK, you should not set this value Learn more Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) { "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.1", // set your iOS deployment target to support Customer.io "useFrameworks": "static" // set if you use FCM to send push notifications to iOS devices } } ], [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<cdpApiKey>", "siteId": "<siteId>", "region": "us" }, "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "useFrameworks": "static", // must match value in expo-build-properties if set "pushNotification": { "provider": "fcm", // use "apn" for Apple Push Notification service "googleServicesFile": "<Path to GoogleService-Info.plist file>", // required if provider is "fcm" and you don't use the @react-native-firebase SDK "useRichPush": true, "appGroupId": "group.com.example.myapp.cio" } } } ] ] } Manual initialization Manual initialization { "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.1", // set your iOS deployment target to support Customer.io "useFrameworks": "static" // set if you use FCM to send push notifications to iOS devices } } ], [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "useFrameworks": "static", // must match value in expo-build-properties if set "pushNotification": { "provider": "fcm", // use "apn" for Apple Push Notification service "googleServicesFile": "<Path to GoogleService-Info.plist file>", // required if provider is "fcm" and you don't use the @react-native-firebase SDK "useRichPush": true, "appGroupId": "group.com.example.myapp.cio", "env": { "cdpApiKey": "<cdpApiKey>", "region": "us" } } } } ] ] } Auto-initialization configuration options (Expo 53+) When using auto-initialization, include a config object in your plugin configuration: Option Type Default Description config object Container for auto-initialization settings. If present, enables auto-initialization. config.cdpApiKey string Required: Your CDP API key config.siteId string Optional: Your site ID for in-app messaging. Required to enable in-app messaging. config.region string US Workspace region: "US" or "EU" config.logLevel string debug Log level: "none", "error", "info", or "debug" config.autoTrackDeviceAttributes boolean true Automatically track device attributes config.trackApplicationLifecycleEvents boolean true Track app lifecycle events config.screenViewUse string all Screen view tracking: "all" (server + in-app) or "inapp" (in-app only) config.migrationSiteId string Previously used site id, required if migrating from earlier versions (1.X) config.location object Enable location tracking. Takes a trackingMode option: MANUAL (default), ON_APP_START, or OFF. Expo plugin configuration options Option Type Default Description android object Required if you want to setup Android even if it is empty. Eg ("android": {}). android.googleServicesFile string Set the path to your google-services.json file. android.setHighPriorityPushHandler boolean This is optional, if you choose to use a 3rd party plugin to handle notification permissions, but want our SDK to handle the notifications. android.pushNotification.channel.id string [your package name] (Optional) Android messaging notification channel ID android.pushNotification.channel.name string [your app name] Notifications (Optional) Android messaging notification channel name android.pushNotification.channel.importance integer 3 (Optional) The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level ios object Required: The parent object for iOS settings, including your initialization keys. ios.useFrameworks string (Optional) Allows the plugin to work with static libraries. Options are static and dynamic ios.pushNotification object Required Configurations for push notifications for iOS. ios.pushNotification.env object Required for manual initialization only Contains your API key (pushNotification.env.cdpApiKey) and region (pushNotification.env.region with one of eu or us) information. Optional when using auto-initialization with config object. ios.pushNotification.env.cdpApiKey string Required: the key to use for authentication ios.pushNotification.env.region us or eu us The region your Customer.io account is in. If you use the wrong region, your mobile traffic won’t make it into Customer.io. ios.pushNotification.autoFetchDeviceToken boolean true When true, the SDK automatically gets the device token. But, if you get tokens through another provider and register them with the SDK using the CustomerIO.registerDeviceToken method, you should set this to false. Learn more ios.pushNotification.provider string apn Use apn for Apple Push Notification service or fcm for Firebase Cloud Messaging. ios.pushNotification.googleServicesFile string Required if ios.pushNotification.provider is fcm. Set the path to your GoogleService-Info.plist file. Do not set this value if you use the @react-native-firebase SDK. Learn more ios.pushNotification.useRichPush boolean false Enables rich push features for iOS (images and links). ios.pushNotification.disableNotificationRegistration boolean false (Optional) Removes the registerPushNotification handler and allows you to control notification permission requests. We recommend that you set this to true if you have customerio-reactnative version >= 2.2.0 installed. ios.pushNotification.showPushAppInForeground boolean true Set to true if you want push notifications sent by Customer.io to be shown when your app is in the foreground. ios.pushNotification.handleDeeplinkInKilledState boolean false Set to true if you want the Customer.io SDK to handle deep links when your app is in a killed/closed state. ios.pushNotification.appGroupId string (Optional) Your App Group identifier for reliable push delivery tracking. Must start with group.. When set, the plugin automatically configures App Group entitlements and passes the identifier to the SDK. Requires useRichPush: true. location object Container for location tracking plugin settings. location.enabled boolean false When true, the plugin adds the native location dependencies (iOS Podfile subspec and Android Gradle properties) during prebuild. Your app must add its own location permissions and privacy usage descriptions. SDK packages The SDK consists of a few packages. You must use the CioConfig and CustomerIO packages to configure and initialize the SDK in your React Native app. Package Product Required? Description CustomerIO ✅ The main SDK package. Used to initialize the SDK and call the SDK’s methods. CioConfig ✅ Configure the SDK including in-app messaging support. CioRegion Used inside the CioConfig.region option to declare your region—EU or US. CioLogLevel Used inside the CioConfig.logLevel option to set the level of logs you can view from the SDK. CioLocationTrackingMode Used inside CioConfig.location to set the tracking mode. See location tracking for details. React configuration options You can determine global behaviors for the SDK in using CioConfig package. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. Note that the logLevel option requires the CioLogLevel package and the region option requires the CioRegion package. import { CioLogLevel, CioRegion, CustomerIO, CioConfig, PushClickBehaviorAndroid, } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', }, push: { android: { pushClickBehavior: PushClickBehaviorAndroid.ActivityPreventRestart } } }; CustomerIO.initialize(config) }, []) } Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region CioRegion.EU or CioRegion.US CioRegion.US Requires the CioRegion package. You must set the region your account is in the EU (CioRegion.EU). apiHost string The domain you’ll proxy requests through. You’ll only need to set this (and cdnHost) if you’re proxying requests. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc cdnHost string The domain you’ll fetch configuration settings from. You’ll only need to set this (and apiHost) if you’re proxying requests. logLevel string error Requires the CioLogLevel package. Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. migrationSiteId string Required if you're updating from 3.x: the credential for previous versions of the SDK. This key lets the SDK send remaining tasks to Customer.io when your audience updates your app. screenViewUse All or InApp All ScreenView.All (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don't want the app to send lifecycle events inApp object Required for in-app support. This object takes a siteId property, determining the workspace your in-app messages come from. push object Takes a single option called PushClickBehaviorAndroid. This object and option controls how your app behaves when your Android audience taps push notifications. location object Enable location tracking. Takes a trackingMode option from the CioLocationTrackingMode package. Proxying requests By default, requests go through our domain at cdp.customer.io. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores. To proxy requests, you’ll need to set the apiHost and cdnHost properties in your SDKConfigBuilder. While these are separate settings, you should set them to the same URL. While you need to initialize the SDK with a cdpApiKey, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the cdpApiKey to a value representing basic authentication credentials that you handle on your own. See proxying requests for more information. import { CioConfig, CustomerIO, CioRegion } from "customerio-reactnative"; useEffect(() => { const config: CioConfig = { cdpApiKey: process.env.EXPO_PUBLIC_CDP_API_KEY, region: CioRegion.US, apiHost: "proxy.example.com", cdnHost: "proxy.example.com", inApp: { siteId: process.env.EXPO_PUBLIC_SITE_ID, } }; CustomerIO.initialize(config); }, []); --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/expo/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Expo plugin. Before you contact support, you might want to [update your iOS dependencies](/Page(/integrations/sdk/expo/push/#update-ios-dependencies) to get the latest packages and see if that fixes the issue. You can also check out our latest iOS changes to see if we’ve already fixed the issue or check out open issues to see if you’re experiencing a known issue. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. Deep linking to iOS when your app is killed There’s a known issue preventing deep links from working when your app is killed on iOS devices. When the app is in the killed state, the native click event is fired before the react app’s lifecycle starts. We recommend setting our Expo config ios.pushNotification.handleDeeplinkInKilledState to true to workaround the issue: { "plugins": [ [ "customerio-expo-plugin", { ...other config "ios": { ...other config "pushNotification": { ...other config "handleDeeplinkInKilledState": true, ...other config } } } ] ] } Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/expo/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person import { CustomerIO } from "customerio-reactnative"; // Call this method whenever you are ready to identify a user CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.setProfileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const profileAttributes = { favouriteFood: "Pizza", favouriteDrink: "Mango Shake" }; CustomerIO.setProfileAttributes(profileAttributes) Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zones, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. If you want an attribute to persist beyond the life of the device token, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify() Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify( userId: "new.person@example.com", traits: { first_name: "New", last_name: "Person" })  --- ## Track events URL: https://docs.customer.io/integrations/sdk/expo/tracking/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. import { CustomerIO } from "customerio-reactnative"; CustomerIO.track("event_name", { propertyName: propertyValue });  Perform downstream actions with semantic events Some downstream actions don’t neatly map to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See Semantic Events for more information. Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.track("User Deleted") --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/expo/tracking/screen-events/ Screen events track the screens people view in your app. Beyond tracking the parts of your app people use, screen tracking is vital for in-app messages because they target specific screens. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking The example below shows one way to implement automatic screen tracking using Expo Router. Use this as inspiration—every app can have its own navigation patterns and screen tracking requirements. Not using Expo Router? If your Expo project renders its own NavigationContainer with React Navigation, checkout our React Native screen-tracking example for ideas you can adapt to your own implementation. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. Requirements and limitations This approach requires: Expo Router: This example only works with apps using Expo Router (not React Navigation or other navigation libraries). Learn more in the Expo Router documentation. Expo SDK 49+: The usePathname and useGlobalSearchParams hooks require recent Expo versions. Check the Expo SDK upgrade guide for migration details. File-based routing: Your app must use Expo Router’s file-based routing structure. See the file-based routing guide for setup instructions.  This example works well with standard Expo Router setups. Since every app has unique navigation patterns and configurations, we recommend testing this implementation in your development environment first. You may need to adjust the code based on your specific Expo workflow (managed vs. bare) or custom routing needs. For navigation-specific questions or customizations, the Expo Router documentation and community forums are great resources. With Expo Router, screen tracking is straightforward because it always has access to a URL. Create a higher-order component that observes the currently selected URL and tracks it in your analytics provider. import { useEffect } from 'react'; import { usePathname, useGlobalSearchParams, Slot } from 'expo-router'; import { CustomerIO } from 'customerio-reactnative'; export default function Layout() { const pathname = usePathname(); const params = useGlobalSearchParams(); // Track the location in your analytics provider useEffect(() => { CustomerIO.screen(pathname, params); }, [pathname, params]); // Export all the children routes return <Slot />; } Place this code in your app/_layout.tsx file to track screen changes throughout your app. Test thoroughly in your development environment before deploying. Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.InApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory region: CioRegion.US, screenViewUse: ScreenView.All trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/expo/tracking/lifecycle-events/ By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your source data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed when your app crashes or Application Updated when people update your app. CustomerIO.track("Application Crashed", { url: "/page/in/app" }); Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in the CioConfig object when you initialize the SDK. import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, trackApplicationLifecycleEvents: false, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration } }; CustomerIO.initialize(config) --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/expo/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their screen view. This triggers the campaign you set up for people who visited the product page. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Location tracking URL: https://docs.customer.io/integrations/sdk/expo/tracking/location/ Real-time location tracking lets you update a person's profile with accurate coordinates so you can send geo-aware messages and segment users by location. How it works The Location module captures location (with user consent) from your app and attaches it to a person’s profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than IP-based geolocation. When you identify a person, the SDK includes the latest location in the identify call. The SDK also sends a Location Update event to the person’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update. The SDK does not request location permission on its own—your app must handle the permission flow. Enable the location module Add location.enabled to your customerio-expo-plugin configuration in app.json or app.config.js. When you run the prebuild, the plugin automatically adds the required native dependencies on both iOS and Android—you don’t need to edit your Podfile or build.gradle manually. { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<cdpApiKey>" }, "android": {}, "ios": { "pushNotification": { "provider": "apn", "useRichPush": true } }, "location": { "enabled": true } } ] ] } After updating your configuration, run npx expo prebuild to regenerate the native project files with location support. Configure the tracking mode Add a location object to your config (for auto-initialization) or CioConfig (for manual initialization) to control how and when the SDK captures location. Option Type Default Description trackingMode LocationTrackingMode MANUAL Controls how and when the SDK captures location. See tracking modes below. Tracking modes Mode Description MANUAL Your app controls when it captures location. Call setLastKnownLocation() or requestLocationUpdate() to provide location. Use this when your app already has a location-tracking mechanism or you want full control over when you capture location data. ON_APP_START The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call setLastKnownLocation() or requestLocationUpdate() alongside automatic capture. Use this for hands-off location tracking with minimal battery impact. OFF Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime. Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) Set the tracking mode in the config.location object in your app.json: { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<cdpApiKey>", "location": { "trackingMode": "MANUAL" } }, "android": {}, "ios": { "pushNotification": { "provider": "apn", "useRichPush": true } }, "location": { "enabled": true } } ] ] } Manual initialization Manual initialization Import CioLocationTrackingMode and set the tracking mode in your CioConfig: import { CustomerIO, CioConfig, CioLocationTrackingMode } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'your-cdp-api-key', // ...other config options location: { trackingMode: CioLocationTrackingMode.Manual, }, }; CustomerIO.initialize(config); Location APIs The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a Location Update event no more than once a day—and only if the person has moved a meaningful distance since the last update. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates. setLastKnownLocation Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io. Parameter Type Description latitude number Latitude in degrees. Must be between -90 and 90. longitude number Longitude in degrees. Must be between -180 and 180. import { CustomerIO } from 'customerio-reactnative'; // Pass coordinates from your app's location provider CustomerIO.location.setLastKnownLocation(37.7749, -122.4194); requestLocationUpdate Request a one-shot location from the native platform’s location services. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user. If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes. Add location permissions to your app.json: { "expo": { "ios": { "infoPlist": { "NSLocationWhenInUseUsageDescription": "We use your location to personalize your experience." } }, "android": { "permissions": [ "ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION" ] } } } After your app requests and receives permission at runtime, call the SDK: import { CustomerIO } from 'customerio-reactnative'; // After location permission is granted CustomerIO.location.requestLocationUpdate();  We recommend using a library like expo-location to handle permission requests in your Expo app. Profile switch behavior When you call CustomerIO.clearIdentify(), the SDK clears cached location data so that one person’s location doesn’t carry over to another person’s profile. The next person you identify starts with a clean slate. Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next identify() call includes it automatically. --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/push/ The Expo plugin supports push notifications over APN for iOS and FCM for Android. Use this page to get started with push notification services. How it works When you use our Expo plugin, you’re already set up to support push notifications, including images and deep links. Before a device can receive a push notification, you must: Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. (Optional) Set up your app to report push metrics back to Customer.io. Before you begin You need to enable Customer.io to send push notifications through your preferred service: Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM) before you can send push notifications through Customer.io.  We don’t support Expo’s Push Service You can’t use push tokens obtained through Expo’s Push Service to send push notifications through Customer.io. If your app previously used Expo’s push service, you’ll have to obtain new FCM or APN tokens to send push notifications through Customer.io. Luckily, you don’t need to do anything to support your app’s users when you implement our SDK! If your app can obtain Expo push tokens, it should also be set up to receive APN or FCM device tokens. You’ll simply configure your app to fetch a token and send that token to Customer.io using CustomerIO.registerDeviceToken("<apn or fcm token here>"). Set up push for Android You don’t need to do anything to support Android devices. When you use our Expo plugin, your Android audience is automatically set up to receive push notifications. Set up push on iOS For the most part, we’ll set up push notifications for you when you run expo prebuild. But before you can support push notifications for iOS devices, you need to add Push Notification capabilities to your project in XCode. Open your React Native project in XCode, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities. Now you’re ready to run the expo prebuild. Update iOS dependencies In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Expo plugin. The command to update these packages depends on whether your Expo project uses a managed or bare workflow. For managed workflows, you can simply re-run the prebuild. Managed workflow Managed workflow expo prebuild --clean # the --clean option is optional Bare workflow Bare workflow pod update --repo-update --project-directory=ios EAS Build with Rich Push If you use EAS Build with useRichPush: true, the Customer.io plugin automatically creates a notification service extension during expo prebuild. You need to tell EAS Build about this extension by adding the appExtensions configuration to your app.config.js.  The appExtensions configuration uses an experimental Expo feature. The configuration structure may change in future Expo versions. export default { expo: { ios: { bundleIdentifier: "com.yourcompany.yourapp" }, extra: { eas: { build: { experimental: { ios: { appExtensions: [ { targetName: "NotificationService", bundleIdentifier: "com.yourcompany.yourapp" + ".richpush", entitlements: { "com.apple.developer.usernotifications.service": true } } ] } } } } } } } Finding the extension bundle identifier: After running expo prebuild, you can find the exact bundle identifier in your ios/<YourAppName>.xcworkspace project. Open the project in Xcode, look for the NotificationService target, and check its bundle identifier in the project settings. It should be your main app’s bundle identifier with .richpush appended (e.g., com.yourcompany.yourapp.richpush). Sound in push iOS push notifications When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display native iOS and Android push notification permission prompts, you’ll use the CustomerIO.pushMessaging.showPromptForPushNotifications method. You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.pushMessaging.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user already responded to the push authorization prompt, the current authorization status is returned as a string. var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Fetch the current device token You can fetch the currently stored device token using the CustomerIO.pushMessaging.getRegisteredDeviceToken() method. This method returns an APN/FCM token in a promise as a string. let token = await CustomerIO.pushMessaging.getRegisteredDeviceToken() if (token) { // Use the token as required in your app for example save in a state setDeviceToken(token); } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS APNs payload iOS APNs payload { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## App Groups for push tracking URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/app-groups/ Configure App Groups for reliable push delivery tracking. App Groups let the SDK recover delivery metrics that iOS might otherwise discard when it terminates the Notification Service Extension. App Groups are required for reliable push delivery tracking on iOS. Without this setup, delivery metrics may be lost if iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers these lost metrics on the next app launch. Before you begin Before you configure App Groups, make sure you’ve completed the following: Set up push notifications with useRichPush: true enabled Created an App Group identifier in the Apple Developer Portal. The identifier must start with group.—for example, group.com.example.myapp.cio. Add the App Group ID to your plugin configuration To enable App Groups, add appGroupId to the ios.pushNotification section of your customerio-expo-plugin configuration. The Expo plugin handles the rest—it automatically: Adds the App Group capability to both your main app target and the Notification Service Extension Configures the entitlements for both targets Passes the App Group identifier to the SDK initialization Auto-initialization (Expo 53+) Auto-initialization (Expo 53+) { "expo": { "plugins": [ [ "customerio-expo-plugin", { "config": { "cdpApiKey": "<CDP_API_KEY>", "region": "us" }, "ios": { "pushNotification": { "useRichPush": true, "appGroupId": "group.com.example.myapp.cio" } } } ] ] } } Manual initialization Manual initialization { "expo": { "plugins": [ [ "customerio-expo-plugin", { "ios": { "pushNotification": { "useRichPush": true, "appGroupId": "group.com.example.myapp.cio", "env": { "cdpApiKey": "<CDP_API_KEY>", "region": "us" } } } } ] ] } } After updating your configuration, rebuild your app: npx expo prebuild --clean  appGroupId requires useRichPush App Groups rely on the Notification Service Extension to track delivery metrics. You must set useRichPush: true for appGroupId to take effect. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Handle deep links in your app There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['myapp://'], config }; Set up deep links for Android To set up deep links in Android, you need to declare your intentFilters in your app.json or app.config.js. { "expo": { ... "android": { ... "intentFilters": [ { "action": "VIEW", "data": [ { "scheme": "<YourScheme>", "host": "<YourHost>" } ], "category": [ "BROWSABLE", "DEFAULT" ] } ] } ... } } Push Click Behavior The android.pushClickBehavior config option controls how your app behaves when your audience taps push notifications. The SDK automatically tracks Opened metrics for all options. { "plugins": [ [ "customerio-expo-plugin", { "android": { "pushClickBehavior": "ActivityPreventRestart" } } ] ] } The available options are: ACTIVITY_PREVENT_RESTART (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ACTIVITY_NO_FLAGS: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. RESET_TASK_STACK: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. Set up deep links for iOS To support iOS, you need to declare your deep link scheme in your Expo project’s app.json or app.config.js file. Apple has a few different ways to set up deep links. Learn more about URL schemes for iOS apps. { "expo": { ... "ios": { ... "infoPlist": { "CFBundleURLTypes": [ { "CFBundleURLSchemes": ["<YourScheme>"] } ] } } ... } }  There’s an issue deep linking into iOS when the app is closed In iOS, deep link click events won’t fire when your app is closed. See our troubleshooting section for a workaround to this issue. --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. How it works Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. By default, our SDK automatically tracks opened and delivered events for push notifications originating from Customer.io, but you can add (optional) custom handling to various push events. You might need a custom handler for push notifications that originate outside of Customer.io or if you want to do something outside our SDK’s scope.  Improve delivery metric reliability Configure App Groups to make sure delivery metrics aren’t lost when iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers any undelivered metrics on the next app launch.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, set the following option in your Expo config file: { ... "plugins": [ ... [ "customerio-expo-plugin", { "ios": { "pushNotification": { "showPushAppInForeground": true, } } } ] ] } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually track push metrics  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking If you have a more advanced use case and want to manually track push metrics, you can disable automatic push tracking with the autoTrackPushEvents option in your Expo config. { ... "plugins": [ ... [ "customerio-expo-plugin", { "ios": { "pushNotification": { "autoTrackPushEvents": false, } } } ] ] } --- ## Android channels URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v2.4 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s app.json file. See Configuring the plugin for more information about these fields. { "plugins": [ [ "expo-build-properties", {} ], [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json", "setHighPriorityPushHandler": true, "pushNotification": { "channel": { "id": "channel_id_value", "name": "Channel Name", "importance": 4 } } }, "ios": {} } ] ] } What channel settings can I change? When you first set up the Customer.io Expo SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io Expo SDK prior to 2.4.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io React Native SDK v4.5.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f --- ## Multiple push providers URL: https://docs.customer.io/integrations/sdk/expo/push-notifications/multiple-push-providers/ By default, our Expo plugin expects to be the only push package in your project. It'll automatically handle push notification metrics and fetch a device tokens for you. But if you use our SDK with other push packages like `react-native-firebase`, you may want to make a couple of configuration changes. Use React Native Firebase with the Customer.io SDK If you use react-native-firebase with the Customer.io SDK, you’ll need to make sure that your app.json or app.config.js file doesn’t include the ios.pushNotification.googleServicesFile option. This ensures that you properly capture metrics for push notifications sent through Customer.io and any notifications you receive using react-native-firebase. { "expo": { "ios": { // include plist file here "googleServicesFile": "./GoogleService-Info.plist" }, "plugins": [ [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "pushNotification": { "provider": "fcm", // don't include your plist file here } } } ] ] } } Capture push tokens from another push provider If you use another push package like expo-notifications, and you want to utilize that package to get device tokens, you’ll need to update your app.json or app.config.js file to disable automatic device token fetching and make sure you register push tokens with Customer.io using the CustomerIO.registerDeviceToken(<token>) method. In your app.json or app.config.js file, set the ios.pushNotification.autoFetchDeviceToken option to false. { "plugins": [ [ "customerio-expo-plugin", { "ios": { "pushNotification": { "autoFetchDeviceToken": false } } } ] ] } Capture push tokens from the other provider and register them with our SDK using the CustomerIO.registerDeviceToken(<token>) method. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/expo/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message. This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. And, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app. 1. Add View to your app UI to support inline messages You’ll need to include a UI element in your app UI to render inline messages. The view will automatically adjust its height when messages are loaded or interacted with.  We’ve set up examples in our test app that might help if you want to see a real-world implementation of this feature. Add the InlineInAppMessageView component to your Expo app: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { return ( <InlineInAppMessageView elementId="my-message" onActionClick={(message, actionValue, actionName) => { console.log('Action clicked:', { message, actionValue, actionName }); }} /> ); } View layout The InlineInAppMessageView automatically adjusts its height at runtime when messages load or users interact with them. You should avoid setting a fixed height on this component as it might interfere with message rendering. You’re responsible for setting layout styles to position your view correctly (width, margins, padding, and so on). The component will handle its own height dynamically. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set the Display to Inline and set the Element ID to the ID you set in your app. If the editor says that the inline display feature is Web/iOS only, don’t worry about that. We’re working on updating this UI. (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the Priority. This determines which message we’ll show to your audience first, if there are multiple messages in the queue. Then craft and send your message! Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. Follow the steps below to implement custom actions for inline messages: 1. Compose an in-app message with a custom action When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. 2. Listen for events There are two ways to listen for these click events in inline in-app messages. Register a callback with your inline view: import { InlineInAppMessageView } from 'customerio-reactnative'; function MyComponent() { const handleActionClick = (message, actionValue, actionName) => { // Perform some logic when people tap an action button. // Example code handling button tap: switch (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message. case "enable-auto-renew": // Perform the action to enable auto-renew enableAutoRenew(actionName); break; // You can add more cases here for other actions default: // Handle unknown actions or do nothing console.log("Unknown action:", actionValue); } }; return ( <InlineInAppMessageView elementId="my-message" onActionClick={handleActionClick} /> ); } Register a global SDK event listener. When you register an event listener with the SDK, we’ll call the messageActionTaken event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic for inline and non-inline messages if you want. Handle responses to messages (event listeners) Like modal in-app messages, you can set up event listeners to handle your audience’s response to your messages. For inline messages, you can listen for three different events: messageShown: a message is “sent” and appears to a user. errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user. messageActionTaken: the user performs an action in the message. As shown above, this is only called if the View instance doesn’t have an onActionClick callback set. Unlike modal in-app messages, you’ll notice that there’s no messageDismissed event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app! --- ## Notification inbox URL: https://docs.customer.io/integrations/sdk/expo/in-app-messages/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io delivers inbox messages as JSON payloads, not fully-rendered messages. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message. Get the inbox instance You’ll access inbox functionality through the inbox() method on the in-app messaging module. const inbox = CustomerIO.inAppMessaging.inbox(); Inbox methods The inbox instance provides several methods to manage messages. Method Description getMessages() Fetch all messages from the inbox. Returns a Promise with the list of messages. getMessages(topic) Fetch messages filtered by topic. Returns a Promise with the filtered list of messages. subscribeToMessages(callbacks) Subscribe to inbox updates for all messages. Returns a subscription object. subscribeToMessages(callbacks, topic) Subscribe to inbox updates filtered by topic. Returns a subscription object. markMessageOpened(message) Mark a message as opened. markMessageUnopened(message) Mark a message as unopened. markMessageDeleted(message) Mark a message as deleted. trackMessageClicked(message) Track a click on the message without an action name. trackMessageClicked(message, actionName) Track a click on the message with an action name. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Fetch messages // Fetch all messages const messages = await inbox.getMessages(); // Fetch messages filtered by topic const promotions = await inbox.getMessages('promotions'); Subscribe to inbox updates // Subscribe to all messages const subscription = inbox.subscribeToMessages({ onMessagesChanged: (messages) => { console.log('Messages updated:', messages); // Update your UI with the messages setMessages(messages); } }); // Subscribe to messages filtered by topic const topicSubscription = inbox.subscribeToMessages({ onMessagesChanged: (messages) => { console.log('Topic messages:', messages); // Update your UI with filtered messages setTopicMessages(messages); } }, 'announcements'); // Don't forget to unsubscribe when you're done subscription.remove(); topicSubscription.remove(); Mark messages as opened or unopened // Mark a message as opened inbox.markMessageOpened(message); // Mark a message as unopened inbox.markMessageUnopened(message); Track message clicks // Track a click without an action name inbox.trackMessageClicked(message); // Track a click with an action name inbox.trackMessageClicked(message, 'view_offer'); Delete messages // Mark a message as deleted inbox.markMessageDeleted(message); Working with message properties You can access message properties to display custom content in your inbox: // Access message properties const { title, body, link, image } = message.properties; // Handle message action when user taps const handleMessagePress = (message) => { // Mark as opened inbox.markMessageOpened(message); // Track click inbox.trackMessageClicked(message); // Open link if available if (message.properties.link) { Linking.openURL(message.properties.link); } }; // Track with specific action name const handleActionButton = (message, actionType) => { inbox.markMessageOpened(message); inbox.trackMessageClicked(message, actionType); // Navigate based on action switch (actionType) { case 'view_offer': navigation.navigate('OfferDetails', { offerId: message.properties.offerId }); break; case 'view_order': navigation.navigate('OrderDetails', { orderId: message.properties.orderId }); break; } }; --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/expo/in-app-messages/in-app/ This page describes how to implement mobile in-app messages in your Expo project. How it works Unlike push notifications, where you need to add settings to your Expo plugins, in-app messaging is enabled automatically. You simply need to add an inApp object with your site_id to your CioConfig to support in-app messages. An in-app message is a message that people see in your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific screens in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your screens and which screen a user is on, so the SDK displays in-app messages on the correct screens in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging When you set up in-app messaging, you’ll need your workspace’s site ID. If you haven’t already, enable in-app messaging under Settings > Workspace Settings > In App Settings. Add the inApp configuration option with your site_id where you initialize the Customer.io SDK in your React Native app. To find your Site ID: Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key to generate them. import { CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const App = () => { useEffect(() => { const config: CioConfig = { cdpApiKey: 'CDP API Key', // Mandatory migrationSiteId: 'siteId', // Required if migrating from an earlier version region: CioRegion.US, inApp: { siteId: 'site_id', } }; CustomerIO.initialize(config) }, []) } Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. Make sure your page rules match the casing of the title in your screen events. Anonymous messages As of version 2.8, you can send anonymous in-app messages. These are messages that are sent only to people you haven’t identified yet. You can use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See Lead forms for more information. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/expo/in-app-messages/handling-and-dismissing-actions/ This page describes how to implement mobile in-app messages in your Expo project. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. You can register an event listener after you initialize the SDK. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging.registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); --- ## 3.x -> 3.3 URL: https://docs.customer.io/integrations/sdk/expo/whats-new/3.3-upgrade/ Version 3.3 of the Customer.io Expo plugin adds support for App Groups, improving push delivery metric tracking on iOS. What changed? Version 3.3 introduces the appGroupId configuration option, which enables App Groups for reliable push delivery metric tracking on iOS. This update also requires customerio-reactnative version 6.4.0 or later as a peer dependency. Do you need to update to this version? This update is additive—your existing integration works without changes. We recommend updating if you want to improve the reliability of push delivery metric tracking on iOS. Update process 1. Update the plugin version Update your package.json to use version 3.3 or later: npm install customerio-expo-plugin@3.3 customerio-reactnative@6.4.0 # or yarn add customerio-expo-plugin@3.3 customerio-reactnative@6.4.0 2. Configure App Groups To take advantage of the new App Groups support, add appGroupId to your plugin configuration. See App Groups for push tracking for setup instructions. 3. Rebuild your app After updating the plugin, rebuild your app: npx expo prebuild --clean npx expo run:ios --- ## 2.x -> 3.x URL: https://docs.customer.io/integrations/sdk/expo/whats-new/3.x-upgrade/ Version 3.0.0 of the Customer.io Expo plugin requires the React Native new architecture. What changed? Version 3.0.0 removes support for React Native’s legacy architecture. This aligns with React Native’s move to exclusively support their new architecture. You must migrate your app to use React Native’s new architecture to use this and future versions of the plugin. Do you need to update to this version? We recommend updating to the latest plugin version. However, if your app uses the old React Native architecture and you’re not ready to migrate, you can continue using version 2.x until you’re ready to adopt the new architecture. Update process Updating to Customer.io Expo plugin version 3.0.0 is straightforward. There are no public API changes. 1. Migrate to the new React Native architecture If you haven’t migrated to React Native’s new architecture yet, see the React Native documentation for instructions. When your app is successfully running on the new architecture, you can update to Customer.io Expo plugin version 3.0.0. 2. Update the plugin version Update your package.json to use version 3.0.0 or later: npm install customerio-expo-plugin@3.0.0 # or yarn add customerio-expo-plugin@3.0.0 3. Rebuild your app After updating the plugin, rebuild your app: # iOS npx expo prebuild --clean npx expo run:ios # Android npx expo prebuild --clean npx expo run:android 4. Test your integration Since there are no public API changes, your existing Customer.io SDK calls will continue to work. However, you should test your app after updating to ensure everything works as expected. Troubleshooting If you encounter build errors after updating, perform a clean rebuild: # Clean and rebuild npx expo prebuild --clean npx expo run:ios npx expo run:android If issues persist, ensure your app is properly configured for React Native’s new architecture and that all dependencies support it.  Try our MCP server! Our MCP server includes an integration tool that can help you install and troubleshoot issues with our SDK, including problems with push and in-app notifications. See our MCP server documentation for more information. --- ## 1x -> 2.x URL: https://docs.customer.io/integrations/sdk/expo/whats-new/2.x-upgrade/ This page helps you upgrade from the 1.0 version of our plugin to 2.0 so can take advantage of the latest features. What changed? While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. This change makes it easier to connect your app to both Customer.io and other destinations—like your analytics platform, data warehouse, or CRM. It provides customer data platform-like (CDP) features for your mobile app. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data “integration” in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click the Directory tab. Find and select the Expo integration. Enter a Name for the integration, like “My Expo App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. You’ll use it to initialize the SDK in your Expo app. (Optional) Set up your Expo app and click Test Connection. If you tested your connection first, click Complete Setup. Otherwise, click Save & Complete Later. 2. Update your app.json or app.config.js We’ve changed some of the options for our plugin, so you’ll need to update your app.json or app.config.js file to use our new configuration settings. The major change is that you’ll need a new cdpApiKey (that you get in the previous step); it replaces the siteId and apiKey parameters. { "plugins": [ [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "pushNotification": { "useRichPush": true, "env": { "cdpApiKey": "<cdpApiKey>", "region": "<region>" }, } } } ] ] } 3. Update your initialization You’ll initialize the new version of the SDK and its packages with CioConfig objects instead of CustomerioConfig. While we have a list of all the new configuration options, you’ll want to pay close attention to the following changes: CustomerIOEnv is no longer necessary. Region becomes CioRegion. siteId becomes migrationSiteId. This allows us to finish sending traffic to Customer.io as your audience upgrades to the new version of your app. You’ll initialize the SDK with initialize(config) instead of initialize(env, config). import { CioLogLevel, CioRegion, CustomerIO, CioConfig } from 'customerio-reactnative'; const config: CioConfig = { cdpApiKey: 'cdp_api_key', // Mandatory migrationSiteId: 'site_id', // For migration region: CioRegion.US, logLevel: CioLogLevel.Debug, trackApplicationLifecycleEvents: true, inApp: { siteId: 'site_id', // this removes the use of enableInApp and simplifies in-app configuration } }; CustomerIO.initialize(config) 4. Update your identify call Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not. identify: identifier becomes userId and body becomes traits track and screen calls are structured the same as previous versions, but the data object is now called properties. We’ve highlighted changes in the sample below. //identify: identifier becomes userId, body becomes traits CustomerIO.identify({ userId: "user_id", traits: { first_name: "user_name", email: "email_identifier", }, }); //track: no significant change to method //in Customer.io data object renamed properties CustomerIO.track("track_event_name", { propertyName: propertyValue }); //screen: no significant change to method. //name becomes title, data object renamed properties CustomerIO.screen("screen_event_name", { propertyName: propertyValue }); Initialization configuration changes As a part of this release, we’ve changed a few configuration options when you initialize the SDK. You’ll use CioConfig to set your configuration options. The table below shows the changes to the configuration options. But you’ll find a complete list of configuration options—both for your app.json file and where you initialize the SDK—on the Packages and configuration options page Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK. | trackApplicationLifeCycleEvents | boolean | true | When true, the SDK automatically tracks application lifecycle events (like Application Installed). | | inApp | object | | Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. | --- ## Changelog URL: https://docs.customer.io/integrations/sdk/expo/whats-new/changelog/ Check out release history our Expo Plugin. Alpha and beta releases provide access new features and fixes that have been tested internally at Customer.io but have not been tested with a production app or audience. We strongly recommend that you use these versions internally, for acceptance testing or to get a head start integrating new features in your app. test --- ## Get Started URL: https://docs.customer.io/integrations/sdk/expo/1.x/getting-started/ The Expo plugin takes advantage of our React Native SDK, and requires very little setup. It extends the Expo config to let you customize the prebuild phase of managed workflow builds, which means you don't need to eject to a bare workflow. After you add the plugin to your project, you'll need to install our React Native SDK and run prebuild. The plugin automatically generates and configures the necessary native code files required to make our React Native SDK to work on your project. This plugin has only been tested with Expo SDK versions `45` and `46`, Using `eas` build with EAS managed credentials and a limited set of Android and iOS versions. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/expo/getting-started/#install" click B href "/integrations/sdk/expo/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/expo/identify" click track-events href "/integrations/sdk/expo/track-events/" click register-token href "/integrations/sdk/expo/push" click push href "/integrations/sdk/expo/push" click rich-push href "/integrations/sdk/expo/rich-push" click in-app href "/integrations/sdk/expo/in-app" click test-support href "/integrations/sdk/expo/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Prerequisites To support the Customer.io SDK, you must: We support Expo versions 45 and later. If you use Expo 50, make sure that you use Expo 50.0.6 or later. Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Installation  We now support Expo SDK 52 You can use our Expo plugin versions 1.0.0-beta.17 or later with Expo 52. If you use Expo 52 or later, you’ll need to set your iOS deploymentTarget to 15.1. The Expo plugin takes advantage of our React Native SDK, and requires very little setup. It extends the Expo config to let you customize the prebuild phase of managed workflow builds, which means you don’t need to eject to a bare workflow. After you add the plugin to your project, you’ll need to install our React Native SDK and run prebuild. The plugin automatically generates and configures the necessary native code files required to make our React Native SDK to work on your project. We’ve tested this plugin with Expo versions 45 - 50 using an eas build with EAS managed credentials on a limited set of Android and iOS versions. By default, the plugin expects to use Apple’s Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android. We plan to add FCM support for iOS in a future release. To run the plugin, you’ll need to do the following. Each step below links to instructions and code samples you can use to get started. Install the plugin with one of the following commands. expo install customerio-expo-plugin npm install customerio-expo-plugin yarn add customerio-expo-plugin Install our React Native SDK; use version 3.9.1 or earlier with the plugin. While there are newer versions of our React Native SDK, our Expo plugin doesn’t yet support them. expo install customerio-reactnative@3.9.1 npm install customerio-reactnative@3.9.1 yarn add customerio-reactnative@3.9.1 Configure the plugin. Initialize it. Run the prebuild. Configure the plugin Add customerio-expo-plugin plugin in your app.json or app.config.js and set configuration options. In most cases, you’ll want to stick to our default options. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. app.json app.json { ... "plugins": [ ... [ "customerio-expo-plugin", { "android": { "googleServicesFile": "./files/google-services.json" }, "ios": { "pushNotification": { "useRichPush": true, "env": { "siteId": "<siteId>", "apiKey": "<apiKey>", "region": "<region>" } } } } ] ] } app.config.js app.config.js export default { ... plugins: [ ... [ "customerio-expo-plugin", { android: { googleServicesFile: "./files/google-services.json" }, ios: { pushNotification: { useRichPush: true, env: { siteId: "<siteId>", apiKey: "<apiKey>", region: "<region>" } } } } ] ] }; When you configure the plugin, you can pass options. In most cases, you’ll want to stick with the defaults, which enables all the SDK features. However you might want to disable rich push for iOS. Option Type Default Description android object undefined Required if you want to setup Android even if it is empty. Eg ("android": {}). ios object undefined Required if you want to setup iOS even if it is empty. Eg ("ios": {}). android.googleServicesFile string undefined Set the path to your google-services.json file. android.setHighPriorityPushHandler boolean undefined This is optional, if you choose to use a 3rd party plugin to handle notification permissions, but want our SDK to handle the notifications. ios.pushNotification object undefined Enables push notifications for iOS, even if it is an empty object ios.pushNotification.useRichPush boolean false Enables rich push for iOS. See [rich push setup](#enable-rich-push-on-ios) to learn more. ios.pushNotification.env object undefined Required to enable push notifications on iOS. Expected values: `siteId`: `string`,`apiKey`: `string`, `region`: `us` or `eu` ios.useFrameworks string undefined (Optional) Allows the plugin to work with static libraries. Options are static and dynamic ios.disableNotificationRegistration boolean true (Optional) Removes the `registerPushNotification` handler and allows you to control notification permission requests. We recommend that you set this to `true` (default) if you have `customerio-reactnative` version `>= 2.2.0` installed. ios.showPushAppInForeground boolean true (Available in Expo plugin version >= `1.0.0-beta.14`) Set to `true` if you want push notifications sent by Customer.io to be shown when your app is in the foreground. ios.handleDeeplinkInKilledState boolean false (Available in Expo plugin version >= `1.0.0-beta.13`) Set to `true` if you want the Customer.io SDK to handle deep links when your app is in a killed/closed state. Initialize the SDK After you install the SDK, you’ll need to initialize it in your app. To do this, you’ll add initialization code in your App.js file—or wherever you want to initialize the customerio-reactnative package. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. This makes the SDK available to use in your app. Note that you’ll still need to identify your app’s users before you can send them messages. import React, {useEffect} from 'react'; import { CustomerIO, CustomerIOEnv, Region } from 'customerio-reactnative'; const App = () => { useEffect(() => { const env = new CustomerIOEnv() env.siteId = "YourSiteId" env.apiKey = "YourAPIKey" // Region is optional, defaults to Region.US. // Use Region.EU for EU-based workspaces. env.region = Region.US CustomerIO.initialize(env) }, []) When you’re done, you may want to return to your main folder and run your application to make sure that everything’s set up correctly: iOS: npx react-native run-ios Android: npx react-native run-android Configure the SDK You can determine global behaviors for the SDK in the CustomerIO.config object. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CustomerioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. import { CustomerIO, CustomerioConfig } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug data.autoTrackDeviceAttributes = true // In-app messages are optional and disabled by default // To enable in-app messages, set enableInApp to true data.enableInApp = true // `env` is the environment constant you used // to initialize the SDK in the previous section CustomerIO.initialize(env, data) When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. enableInApp boolean false Enables in-app messaging. See in-app messaging for more details. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. trackApiUrl string Do not change this setting. This points to our Track API. iOS-specific instructions For iOS, you’ll need to perform a few special setup steps. Before you run the prebuild, you’ll need to set the iOS deploymentTarget. Install expo-build-properties and set the ios.deploymentTarget in your app.json or app.plugin.js. The deploymentTarget changes depending on the version of Expo you use. For Expo 52: 15.1. For Expo 50 and 51: 13.4. For earlier versions of Expo: 13. This is the minimum deployment target. app.json app.json { ... "plugins": [ ... [ "expo-build-properties", { "ios": { // This is the value for Expo 52 // Set to 13.4 if you use Expo 50 or 51 // Set to 13 if you use an earlier Expo version "deploymentTarget": "15.1" } } ] ] } app.plugin.js app.plugin.js export default { ... plugins: [ ... [ "", { "ios": { "deploymentTarget": "13.0" // Update to 13.4 if you use Expo SDK 50 or later } } ] ] }; Run the prebuild After you configure the SDK and initialize it in your code, run the prebuild. # Run prebuild expo prebuild # Delete ios and android folders before prebuild expo prebuild --clean Now your project is ready to use the React Native SDK. Enable Rich Push on iOS Setup ios.pushNotification.env with your siteId,apiKey and region. Enable feature with configuration option: ios.pushNotification.useRichPush set to true. When you enable rich push, the Customer.io Expo plugin adds an iOS App Extension to your iOS Xcode project. You need to setup iOS code signing for this App Extension. We have some suggestions for how to do this in your project. If you have a managed Expo project, add the app extension to your app.json file. Learn more in Expo’s documentation. "expo": { ... "extra": { "eas": { "build": { "experimental": { "ios": { "appExtensions": [{ "targetName": "NotificationService", "bundleIdentifier": "${appIdentifier}.richpush" }] } } } } } } If you have a bare Expo project, follow the official Expo docs to setup code signing for the new Xcode target. The bundle ID of the new Xcode target is: <app-bundle-id>.richpush where <app-bundle-id> is the bundle ID of your host iOS app. Example: io.customer.super-awesome-store. If neither of these suggestions helps you, follow the Expo documentation for iOS credentials to add a certification and provisioning profile (no need to create a push notification key for this new target) to your Expo configuration for this new target. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] Using the SDK as a data-in integration The SDK uses our Legacy Track API API, but it can also double as a source of data for other integrations without additional development work. To do this, we translate calls from the SDK to our newer Data Pipelines API format before we send them to your destinations. In general, we recommend that you upgrade your app to use a newer version of the SDK. Our newer versions rely on the Data Pipelines API, so you can take advantage of your mobile data without without us translating it for you. It can make it easier to trace data from your app to your destinations and troubleshoot issues as they arise. Our newer SDKs also support more features, like anonymous tracking. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/expo/1.x/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/expo/getting-started/#install" click B href "/integrations/sdk/expo/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/expo/identify" click track-events href "/integrations/sdk/expo/track-events/" click register-token href "/integrations/sdk/expo/push" click push href "/integrations/sdk/expo/push" click rich-push href "/integrations/sdk/expo/rich-push" click in-app href "/integrations/sdk/expo/in-app" click test-support href "/integrations/sdk/expo/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes the following parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). (when updating people), depending on your workspace settings. body (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person. CustomerIO.identify("person@example.com", {"first_name": "person"}) Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. If a person is already identified, and then updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their attributes with setProfileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute ["first_name": "Dana"], and then you call CustomerIO.shared.profileAttributes = ["favorite_food": "pizza"] after that, the person’s first_name attribute will still be Dana. const setProfileAttributes = () => { const profileAttributes = { favouriteFood : "Pizza", favouriteDrink : "Mango Shake", customProfileAttributes: customProfileAttribute, additionalAttributes : additionalAttributes }; CustomerIO.setProfileAttributes(profileAttributes) } Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const setDeviceAttributes = () => { const deviceAttributes = { type : "primary_device", parentObject : { childProperty : "someValue", }, }; CustomerIO.setDeviceAttributes(deviceAttributes) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. --- ## Track events URL: https://docs.customer.io/integrations/sdk/expo/1.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/expo/getting-started/#install" click B href "/integrations/sdk/expo/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/expo/identify" click track-events href "/integrations/sdk/expo/track-events/" click register-token href "/integrations/sdk/expo/push" click push href "/integrations/sdk/expo/push" click rich-push href "/integrations/sdk/expo/rich-push" click in-app href "/integrations/sdk/expo/in-app" click test-support href "/integrations/sdk/expo/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. You can use events to trigger campaigns, add people to segments, etc. Your event-triggered campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. A data object (Optional): Additional information that you might want to reference in messages or use to segment your audience, etc. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.track("add-to-cart", {"product": "shoes", "price": "29.99"}) Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking You can automatically track the screens your audience visits by setting up a function send a CustomerIO.screen event whenever the screen changes. We’ve provided some example code below. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. import React, {useRef} from 'react'; export default function App() { const navigationRef = useNavigationContainerRef(); const routeNameRef = useRef(); const config = { screens: { screen: 'myscreen', }, }; const linking = { prefixes: ['myapp://'], config }; return ( <NavigationContainer ref={navigationRef} linking={linking} onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute().name; }} onStateChange={async () => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.getCurrentRoute().name; if (previousRouteName !== currentRouteName) { CustomerIO.screen(currentRouteName) } routeNameRef.current = currentRouteName; }} // End > ); } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen("screen-name", {"property": "value"}) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/expo/1.x/push-notifications/push/ The Expo plugin supports push notifications over APN for iOS and FCM for Android. Use this page to get started with push notification services. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/expo/getting-started/#install" click B href "/integrations/sdk/expo/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/expo/identify" click track-events href "/integrations/sdk/expo/track-events/" click register-token href "/integrations/sdk/expo/push" click push href "/integrations/sdk/expo/push" click rich-push href "/integrations/sdk/expo/rich-push" click in-app href "/integrations/sdk/expo/in-app" click test-support href "/integrations/sdk/expo/test-support" style push fill:#B5FFEF,stroke:#007069 Before you begin This page explains how to receive push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through your preferred service: Apple Push Notification Service (APNs) and/or Firebase Cloud Messaging (FCM). This process lets you receive both basic push notifications in your app—a title and a message body—and rich push notifications with images and deep links.  We do not support Expo’s Push Service You can’t use push tokens obtained through Expo’s Push Service to send push notifications through Customer.io. If your app previously used Expo’s push service, you’ll have to obtain new FCM or APN tokens to send push notifications through Customer.io. Luckily, you won’t need to do anything to support your app’s users as you implement our SDK! If your app is able to obtain Expo push tokens, it should also be set up to receive APN or FCM device tokens. You’ll simply configure your app to obtain an APN or FCM push token and send that token to Customer.io using CustomerIO.registerDeviceToken("<apn or fcm token here>"). How it works When you use our Expo plugin, you’re already set up to support push. Before a device can receive a push notification, you must: Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. (Optional) Set up your app to report push metrics back to Customer.io. Set up push on iOS For the most part, we’ll set up push notifications for you when you run expo prebuild. But before you do, you’ll need to add some additional code to support push notifications for iOS. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your React Native project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project, and then under Targets, select your main app. Click the Signing & Capabilities tab Click Capability. Add Push Notifications to your app. When you’re done, you’ll see Push Notifications added to your app’s capabilities. Now you’re ready to run the expo prebuild. Update iOS dependencies In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Expo plugin. The command to update these packages depends on whether your Expo project uses a managed or bare workflow. For managed workflows, you can simply re-run the prebuild. Managed workflow Managed workflow expo prebuild --clean # the --clean option is optional Bare workflow Bare workflow pod update --repo-update --project-directory=ios Rich push payloads When you send a push notification, your app will expect the payload below; you can use the Custom Payload option in the push editor to . In the editor, you’ll select the type of device you want to send your message to: you can have separate payloads for Android and iOS. The top level of the payload changes slightly depending on your push provider, APNS or FCM. Otherwise, your JSON is split into two major objects: an aps object, which contains the standard aspects of a push—the alert.title and alert.body of your message—and Apple’s push options. a CIO object containing the rich aspects of your message that the SDK will interpret. At present, it contains link and image strings. iOS APNs iOS APNs { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app:://... "image": "string" //HTTPS URL of your image, including file extension } } } CIO object Contains options supported by the Customer.io SDK. push object Required Describes push notification options supported by the CIO SDK. iOS FCM iOS FCM { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Prompt users to opt-into push notifications Your audience has to opt into push notifications. To display the native iOS and Android push notification permission prompt, you’ll use the CustomerIO.showPromptForPushNotifications method (available only in customerio-reactnative version >=2.2.0). You can configure push notifications to request authorization for sounds and badges as well (only on iOS). If a user opts into push notifications, the CustomerIO.showPromptForPushNotifications method will return Granted, otherwise it returns Denied as a string. If the user already responded to the push authorization prompt, the current authorization status is returned as a string. var options = {"ios" : {"sound" : true, "badge" : true}} CustomerIO.showPromptForPushNotifications(options).then(status => { switch(status) { case "Granted": // Push permission is granted, your app can now receive push notifications break; case "Denied": // App is not authorized to receive push notifications // You might need to explain users why your app needs permission to receive push notifications break; case "NotDetermined": // Push permission status is not determined (Only for iOS) break; } }).catch(error => { // Failed to show push permission prompt console.log(error) }) Get a user’s permission status To get a user’s current permission status, call the CustomerIO.getPushPermissionStatus() method. This returns a promise with the current status as a string. CustomerIO.getPushPermissionStatus().then(status => { console.log("Push permission status is - " + status) }) Test your implementation Before you release your app, you should test your implementation. Use the payloads below to send a push in the Customer.io web app with a Custom Payload. In both of the test payloads below, you should: Set the link to the deep link URL that you want to open when your tester taps your notification. Set the image to the URL of an image you want to show in your notification. It’s important that the image URL starts with https:// and not http:// or the image might not show up. APNs test payload APNs test payload { "CIO": { "push": { "link": "remote-habits://deep?message=hello&message2=world", "image": "https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg" } }, "aps": { "mutable-content": 1, "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" } } } FCM test payload FCM test payload { "message": { "apns": { "payload": { "CIO": { "push": { "link": "remote-habits://deep?message=hello&message2=world", "image": "https://thumbs.dreamstime.com/b/bee-flower-27533578.jpg" } }, "aps": { "mutable-content": 1, "alert": { "title": "Title of your push goes here!", "body": "Body of your push goes here!" } } } } } } --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/expo/1.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Set up deep links There are a number of ways to enable deep links. Our example below uses @react-navigation with a config and prefix to automatically set paths. The paths are the values you’d use in your push payload to send a link. import { NavigationContainer } from '@react-navigation/native'; const config = { screens: { Home: { path: 'home/:id?', parse: { id: (id: String) => `${id}`, }, }, } }; const linking = { prefixes: ['myapp://'], config }; --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/expo/1.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the React Native SDK, you're ready to go. But there are some side-cases where you may want to capture metrics outside the SDK. How it works Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. As of version 1.0.0-beta.14 of our Expo plugin, we track both opened and delivered metrics automatically for you. Beginning in version 1.0.0-beta.14 of our Expo plugin, the SDK automatically tracks opened and delivered events for push notifications originating from Customer.io. No more code is required for your app to track opened push metrics or launch deep links!  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Read the sections below to see how you can add (optional) custom handling to various push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, set the following option in your Expo config file: { ... "plugins": [ ... [ "customerio-expo-plugin", { "ios": { "showPushAppInForeground": true, } } ] ] } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like you want to process custom fields in your push notification payload. For now, the React Native SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationOpened((notification: Notification, completion) => { // Process custom data attached to payload, if you need: let pushPayload = notification.payload; // Important: When you're done processing the push notification, you're required to call completion(). // Even if you do not process a push, this is still a requirement. completion(); });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the React Native SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular React Native push notification SDKs to receive a callback. For example, the code below receives a callback using react-native-push-notification. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import { Notifications } from 'react-native-notifications'; Notifications.events().registerNotificationReceivedForeground( (notification: Notification, completion) => { // Important: When you're done processing the push notification, you must call completion(). // Even if you do not process a push, you must still call completion(). completion({ alert: true, sound: true, badge: true }); // If the push notification originated from Customer.io, the value returned in the `completion` is ignored by the SDK. // Use the SDK's push configuration options instead. }); Manually track push metrics  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics.  Known issue tracking opened push metrics in app killed state When manually tracking push metrics using Javascript methods, opened push metrics are not tracked when the app is in killed or closed state. This is a known behavior and it’s recommended to instead use the automatic push tracking feature. To monitor the delivered push metrics of a received push notification, use the CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationReceived(<CUSTOMER.IO_PAYLOAD>) To track opened push metrics, use the CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) method. CustomerIO.pushMessaging.trackNotificationResponseReceived(<CUSTOMER.IO_PAYLOAD>) The method that you use to retrieve the <CUSTOMER.IO_PAYLOAD> value depends on API of the SDK that you are using to receive push notifications from. Here is a code snippet as an example from expo-notifications: // Listener called when a push notification is received Notifications.addNotificationReceivedListener(notification => { ... // Fetch Customer.io payload from the push notification const payload = notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationReceived(payload) ... }); // Receives response when user interacts with the push notification Notifications.addNotificationResponseReceivedListener(response => { ... // Fetch Customer.io payload from the push notification response const payload = response.notification.request.trigger.payload CustomerIO.pushMessaging.trackNotificationResponseReceived(payload) ... }); Disabling automatic push tracking If you have a more advanced use case and want to manually track push metrics, you can disable automatic push tracking with the autoTrackPushEvents option in your Expo config. { ... "plugins": [ ... [ "customerio-expo-plugin", { "ios": { "autoTrackPushEvents": false, } } ] ] } --- ## Set up in-app messages URL: https://docs.customer.io/integrations/sdk/expo/1.x/in-app/in-app/ This page describes how to implement mobile in-app messages in your Expo project. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/expo/getting-started/#install" click B href "/integrations/sdk/expo/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/expo/identify" click track-events href "/integrations/sdk/expo/track-events/" click register-token href "/integrations/sdk/expo/push" click push href "/integrations/sdk/expo/push" click rich-push href "/integrations/sdk/expo/rich-push" click in-app href "/integrations/sdk/expo/in-app" click test-support href "/integrations/sdk/expo/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging Before you can take advantage of in-app messaging, you need to set up your workspace to receive in-app messages. You’ll find your in-app settings under Settings > Workspace Settings > In App Settings. You also need to set enableInApp to true in your configuration options. import { CustomerIO, CustomerioConfig, CustomerIOEnv, Region } from 'customerio-reactnative'; const data = new CustomerioConfig() data.logLevel = CioLogLevel.debug data.autoTrackDeviceAttributes = true // In-app messages are optional and disabled by default // To enable in-app messages, set enableInApp to true data.enableInApp = true CustomerIO.initialize(env, data) Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/expo/1.x/in-app/handling-and-dismissing-actions/ This page describes how to implement mobile in-app messages in your Expo project. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) If you’re using customerio-reactnative package version 2.1.0 or later, you can listen for, and execute code based on, your audience’s responses to in-app messages. See our getting started guide if you need to update your React Native package. You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. import { CustomerIO, InAppMessageEventType } from "customerio-reactnative"; CustomerIO.inAppMessaging().registerEventsListener((event) => { switch (event.eventType) { case InAppMessageEventType.messageShown: // handle message shown break; case InAppMessageEventType.messageDismissed: // handle message dismissed break; case InAppMessageEventType.errorWithMessage: // handle message error break; case InAppMessageEventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. // handle message action break; } }); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging().dismissMessage(); --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/expo/1.x/updates-and-troubleshooting/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Try updating your iOS push package In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Expo plugin. Before you contact support, you might want to [update your iOS dependencies](/Page(/integrations/sdk/expo/push/#update-ios-dependencies) to get the latest packages and see if that fixes the issue. You can also check out our latest iOS changes to see if we’ve already fixed the issue or check out open issues to see if you’re experiencing a known issue. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/expo/1.x/updates-and-troubleshooting/changelog/ Check out release history our Expo Plugin. Alpha and beta releases provide access new features and fixes that have been tested internally at Customer.io but have not been tested with a production app or audience. We strongly recommend that you use these versions internally, for acceptance testing or to get a head start integrating new features in your app. test --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/flutter/quick-start-guide/ Before you can take advantage of our SDK, you need to install and initialize the SDK.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview Flutter lets you build native mobile apps using Dart. Our Flutter SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages. Install and initialize the SDK. Identify and Track Push Notifications In-App 1. Install the SDK In your project folder install the customer_io package: flutter pub add customer_io This adds a line to your package’s pubspec.yaml dependencies: customer_io: ^4.0.1 Set up your project to support iOS and/or Android: iOS iOS Set up your iOS dependencies: CocoaPods: Run pod install --repo-update --project-directory=ios. SPM: Make sure SPM is enabled in your Flutter project (see Flutter docs), and run flutter pub get. The SDK is automatically resolved. For full iOS setup details, including the Notification Service Extension for rich push, see Set up push notifications. Android Android Go to the Android subfolder and include google-services-plugin by adding the following lines to the project-level android/build.gradle file: buildscript { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } dependencies { // Add this line: classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } } Add the following line to android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and copy the file to android/app/google-services.json. Initialize the SDK: Add your CDP API key and site ID to your configuration. CDP API Key: You’ll find this key in your Flutter connection. Site ID: You’ll find this value in your workspace under Settings > Workspace Settings > API and webhook credentials. Initialize the SDK in your app. In your main.dart file—or wherever you want to initialize the CustomerIO plugin—add the code below: import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. inAppConfig: InAppConfig(siteId: 'siteId'), ), ); Run your application to ensure everything is set up correctly. 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"}); Track screen views to trigger in-app messages associated with specific screens. CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"}); 3. Push Notifications iOS iOS Set up your push notification credentials in Customer.io: Upload your Firebase Cloud Messaging server key (.json format). Our Flutter SDK uses FCM for both iOS and Android push notifications. Request push notification permissions from the user. You can do this through Firebase or any other package. To ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes. Android Android Set up your push notification credentials in Customer.io: Upload your Firebase Cloud Messaging server key (.json format). Request push notification permissions from the user. You can do this through Firebase or any other package. Ensure that you: Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android. Our Flutter SDK receives push notifications from FCM. Add notification icon resources: Place a notification icon file named ic_notification.png in your drawable folders. Make sure your app’s AndroidManifest.xml has the proper FCM permissions. 4. In-App To enable in-app messaging, all you need to do is add your site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app. You can call the initialize method from your components or services: import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. inAppConfig: InAppConfig(siteId: 'siteId'), pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehavior: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); --- ## How it works URL: https://docs.customer.io/integrations/sdk/flutter/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum support requirements To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/flutter/getting-started/auth/ To use the SDK, you'll need to get two kinds of keys: A [*CDP API Key*](#get-your-cdp-api-key) to send data to Customer.io and a [*Site ID*](#get-your-site-id) that tells the SDK which workspace your messages come from. Get your CDP API Key You’ll use a CDP API Key to initialize the SDK and send data to Customer.io. You’ll get this key when you set up your mobile app as a data inAn integration that feeds data into Customer.io. integration in Customer.io. If you haven’t already set up your integration in Customer.io, you’ll need to do that first. Go to Integrations. Select your Flutter integration in the Overview tab. If you don’t see a Flutter integration, you’ll need to set it up. Go to Settings and find your CDP API Key. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should set the siteId that you used in previous versions as the migrationSiteId in your config. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your CDP API Key>', //migrationSiteId is required if you're updating from a previous version migrationSiteId: '<your siteId>', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), );  You’re not done yet You still need your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials to initialize the CioMessagingInApp package and to support people updating your app from a previous version of Customer.io SDK. See Get your Site ID below. Set up a new integration If you don’t already have a write key, you’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select Flutter. Enter a Name for your integration, like “My Flutter App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Test your connection and click Complete Setup. Or, if you don’t want to test your implementation yet, Save & Complete Later and then click Install Source to finish the setup process. In this case, Complete Later simply means that we haven’t seen any data from your Flutter app yet. Remember, you can also connect your Flutter app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your Site ID to send in-app messages from your workspace. If you’re upgrading from a previous version, my can also set your Site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the inApp package. If you’re upgrading from a previous version, you’ll also use it as your migrationSiteId. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', //required if you're updating from a previous version migrationSiteId: '<your siteId>', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Configuration Options URL: https://docs.customer.io/integrations/sdk/flutter/getting-started/packages-options/ The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use. You’ll call configuration options before you initialize the SDK with CustomerIOConfig. When you initialize the SDK, you can pass configuration options. In most cases, you’ll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: "YOUR_CDP_API_KEY", // Required migrationSiteId: "YOUR_SITE_ID", // Required to migrate from a previous version autoTrackDeviceAttributes: true, region: Region.us, logLevel: CioLogLevel.error ) ); Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region eu or us us Required if your account is in the EU region. apiHost string The domain you’ll proxy requests through. You’ll only need to set this (and cdnHost) if you’re proxying requests. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc cdnHost string The domain you’ll fetch configuration settings from. You’ll only need to set this (and apiHost) if you’re proxying requests. logLevel string error Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. migrationSiteId string Required if you're updating from 1.x: the credential for previous versions of the SDK. We use this key to send remaining tasks to Customer.io when your audience updates your app. screenViewUse All or inApp all ScreenView.all (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don’t want the app to send lifecycle events like Application Opened inApp.siteId string Used to initialize the inApp package, and determines the workspace your in-app messages come from. push.android.pushClickBehavior string activityPreventRestart One of resetTaskStack, activityPreventRestart, activityNoFlags; determines how to handle push clicks. locationConfig object Enable location tracking. Takes a trackingMode option using the LocationTrackingMode enum. Proxying requests By default, requests go through our domain at cdp.customer.io. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores. To proxy requests, you’ll need to set the apiHost and cdnHost properties in your SDKConfigBuilder. While these are separate settings, you should set them to the same URL. While you need to initialize the SDK with a cdpApiKey, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the cdpApiKey to a value representing basic authentication credentials that you handle on your own. See proxying requests for more information. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: "YOUR_CDP_API_KEY", apiHost: "YOUR_PROXY_HOST", cdnHost: "YOUR_CDN_HOST", region: Region.us, logLevel: CioLogLevel.error ) ) --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/flutter/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Capture logs Logs help us pinpoint the problem and find a solution. To capture logs, you should install Flutter DevTools if you haven’t already. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "919a7e12107bd03155f6", apiKey: "86344654754f1c48d32b", region: Region.us, //config options go here logLevel: CioLogLevel.debug ), ); Build and run your app on a physical device or emulator. Open the Logging view in your development application. If you use Android Studio, select View > Tool Windows > Logcat to see your logs. Filter for CIO in the top to find log messages specific to the Customer.io SDK. Export your log and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Capturing iOS logs Logs for iOS are emitted via Apple’s Unified Logging system, so you’ll need to capture them using Xcode or the MacOS console, not just the Flutter debug console. The logLevel set in your CustomerIOConfig is forwarded to the native iOS SDK, but these logs won’t reliably appear in flutter run output. To capture iOS logs for troubleshooting: Run your iOS app via Xcode or on a device/simulator attached to Xcode. Use the macOS Console to capture logs. You might want ot filter for CIO to find log messages specific to the Customer.io SDK. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Flutter SDK. Before you contact support, you might want to [update your iOS dependencies](/Page(/integrations/sdk/flutter/push/#update-ios-dependencies) to get the latest packages and see if that fixes the issue. You can also check out our latest iOS changes to see if we’ve already fixed the issue or check out open issues to see if you’re experiencing a known issue. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. Notifications not coming through when app is in background If your app does not receive push notifications when it’s in the background, check the following: Ensure that you have implemented the _firebaseMessagingBackgroundHandler as suggested by the Firebase Messaging documentation. This handler is responsible for processing messages received while the app is in the background or closed. Verify that the handler is set correctly and receiving callbacks. Double-check the implementation to ensure it’s properly registered within your app’s code. Confirm the Flutter version you are using. For Flutter version 3.3.0 or higher, you will need to add @pragma('vm:entry-point') to the handler function for it to work correctly. Swift Package Manager (SPM) issues Unable to find module dependency: 'CioMessagingPushFCM' in NSE The NotificationServiceExtension target is missing the SPM package link. In Xcode, select the NotificationServiceExtension target and go to General > Frameworks and Libraries Click + and add FlutterGeneratedPluginSwiftPackage. redefinition of module 'Firebase' Firebase is being loaded from both CocoaPods and SPM, causing a conflict. Update your Firebase dependencies to SPM-compatible versions in your pubspec.yaml: firebase_core: ^3.5.0 # we recommend at least ^4.6.0 firebase_messaging: ^15.2.0 # we recommend at least ^16.1.3 Remove any manual Firebase pod lines from your ios/Podfile and run flutter pub upgrade firebase_core firebase_messaging. requires minimum platform version 15.0 for the iOS platform The Customer.io SDK and Firebase require iOS 15.0 or later. Set the deployment target to 15.0 or later in Xcode: Select the Runner project and go to Build Settings Set iOS Deployment Target to 15.0. Update your ios/Podfile: platform :ios, '15.0' NSE unable to find CioMessagingPushFCM dependency The NotificationServiceExtension can’t resolve the Customer.io SDK dependency. This can happen in both local builds and CI environments. Make sure SPM is enabled before building. You can enable it globally or per-project: Global: Run flutter config --enable-swift-package-manager Per-project: Add enable-swift-package-manager: true to the flutter: > config: section of your pubspec.yaml Clean and rebuild: flutter clean flutter pub get flutter build ios In your CI pipeline, make sure that you run flutter config --enable-swift-package-manager before running flutter pub get to ensure that the configuration takes effect. import UserNotifications error SPM doesn’t transitively import system frameworks the way CocoaPods does. If your NSE fails to compile with errors about UNNotificationServiceExtension, UNNotificationRequest, or UNNotificationContent, you’re missing the system import. Add import UserNotifications at the top of your NotificationService.swift: import CioMessagingPushFCM import UserNotifications In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/flutter/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes in Customer.io. If you’ve already identified a person, and they update their preferences, provide additional information about themselves, or perform other attribute-changing actions, you can update their attributes with profileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute {"first_name": "Dana"}, and then you call CustomerIO.instance.setProfileAttributes(traits: {"favorite_food": "pizza"});, the person will gain the favorite_food attribute but first_name attribute will still be Dana. CustomerIO.instance.setProfileAttributes(traits: { "first_name": "Cool", "last_name": "User", "is_premium": false, }); Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person more broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const deviceAttributes = { "type" : "primary_device", "parentObject" : { "childProperty" : "someValue", }, }; CustomerIO.instance.setDeviceAttributes(attributes: deviceAttributes); Disable automatic device attribute collection By default, the SDK automatically collects the device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. defined above. You can disable the autoTrackDeviceAttributes setting to prevent the SDK from automatically collecting these attributes. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', autoTrackDeviceAttributes: false, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call CustomerIO.instance.registerDeviceToken(token). Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.instance.clearIdentify(); Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.instance.identify(identifier: "new.person@example.com", attributes: {"first_name": "New", "last_name": "Person"}); --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/flutter/tracking/lifecycle-events/ By default, the Customer.io SDK automatically tracks lifecycle events for your users. These are events that represent the lifecycle of your app and your users' experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track call. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed if your app crashes or Application Updated when people update your app. CustomerIO.instance.track( name: "Application Crashed", properties: [ "url": "urls://page/in/app" ] ) Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in your configuration. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', region: Region.us, trackApplicationLifecycleEvents: false, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/flutter/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, and we merge anonymous activity with the identified person, the previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity with their identified profile, and their previously-anonymous screen view triggers the campaign you set up for people who visited the product page. You can return a person’s anonymous ID at ay time by calling CustomerIO.shared.anonymousId. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/flutter/tracking/screen-events/ Screen events track the screens people view in your app. They help you track the parts of your app your users engage with and are vital in determining where you display in-app messages. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a title representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your they use. Screen events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using Route observer for automatic screen tracking. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> { void _sendScreenView(PageRoute<dynamic> route) { var screenName = route.settings.name; // track screen manually CustomerIO.screen(name: screenName ?? "N/A"); } @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPush(route, previousRoute); if (route is PageRoute) { _sendScreenView(route); } } @override void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); if (newRoute is PageRoute) { _sendScreenView(newRoute); } } @override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPop(route, previousRoute); if (previousRoute is PageRoute && route is PageRoute) { _sendScreenView(previousRoute); } } } // Usage class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(), navigatorObservers: [MyRouteObserver()], home: Screen1(), routes: { 'screen2': (context) => Screen2(), 'screen3': (context) => Screen3(), }, ); } } Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.inApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: "YOUR_CDP_API_KEY", // Required screenViewUse: ScreenView.inApp, ), ); Send your own screen events Screen events use the .screen method. Like other event types, you can add a properties object containing additional information about the screen or the user. CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"}); --- ## Track events URL: https://docs.customer.io/integrations/sdk/flutter/tracking/track-events/ Events are things people do in your app. They help you track your audience's behaviors, activity, and metrics. You can use them to segment your audience, trigger messaging campaigns, and see how people use your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name (Required): The name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you want to reference in messages or use to segment your audience, etc. You can reference event properties in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<properties>}}. CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"}); Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.instance.track(name: "User Deleted) --- ## Location tracking URL: https://docs.customer.io/integrations/sdk/flutter/tracking/location/ Real-time location tracking lets you update a person's profile with accurate coordinates so you can send geo-aware messages and segment users by location. How it works The Location module captures location (with user consent) from your app and attaches it to a person’s profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than IP-based geolocation. When you identify a person, the SDK includes the latest location in the identify call. The SDK also sends a Location Update event to the person’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update. The SDK does not request location permission on its own—your app must handle the permission flow. Install the location module The Location module requires native dependencies on both iOS and Android. Follow the platform-specific steps below. Add the following property to your project’s android/gradle.properties file: customerio_location_enabled=true CocoaPods CocoaPods Add the location subspec to your Podfile. Open ios/Podfile in your Flutter project and add the following line alongside your existing customer_io pod: pod 'customer_io/location', :path => '.symlinks/plugins/customer_io/ios' SPM SPM No additional iOS setup is needed. The customerio_location_enabled flag you set in gradle.properties also controls whether the Location module is included via SPM on iOS. If the flag isn’t detected (e.g. in Flutter add-to-app modules or some CI environments), you can set the CIO_LOCATION environment variable instead: CIO_LOCATION=true flutter build ios You can also set this in Android Studio under Run > Edit Configurations > Environment variables. Initialize the SDK with the location module Add a locationConfig to your CustomerIOConfig to enable the module. The trackingMode property controls how and when the SDK captures location. Option Type Default Description trackingMode LocationTrackingMode manual Controls how and when the SDK captures location. See tracking modes below. Tracking modes Mode Description LocationTrackingMode.manual Your app controls when it captures location. Call setLastKnownLocation() or requestLocationUpdate() to provide location. Use this when your app already has a location-tracking mechanism or you want full control over when you capture location data. LocationTrackingMode.onAppStart The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call setLastKnownLocation() or requestLocationUpdate() alongside automatic capture. Use this for hands-off location tracking with minimal battery impact. LocationTrackingMode.off Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'YOUR_CDP_API_KEY', // ...other config options locationConfig: LocationConfig( trackingMode: LocationTrackingMode.manual, ), ), ); Location APIs The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a Location Update event no more than once a day—and only if the person has moved a meaningful distance since the last update. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates. setLastKnownLocation Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io. Parameter Type Description latitude double Latitude in degrees. Must be between -90 and 90. longitude double Longitude in degrees. Must be between -180 and 180. // Pass coordinates from your app's location provider CustomerIO.location.setLastKnownLocation( latitude: 37.7749, longitude: -122.4194, ); requestLocationUpdate Request a one-shot location from the native platform’s location services. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user. If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes. Add the required key to your Info.plist: <key>NSLocationWhenInUseUsageDescription</key> <string>We use your location to personalize your experience.</string> (Optional) If you want more precise location, add ACCESS_FINE_LOCATION to your AndroidManifest.xml. The SDK already includes ACCESS_COARSE_LOCATION when the location module is enabled. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> After your app requests and receives permission at runtime, call the SDK: // After location permission is granted CustomerIO.location.requestLocationUpdate();  We recommend using a package like geolocator or location to handle location permission requests in your Flutter app. Profile switch behavior When you call CustomerIO.instance.clearIdentify(), the SDK clears cached location data so that one person’s location doesn’t carry over to another person’s profile. The next person you identify starts with a clean slate. Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next identify() call includes it automatically. --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/push-setup/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. This page helps you add push support to your app. How it works If you’ve followed our getting started instructions, you’re already set up to send push notifications to your Android audience. But you’ll need to add a bit of code to support your iOS users. Remember that a device/user can’t receive a push notification until you: (iOS) Register a device token for the device; code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. (Both iOS and Android) Check for notification permissions. If your app user doesn’t grant permission, notifications will not appear in the system tray. (Optional) Set up your app to report push metrics back to Customer.io.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Firebase Cloud Messaging (FCM). Set up push for Android If you followed our Getting Started instructions, you’re already set up to send push notifications to Android devices. You just need to set up iOS push support in your app. Next, you can set up deep links if you want your notifications to link into your app. Set or change your push icon You’ll set the icon that appears on push notifications as a part of the AndroidManifest.xml file in your app’s android folder. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Set up push for iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your Flutter project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project. Under Targets, select your main app. Click the Signing & Capabilities tab and click Capability. Add Push Notifications to your app. Select File > New > Target. Select Notification Service Extension and click Next. You should see a window such as this: You can leave many of the options in this window as their defaults, but you should: Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) Confirm that your main app is selected in the Embed in Application drop-down menu When you’re done, click Finish. When presented with the dialog below, click Cancel. This will help Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Install dependencies You can install iOS dependencies using CocoaPods or Swift Package Manager (SPM).  Switching to SPM? If you’re migrating from CocoaPods to SPM, see Upgrade from 3.x to 4.0.0 for step-by-step instructions. CocoaPods CocoaPods If you previously enabled SPM, make sure it’s disabled by running flutter config --no-enable-swift-package-manager. Open the file ios/Podfile and make the following modifications: target 'Runner' do # Look for the main app target. # Required by FCM push notification service use_frameworks! # Make all file modifications after these lines: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios' end Run pod install --repo-update --project-directory=ios from the root directory of your Flutter project. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Swift Package Manager (SPM) Swift Package Manager (SPM) Before you start, make sure you meet the following requirements: Flutter 3.24+ Xcode 16.0+ firebase_core: ^3.5.0 and firebase_messaging: ^15.2.0 or higher (older versions don’t support SPM). We recommend at least firebase_core: ^4.6.0 and firebase_messaging: ^16.1.3. Enable SPM (if not already enabled). Run flutter config --enable-swift-package-manager, and then run flutter pub get. The Customer.io SDK is automatically resolved - no manual Xcode setup needed for the main app. Update your Podfile. Remove any customer_io pod lines and the NotificationServiceExtension target block from your ios/Podfile: pod 'customer_io/fcm', :path => ... pod 'customer_io/location', :path => ... pod 'customer_io_richpush/fcm', :path => ... With SPM enabled, Flutter’s pod helper automatically skips SPM-handled plugins. Set up Rich Push (NSE). Open ios/Runner.xcworkspace in Xcode. Select the NotificationServiceExtension target. Go to General > Frameworks and Libraries > click + and add FlutterGeneratedPluginSwiftPackage if it isn’t already added. The plugin automatically includes the following modules via SPM: Module Purpose DataPipelines Core SDK MessagingInApp In-app messaging MessagingPushFCM Push notifications and Firebase integration The Location module is not included by default. To include it, set customerio_location_enabled=true in your android/gradle.properties file. This flag controls both Android and iOS. See Location tracking for details. Configure your app Update your AppDelegate.swift file to handle push notifications: import UIKit import Flutter import CioMessagingPushFCM import CioFirebaseWrapper import FirebaseMessaging import FirebaseCore @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Depending on how you install Firebase, // you may need to add functions to this file, like: // FirebaseApp.configure() // // Read the official Firebase docs to install Firebase correctly! MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() // .appGroupId("group.com.example.myapp.cio") // Optional: for reliable delivery tracking .build() ) // Add line below only if you want to have custom control over notifications being presented and processed - Customer.io will handle those automatically UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) Messaging.messaging().apnsToken = deviceToken } } Open your NotificationService.swift file in Xcode and modify it with the highlighted changes below: import CioMessagingPushFCM import UserNotifications // Required when using SPM class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YourCdpApiKey") // Optional: set your Customer.io account region (.US or .EU). Default: US .region(.US) // .appGroupId("group.com.example.myapp.cio") // Optional: for reliable delivery tracking .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } }  import UserNotifications is required when using SPM. CocoaPods transitively imports system frameworks, so the import isn’t required with CocoaPods, but it doesn’t hurt to include it. Now you can run your app on a physical device and send yourself push notifications with images and deep links to test your implementation. You’ll have to use a physical device because emulators can’t receive push notifications. Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Display push notifications in the foreground (iOS) By default, iOS doesn’t display push notifications when your app is in the foreground. The showPushAppInForeground configuration flag (set to true by default) tells iOS to display notifications even when your app is active. MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .showPushAppInForeground(true) .build() )  For more information about handling push notifications in the foreground, including custom handling for non-Customer.io push notifications, see push metrics and custom handling. Implement the notification center delegate for more control For more granular control over foreground notification behavior, you can implement the userNotificationCenter(_:willPresent:withCompletionHandler:) method in your AppDelegate.swift file. This native implementation takes precedence over the showPushAppInForeground configuration flag. This method lets you inspect the notification content and decide whether to display it, and choose which presentation options to use (banner, list, badge, sound, or any combination). Add this method to the AppDelegate class you set up in the integration step above: override func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { // Show notifications in foreground with banner, list, badge, and sound completionHandler([.banner, .list, .badge, .sound]) // Or return empty array if you do NOT want notifications shown in foreground // completionHandler([]) } Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. Next steps After you set up push notifications, configure App Groups for reliable delivery tracking on iOS. Without App Groups, iOS can end the Notification Service Extension before delivery metrics are sent, which means some data may never reach Customer.io. App Groups provide shared storage so the SDK can recover any lost metrics on the next app launch. --- ## App Groups for push tracking URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/app-groups/ Configure App Groups for reliable push delivery tracking. App Groups let the SDK recover delivery metrics that iOS might otherwise discard when it terminates the Notification Service Extension. You need App Groups for reliable push delivery tracking on iOS. Without this setup, delivery metrics may be lost if iOS terminates the Notification Service Extension before the tracking request completes. With App Groups configured, the SDK automatically recovers any lost metrics on the next app launch. Before you begin Before you configure App Groups, make sure you’ve completed the following: Set up push notifications in your app, including the Notification Service Extension (NSE) 1. Add the App Group capability in Xcode You need to add the App Groups capability to both your main app target and your Notification Service Extension target in Xcode. Automatic signing Automatic signing If you use automatic signing (the most common setup), this is the only step outside of code. Xcode registers the group and updates provisioning profiles automatically. In Xcode, select your main app target > Signing & Capabilities > + Capability > App Groups. Click + and enter your group identifier—for example, group.com.example.myapp.cio. Select your Notification Service Extension target > Signing & Capabilities > + Capability > App Groups. Select the same App Group you created in step 2. Both targets must reference the exact same App Group string. Manual signing Manual signing If you use manual signing, you need to register the group in the Apple Developer Portal and regenerate your provisioning profiles. Sign in to the Apple Developer Portal and go to Certificates, Identifiers & Profiles. Click Identifiers > + > App Groups. Enter your identifier. It must start with group.—for example, group.com.example.myapp.cio. Under Identifiers, select your main app’s App ID, enable App Groups, click Configure, and select your group. Repeat step 4 for your Notification Service Extension’s App ID. Regenerate provisioning profiles for both your main app and Notification Service Extension. Enabling App Groups invalidates your existing provisioning profiles.  You must regenerate your provisioning profiles in the Apple Developer Portal after enabling App Groups. You don’t need to regenerate certificates.  Already have an App Group? You can reuse an existing App Group by passing its identifier to .appGroupId(). There’s no conflict in having multiple App Groups on a target. 2. Pass the App Group ID in your SDK configuration After you add the App Group capability, pass the App Group ID to the SDK in both your host app and your Notification Service Extension. Both are required—App Groups work by sharing storage between the two targets, so the SDK needs the identifier in each place to read and write delivery metrics. The App Group ID must be identical in both places and must match the entitlements you set up in Xcode.  The .appGroupId() method is configured in native Swift code, not in Dart. The Notification Service Extension runs as a separate iOS process outside the Flutter runtime. Host app initialization In your AppDelegate.swift file, add .appGroupId() to the MessagingPushFCM.initialize call: MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .appGroupId("group.com.example.myapp.cio") .build() ) Notification service extension initialization In your NotificationService.swift file, add .appGroupId() to the MessagingPushFCM.initializeForExtension call: MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YOUR_CDP_API_KEY") .appGroupId("group.com.example.myapp.cio") .build() )  App Group ID must match everywhere The .appGroupId() value must be identical in your host app initialization, your NSE initialization, and the App Group entitlements on both targets. A mismatch prevents the SDK from accessing shared storage. Fallback behavior If you omit .appGroupId(...), the SDK attempts to infer the identifier using group.{bundleId}.cio. This can work as a fallback, but we recommend explicitly passing the value to avoid configuration issues. --- ## Deep links URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: Set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. Visit Flutter’s documentation for more info on deep linking. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "myapp://home" --> <data android:host="home" android:scheme="myapp" /> </intent-filter> After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior (Android) When you initialize the SDK, you can set a pushConfig that controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. CustomerIO.initialize( config: CustomerIOConfig( // other config options pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehavior: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); The available options are: activityPreventRestart (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. activityNoFlags: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. resetTaskStack: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links Before you can follow this process, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Set up Universal Links Follow Flutter’s documentation to implement Universal Links in your app. --- ## Handling multiple push providers URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/multiple-push-providers/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If you use another push service alongside our SDK (like FlutterFire), then that other service takes over push handling by default and prevents your app from receiving rich push notifications from Customer.io. There are two ways to solve this problem, but we typically recommend the first option, because it’s more flexible and lets you process notifications through another service. The second option causes our SDK to take over push handling entirely. Option 1 (Recommended): Let Customer.io process notification payloads You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the onMessageReceived and onBackgroundMessageReceived methods for this purpose. A true value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A false value means that the SDK will only process the notification to track metrics but will not generate a notification on the device. App in foreground App in foreground CustomerIO.messagingPush().onMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); App in background App in background CustomerIO.messagingPush().onBackgroundMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); Imagine that you use FlutterFire (Firebase for Flutter) alongside our SDK. You might use the onMessageReceived and onBackgroundMessageReceived methods to handle notifications like this: FlutterFire foreground example FlutterFire foreground example FirebaseMessaging.onMessage.listen((RemoteMessage message) { CustomerIO.messagingPush().onMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); }); FlutterFire background example FlutterFire background example // Annotation is required only for Flutter version 3.3.0 or higher (to prevent removal during tree shaking in release mode) @pragma('vm:entry-point') Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); CustomerIO.messagingPush().onBackgroundMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); } void main() async { // Initialize required SDKs FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // Run the app } Option 2: Register Customer.io Messaging Service If you add another push service along with our SDK, like FlutterFire (Firebase for Flutter), it will take over push notification handling and prevent your app from receiving rich push notifications from Customer.io. To fix this issue, you need to add the following code under the <application> tag in the Manifest.xml file in your app’s Android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  This method causes the Customer.io SDK to handle all your push notifications If you use the code above: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture push metrics URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/push-metrics/ If you've already set up rich push capabilities with the Flutter SDK, you're ready to go. For more advanced use cases, see this document. Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. The SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. You don’t have to add any code to track opened push metrics or launch deep links.  Improve delivery metric reliability Configure App Groups to make sure delivery metrics aren’t lost when iOS terminates the Notification Service Extension before the tracking request completes. With App Groups, the SDK automatically recovers any undelivered metrics on the next app launch.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Read the sections below to see how you can add (optional) custom handling to various push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, add the following highlighted line of code in your AppDelegate.swift file: func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .showPushAppInForeground(true) // `true` will display the push when app in foreground .build() ) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like when you want to process custom fields in your push notification payload. For now, the Flutter SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { // Handle push notification that was clicked, when app was in the killed state }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { // Handle push notification that was clicked, when app was in the background state });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the Flutter SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives a callback using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.onMessage.listen((RemoteMessage message) { // Handle push notification received while app in foreground }); Manually track push metrics You can manually parse a push notification payload and send opened or delivered events to the SDK: // extracted from payload received in the push notification. const deliveryID = '123'; // extracted from payload received in the push notification const deviceToken = 'abc'; // or MetricEvent.delivered const event = MetricEvent.opened; CustomerIO.instance.trackMetric( deliveryID: deliveryID, deviceToken: deviceToken, event: event); --- ## Android channels URL: https://docs.customer.io/integrations/sdk/flutter/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v2.4.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io Flutter SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io Flutter SDK prior to 2.4.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io Flutter SDK v2.4.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v2.4.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/flutter/in-app-messages/in-app/ This page describes how to implement mobile in-app messages. How it works An in-app message is a message that people see in your app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the titles of your screens and which screen a person is on, so we can display in-app messages on the correct pages/screens in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open your app|d g-->|yes|h[user doesn't get your message] Set up in-app messaging In-app messages are disabled by default. Just pass the inAppConfig parameter in your CustomerIOConfig and add your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials. 1 2 3 4 5 6 7 8 9 10 11 import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, inAppConfig: InAppConfig(siteId: 'siteId') ), ); Page rules When you send an in-app message, you can set Page Rules determining the page(s) where your audience can see your message. Before you can take advantage of page rules, you need to: Track screens in your app. See Screen Events for help sending screen events. Provide screen titles to whoever sets up in-app messages in Customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message! Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the title in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/flutter/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific widget in your app. You create a placeholder InlineInAppMessageView in your UI, and the SDK fills it with the content of your message. Inline messages let you show dynamic content without releasing a new version of your app. Unlike push notifications, banners, or modal in-app messages, an inline message looks and feels like part of your interface. 1. Add View to your app UI to support inline messages Add InlineInAppMessageView anywhere you want to display inline messages. The widget expands or contracts automatically when a message loads or when people interact with it.  See our sample apps for real-world implementations. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_widgets.dart'; class InlineExample extends StatelessWidget { const InlineExample({super.key}); @override Widget build(BuildContext context) { return InlineInAppMessageView( elementId: 'inline', // Use this ID in Customer.io when you build your message. onActionClick: ( InAppMessage message, String actionValue, String actionName, ) { // Handle button taps or other actions. debugPrint( 'Inline message action clicked: $actionName with value: $actionValue', ); }, ); } } View layout Avoid hard-coding a height. The widget manages its own height as messages load and change. You control layout—padding, margins, alignment—just like any other widget. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set Display to Inline. Enter the Element ID that matches the elementId you set in your widget. (Optional) If you send multiple messages to the same Element ID, set a Priority so we know which message to show first. Then design and send your message! Handling custom actions When you configure an in-app message, you decide what happens when someone taps a button or the message itself. For deep links, the SDK opens the link automatically. For other interactions—like showing a settings page—you can listen for action events and run your own code. 1. Compose a message with a custom action In Customer.io, add an action to your in-app message, choose Custom Action, and set the action Name and Value. The Name maps to actionName; the Value maps to actionValue in your callback. 2. Listen for events You have two ways to detect clicks in inline messages. Callback on the widget – Pass onActionClick (shown above) to handle actions for that specific inline view. Global listener – Subscribe to CustomerIO.inAppMessaging.subscribeToEventsListener to handle inline and modal message events in one place. final subscription = CustomerIO.inAppMessaging.subscribeToEventsListener( (InAppEvent event) { if (event.eventType == EventType.messageActionTaken) { // Perform your logic here. debugPrint('Action taken: ${event.actionName} / ${event.actionValue}'); } }, ); // Later, when you no longer need events subscription.cancel(); Handle responses to messages (event listeners) Just like modal messages, inline messages emit events you can react to: messageShown – The message appeared. errorWithMessage – We encountered an error rendering the message. messageActionTaken – Someone tapped an action. This only triggers if the inline view does not have an onActionClick callback. Inline messages have no dismiss concept, so there is no messageDismissed event. --- ## Notification inbox URL: https://docs.customer.io/integrations/sdk/flutter/in-app-messages/inbox/ When you use Customer.io to send in-app messages, you can send messages to a notification inbox that your audience can access at their leisure. This page helps you understand how inbox features work so you can build your inbox and handle incoming messages. How it works Unlike other messages, inbox messages don’t necessarily appear immediately to users, and they don’t disappear when the user dismisses them. Instead, you’ll display these messages through a notification inbox that your audience can access at their leisure. Customer.io delivers inbox messages as JSON payloads, not fully-rendered messages. The SDK helps you listen for these payloads, but you’ll determine how to display them in your own inbox client. You can send an inbox message as a part of a campaign, broadcast, or transactional message. Get the inbox instance You’ll access inbox functionality through the inbox property on the in-app messaging module. final inbox = CustomerIO.inAppMessaging.inbox; Inbox methods The inbox instance provides several methods to manage messages. Method Description fetchMessages({String? topic}) Fetch messages from the inbox. Optionally filter by topic. Returns a Future with the list of messages. messages({String? topic}) Stream messages from the inbox for real-time updates. Optionally filter by topic. Returns a Stream of message lists. markMessageOpened(message) Mark a message as opened. markMessageUnopened(message) Mark a message as unopened. markMessageDeleted(message) Mark a message as deleted. trackMessageClicked(message, {String? actionName}) Track a click on the message. The actionName parameter is optional. Inbox message payloads Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client. The client payload includes the following fields, but you’re most concerned with the properties object, which represents your message content. By default, we’ll send a title and body field, but you can add other fields like an image or a link—whatever you set up your inbox to expect. Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages. Field Type Description messageId string Unique identifier for the message. sentAt string When the message was sent. expiresAt string When the message will expire. opened boolean Whether the message has been opened. topics array The topics that the message belongs to. type string The type of message. properties object The properties of the message. { "messageId": "1234567890", "sentAt": "2026-02-05T12:00:00Z", "expiresAt": "2026-02-05T12:00:00Z", "opened": false, "topics": ["orders", "shipping"], "type": "order_shipped", "properties": { "title": "Hey Cool Person, your order shipped!", "body": "You can track your order #1234567890 here:", "link": "https://example.com/orders/1234567890" } } Inbox topics and types When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox. Messages also have a type. Think of this like a sub-category or topic for a message. For example, you might have orders and sale topics, where orders don’t have images but sale topics might. Or, within the orders topic, you might have order_placed and order_shipped types, where order_placed lists order details and images of purchased products and order_shipped provides a link to the tracking information for the order that opens in a new tab. Setup your notification inbox Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want. Fetch messages // Fetch all messages final messages = await inbox.fetchMessages(); // Fetch messages filtered by topic final promotions = await inbox.fetchMessages(topic: 'promotions'); Stream messages for real-time updates // Stream all messages (real-time updates) inbox.messages().listen((messages) { // Update your UI with the messages updateInboxUI(messages); }); // Stream messages filtered by topic inbox.messages(topic: 'promotions').listen((messages) { // Update your UI with promotions updatePromotionsUI(messages); }); Mark messages as opened or unopened // Mark a message as opened inbox.markMessageOpened(message); // Mark a message as unopened inbox.markMessageUnopened(message); Track message clicks // Track a click without an action name inbox.trackMessageClicked(message); // Track a click with an action name inbox.trackMessageClicked(message, actionName: 'view_details'); Delete messages // Mark a message as deleted inbox.markMessageDeleted(message); Working with message properties You can access message properties to display custom content in your inbox: // Access message properties final title = message.properties['title'] as String?; final body = message.properties['body'] as String?; final link = message.properties['link'] as String?; final imageUrl = message.properties['image'] as String?; // Handle message action when user taps void handleMessageTap(InboxMessage message) { // Mark as opened inbox.markMessageOpened(message); // Track the click inbox.trackMessageClicked(message); // Open link if available final link = message.properties['link'] as String?; if (link != null) { launchUrl(Uri.parse(link)); } } --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/flutter/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. // subscribe to stream StreamSubscription subscription = CustomerIO.inAppMessaging.subscribeToEventsListener((InAppEvent event) { // cases for each event.eventType switch (event.eventType) { case EventType.messageShown: print("messageShown: ${event.message}"); break; case EventType.messageDismissed: print("messageDismissed: ${event.message}"); break; case EventType.errorWithMessage: print("errorWithMessage: ${event.message}"); break; case EventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. print("messageActionTaken: ${event.message}"); break; } }); // to unsubscribe from the event listener subscription.cancel(); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## 3.x -> 4.0.0 URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/4.0.0-upgrade/ Version 4.0.0 of the Customer.io Flutter SDK adds Swift Package Manager (SPM) support for iOS. What changed? Version 4.0.0 of the Flutter SDK adds Swift Package Manager (SPM) support as an alternative iOS dependency manager. SPM is Apple’s built-in package manager with native Xcode integration, so you don’t need to manage a separate Podfile for Customer.io dependencies. When SPM is enabled, the SDK is automatically resolved - no manual Xcode package setup needed for the main app. CocoaPods continues to work. If you want to stay on CocoaPods, you don’t need to change anything. Upgrade process Update to version 4.0.0 or later of the Customer.io Flutter SDK in your pubspec.yaml: dependencies: customer_io: ^4.0.0 If you use CocoaPods (no changes needed) If you want to continue using CocoaPods, you don’t need to change anything. Your existing Podfile will continue to work. Just update the SDK version above and run pod install --repo-update --project-directory=ios. If you want to switch to SPM Requirements Flutter 3.24+ Xcode 16.0+ firebase_core: ^3.5.0 or higher (we recommend at least ^4.6.0) firebase_messaging: ^15.2.0 or higher (we recommend at least ^16.1.3) Older versions of firebase_core and firebase_messaging don’t support SPM and cause module conflicts. Steps Update Firebase dependencies in your pubspec.yaml to SPM-compatible versions: dependencies: firebase_core: ^3.5.0 # we recommend at least ^4.6.0 firebase_messaging: ^15.2.0 # we recommend at least ^16.1.3 Enable SPM (if not already enabled). If you haven’t enabled Swift Package Manager in your Flutter project, you can do so globally or per-project: Global: Run flutter config --enable-swift-package-manager Per-project: Add the following to your pubspec.yaml: flutter: config: enable-swift-package-manager: true Remove CIO pod lines and the NSE target from your Podfile. Remove any customer_io pod lines and the NotificationServiceExtension target block: pod 'customer_io/fcm', :path => ... pod 'customer_io/location', :path => ... pod 'customer_io_richpush/fcm', :path => ... With SPM enabled, Flutter’s pod helper automatically skips SPM-handled plugins. The core SDK, in-app messaging, and push notifications modules (DataPipelines, MessagingInApp, MessagingPushFCM) are automatically included via SPM - no additional setup needed.  Using the Location module? If you use the Location module, make sure customerio_location_enabled=true is set in your android/gradle.properties. This flag controls Location inclusion on both Android and iOS when using SPM. If the flag can’t be read (e.g. in add-to-app modules or some CI setups), you can use the CIO_LOCATION environment variable as a fallback: CIO_LOCATION=true flutter build ios. See Location tracking for details. Remove the old CocoaPods framework from the NSE target in Xcode. Go to your NotificationServiceExtension target > General > Frameworks, Libraries, and Embedded Content and remove Pods_NotificationServiceExtension.framework. Add the SPM framework to the NSE target. In the same Frameworks and Libraries section, click + and add FlutterGeneratedPluginSwiftPackage. Add import UserNotifications to the top of your NotificationService.swift: import CioMessagingPushFCM import UserNotifications SPM doesn’t transitively import system frameworks like CocoaPods does, so you must add the import. Clean and rebuild: cd ios && rm -rf Pods Podfile.lock cd .. && flutter clean && flutter pub get flutter build ios Troubleshooting If you run into issues during migration, see SPM troubleshooting for solutions to common problems like module conflicts, deployment target errors, and CI build failures. --- ## 3.x -> 3.5.0 URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/3.5.0-upgrade/ Version 3.5.0 of the Customer.io Flutter SDK adds App Groups support for more reliable push delivery metric tracking on iOS. This update is additive—existing integrations work without modification. What changed? Version 3.5.0 adds support for App Groups, which improves the reliability of push delivery metric tracking on iOS. This update is additive—your existing integration continues to work without changes. However, to take advantage of App Groups, you’ll need to update your Xcode project configuration and regenerate provisioning profiles if you use manual signing. Why app groups? When you send a push notification, the SDK tracks delivery metrics in your Notification Service Extension (NSE). However, iOS can end the NSE before the tracking request completes, which means some delivery data may never reach Customer.io. App Groups provide shared storage between your main app and the NSE, so the SDK can save metrics and recover them on the next app launch. Upgrade process Update to version 3.5.0 or later of the Customer.io Flutter SDK. Follow the App Groups setup instructions to configure your Xcode project and pass .appGroupId() to the SDK in your native Swift code. No other code changes are needed. --- ## 2.x -> 3.0.0 URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/3.x-upgrade/ Version 3.x of the Customer.io Flutter SDK introduces Firebase wrapper support for FCM users that improves Firebase compatibility and simplifies push notification setup. What changed? Version 3.x introduces a Firebase wrapper that improves compatibility with Firebase Cloud Messaging (FCM). What needs to change? You need to add the CioFirebaseWrapper import to your Swift files that use CioMessagingPushFCM. Add the Firebase wrapper import to your AppDelegate.swift file: import UIKit import Flutter import CioMessagingPushFCM import CioFirebaseWrapper // Add this import for FCM users import FirebaseMessaging import FirebaseCore @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} Troubleshooting If you see build errors related to Firebase after upgrading: Clean your build: Run cd ios && pod install && cd .. then rebuild Check imports: Ensure you’ve added import CioFirebaseWrapper to all files that import CioMessagingPushFCM --- ## 2.x -> 2.2 URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/2.2-upgrade/ Version 2.2 of the Customer.io Flutter SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 2.2 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your Flutter app’s iOS configuration. Upgrading to SDK 2.2 Update your dependencies: Update your pubspec.yaml file: dependencies: customer_io: ^2.2.0 Run dependency update: flutter pub get && cd ios && pod install --repo-update && cd .. Update with FCM (Firebase Cloud Messaging) Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (2.x) Before (2.x) import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) Messaging.messaging().delegate = self MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().setAPNSToken(deviceToken, type: .unknown); } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } After (2.2) After (2.2) import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Initialize push with wrapper - handles all push methods automatically MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) // Optional: Add only if you want custom control over notifications UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) Messaging.messaging().apnsToken = deviceToken } // No manual push methods needed - CioAppDelegateWrapper handles everything } Configuration Options You can customize the push behavior when you initialize your push package: MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .showPushAppInForeground(true) // Show push when app is in foreground .autoTrackPushEvents(true) // Automatically track push metrics .build() ) Important Notes Manual push handling methods are not required: the CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError All other push-related delegate methods The @main attribute - Must be on the wrapper class, not your AppDelegate. Flutter-specific: Your Flutter app’s main.dart file doesn’t need any changes. NotificationService.swift remains unchanged - Your notification service extension configuration works the same with both approaches. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushFCM.initialize() Run flutter clean followed by pod install --repo-update --project-directory=ios Test on a physical device (push notifications don’t work on simulators) --- ## Upgrade to Flutter 2.x URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/2.x-upgrade/ This page provides steps to help you upgrade from our Flutter 1.x SDK so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: You’ll use SDK methods from an instance: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select Flutter. Enter a Name for your integration, like “My Flutter App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Test your connection and click Complete Setup. Or, if you don’t want to test your implementation yet, Save & Complete Later and then click Install Source to finish the setup process. In this case, Complete Later simply means that we haven’t seen any data from your Flutter app yet. Remember, you can also connect your Flutter app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Update the SDK initialization You’ll need to update the way you initialize the SDK—with a new key and configuration options for in-app and push. We show an example configuration below. You’ll find a complete list of configuration options on the Packages and Configuration Options page, but you’ll want to pay close attention to the following changes: You’ll initialize the SDK with a cdpApiKey. This is the key you’ll get when you create your Flutter integration in Customer.io. siteId becomes migrationSiteId. Your inAppConfig changes and requires your siteId. The optional pushConfig is now a part of your initialization call. You won’t set push settings as a part of a separate configuration like you did with the 1.x SDK. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', migrationSiteId: 'migrationSiteId', region: Region.us, autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: 'siteId'), // pushConfig is optional if you use default settings pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehaviorAndroid: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); 3. Update your podfile (iOS) For iOS, you’ll need to update your podfile with the correct iOS dependencies. Update your Runner and NotificationServiceExtension targets with the code below. target 'Runner' do pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios' end target 'NotificationServiceExtension' do pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios' end 4. Update your push notification handler In the previous version of the SDK, you had to initialize the SDK itself and the MessagingPushFCM package. You no longer need to initialize the SDK in your notification handler. You’ll also notice that all the configuration settings for push are a part of the SDK initialization itself. You won’t set push settings in your notification handler anymore. import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Depending on how you install Firebase, // you may need to add functions to this file, like: // FirebaseApp.configure() // // Read the official Firebase docs to install Firebase correctly! Messaging.messaging().delegate = self MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) // This Sets a 3rd party push event handler for the app—rather than the Customer.io SDK and FlutterFire. // Setting the AppDelegate as the handler will internally use `flutter_local_notifications` to handle push events. UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().setAPNSToken(deviceToken, type: .unknown); } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } 5. Update your API calls You’ll now make your calls from .instance methods. There are also a few crucial differences between the 1.x API and the 2.x API: userId replaces identifier in your identify call. attributes has changed to traits in identify, setProfileAttributes, and setDeviceAttributes methods. properties replaces attributes in track, screen, and any other eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. calls. Identify Identify //new call CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); //old call CustomerIO.identify(identifier: email, attributes: { "name": user.displayName, "email": user.email, "age": user.age, }); Track Track //new call CustomerIO.instance.track(name: "Movie Watched", properties: { "movie_name": "The Incredibles", "watch_time_in_minutes": 102, }); //old call CustomerIO.track(name: "Movie Watched", attributes: { "movie_name": "The Incredibles", "watch_time_in_minutes": 102, }); Screen Screen //new call CustomerIO.instance.screen(title: "Settings", properties: { "source": "Dashboard", "is_logged_in": true, }); //old call CustomerIO.screen(name: "Settings", attributes: { "source": "Dashboard", "is_logged_in": true, }); Configuration Changes As a part of this release, we’ve changed a few CustomerIOConfig configuration options when you initialize the SDK. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data to Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). inAppConfig object Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. pushConfig object Optional push configuration settings directly in the CustomerIOConfig. For now, it only takes the android.PushClickBehavior setting. If you don’t set a pushConfig, we’ll use default push settings. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/flutter/whats-new/changelog/ Check out the release history for the Flutter SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Quick Start Guide URL: https://docs.customer.io/integrations/sdk/flutter/2.x/quick-start-guide/ Before you can take advantage of our SDK, you need to install and initialize the SDK.  Our MCP server can help you get started Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See Set up Customer.io MCP to get started. Setup process overview Flutter lets you build native mobile apps using Dart. Our Flutter SDK helps you integrate Customer.io to identify people, track their activity, and send both push notifications and in-app messages. Install and initialize the SDK. Identify and Track Push Notifications In-App 1. Install the SDK In your project folder install the customer_io package: flutter pub add customer_io This adds a line to your package’s pubspec.yaml dependencies: customer_io: ^4.0.1 Set up your project to support iOS and/or Android: iOS iOS In your terminal, run pod install --repo-update --project-directory=ios. This adds the required iOS dependencies to your project. When the process is complete, you’ll see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Android Android Go to the Android subfolder and include google-services-plugin by adding the following lines to the project-level android/build.gradle file: buildscript { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } dependencies { // Add this line: classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } } Add the following line to android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and copy the file to android/app/google-services.json. Initialize the SDK: Add your CDP API key and site ID to your configuration. CDP API Key: You’ll find this key in your Flutter connection. Site ID: You’ll find this value in your workspace under Settings > Workspace Settings > API and webhook credentials. Initialize the SDK in your app. In your main.dart file—or wherever you want to initialize the CustomerIO plugin—add the code below: import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. inAppConfig: InAppConfig(siteId: 'siteId'), ), ); Run your application to ensure everything is set up correctly. 2. Identify and Track Identify a user in your app using the CustomerIO.identify method. You must identify a user before you can send push notifications and personalized in-app messages. CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); Track a custom event using the CustomerIO.track method. Events help you trigger personalized campaigns and track user activity. CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"}); Track screen views to trigger in-app messages associated with specific screens. CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"}); 3. Push Notifications iOS iOS Set up your push notification credentials in Customer.io: Upload your Firebase Cloud Messaging server key (.json format). Our Flutter SDK uses FCM for both iOS and Android push notifications. Request push notification permissions from the user. You can do this through Firebase or any other package. To ensure that metrics are tracked, configure Background Modes. In Xcode, enable “Remote notifications” under Capabilities > Background Modes. Android Android Set up your push notification credentials in Customer.io: Upload your Firebase Cloud Messaging server key (.json format). Request push notification permissions from the user. You can do this through Firebase or any other package. Ensure that you: Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android. Our Flutter SDK receives push notifications from FCM. Add notification icon resources: Place a notification icon file named ic_notification.png in your drawable folders. Make sure your app’s AndroidManifest.xml has the proper FCM permissions. 4. In-App To enable in-app messaging, all you need to do is add your site ID. Remember, you’ll find your site ID under Integrations > Customer.io API: Track in the Connections tab. Ensure that the SDK is initialized with the site ID in your app. You can call the initialize method from your components or services: import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. inAppConfig: InAppConfig(siteId: 'siteId'), pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehavior: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); --- ## How it works URL: https://docs.customer.io/integrations/sdk/flutter/2.x/getting-started/how-it-works/ Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations. Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A-->>B: Anonymous User activity B-->>C:   A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO C-->>C: Associate anonymous activity with identified user A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A-->>B: Anonymous user activity Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io. When someone logs into your app, you’ll send an identify call to Customer.io. This makes the person eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Your app is a data source and Customer.io is a destination Our SDK is a data inAn integration that feeds data into Customer.io. integration. It routes data from your app to both Customer.io and any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code. When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace and destinations outside of Customer.io. Minimum support requirements To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. --- ## Authentication URL: https://docs.customer.io/integrations/sdk/flutter/2.x/getting-started/auth/ To use the SDK, you'll need to get two kinds of keys: A [*CDP API Key*](#get-your-cdp-api-key) to send data to Customer.io and a [*Site ID*](#get-your-site-id) that tells the SDK which workspace your messages come from. Get your CDP API Key You’ll use a CDP API Key to initialize the SDK and send data to Customer.io. You’ll get this key when you set up your mobile app as a data inAn integration that feeds data into Customer.io. integration in Customer.io. If you haven’t already set up your integration in Customer.io, you’ll need to do that first. Go to Integrations. Select your Flutter integration in the Overview tab. If you don’t see a Flutter integration, you’ll need to set it up. Go to Settings and find your CDP API Key. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should set the siteId that you used in previous versions as the migrationSiteId in your config. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your CDP API Key>', //migrationSiteId is required if you're updating from a previous version migrationSiteId: '<your siteId>', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), );  You’re not done yet You still need your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials to initialize the CioMessagingInApp package and to support people updating your app from a previous version of Customer.io SDK. See Get your Site ID below. Set up a new integration If you don’t already have a write key, you’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io. Go to Integrations and click Add Integration. Select Flutter. Enter a Name for your integration, like “My Flutter App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Test your connection and click Complete Setup. Or, if you don’t want to test your implementation yet, Save & Complete Later and then click Install Source to finish the setup process. In this case, Complete Later simply means that we haven’t seen any data from your Flutter app yet. Remember, you can also connect your Flutter app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. Get your Site ID You’ll use your Site ID to send in-app messages from your workspace. If you’re upgrading from a previous version, my can also set your Site ID as your migrationSiteId. This key is used to send remaining tasks to Customer.io when your audience updates your app. Go to and select Workspace Settings in the upper-right corner of the Customer.io app and go to API and Webhook Credentials. Copy the Site ID for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click Create Tracking API Key. You’ll use this key to initialize the inApp package. If you’re upgrading from a previous version, you’ll also use it as your migrationSiteId. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', //required if you're updating from a previous version migrationSiteId: '<your siteId>', region: Region.us, // Replace with Region.EU if your Customer.io account is in the EU. autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. --- ## Configuration Options URL: https://docs.customer.io/integrations/sdk/flutter/2.x/getting-started/packages-options/ The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use. You’ll call configuration options before you initialize the SDK with CustomerIOConfig. When you initialize the SDK, you can pass configuration options. In most cases, you’ll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: "YOUR_CDP_API_KEY", // Required migrationSiteId: "YOUR_SITE_ID", // Required to migrate from a previous version autoTrackDeviceAttributes: true, region: Region.us, logLevel: CioLogLevel.error ) ); Option Type Default Description cdpApiKey string Required: the key you'll use to initialize the SDK and send data to Customer.io region eu or us us Required if your account is in the EU region. migrationSiteId string Required if you're updating from 1.x: the credential for previous versions of the SDK. We use this key to send remaining tasks to Customer.io when your audience updates your app. autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc screenViewUse All or inApp all ScreenView.all (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger campaigns, and target in-app messages. ScreenView.InApp: Screen view events not sent to Customer.io. You’ll only use them to target in-app messages based on page rules. trackApplicationLifecycleEvents boolean true Set to false if you don’t want the app to send lifecycle events like Application Opened logLevel string error Sets the level of logs you can view from the SDK. Set to debug or info to see more logging output. inApp.siteId string Used to initialize the inApp package, and determines the workspace your in-app messages come from. push.android.pushClickBehavior string activityPreventRestart One of resetTaskStack, activityPreventRestart, activityNoFlags; determines how to handle push clicks. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/flutter/2.x/getting-started/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Troubleshooting issues with our MCP server Our MCP server includes an integration tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc. You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?” Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.” The tool will return detailed steps to help you find and troubleshoot problems. Capture logs Logs help us pinpoint the problem and find a solution. To capture logs, you should install Flutter DevTools if you haven’t already. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "919a7e12107bd03155f6", apiKey: "86344654754f1c48d32b", region: Region.us, //config options go here logLevel: CioLogLevel.debug ), ); Build and run your app on a physical device or emulator. Open the Logging view in your development application. If you use Android Studio, select View > Tool Windows > Logcat to see your logs. Filter for CIO in the top to find log messages specific to the Customer.io SDK. Export your log and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. NaN, infinite, or imaginary number values Customer.io doesn’t handle invalid JSON values in your payloads, like NaN, infinite, or imaginary number values. If you send these values in identify, track, screen, or similar calls, we’ll drop them and record errors. While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a NaN value, we’ll drop the NaN value, but the identify call succeeds with the other attribute. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Flutter SDK. Before you contact support, you might want to [update your iOS dependencies](/Page(/integrations/sdk/flutter/push/#update-ios-dependencies) to get the latest packages and see if that fixes the issue. You can also check out our latest iOS changes to see if we’ve already fixed the issue or check out open issues to see if you’re experiencing a known issue. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. Notifications not coming through when app is in background If your app does not receive push notifications when it’s in the background, check the following: Ensure that you have implemented the _firebaseMessagingBackgroundHandler as suggested by the Firebase Messaging documentation. This handler is responsible for processing messages received while the app is in the background or closed. Verify that the handler is set correctly and receiving callbacks. Double-check the implementation to ensure it’s properly registered within your app’s code. Confirm the Flutter version you are using. For Flutter version 3.3.0 or higher, you will need to add @pragma('vm:entry-point') to the handler function for it to work correctly. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/flutter/2.x/tracking/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: userId (Required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc). traits (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the identify() function, you can update a person’s attributes in Customer.io. If you’ve already identified a person, and they update their preferences, provide additional information about themselves, or perform other attribute-changing actions, you can update their attributes with profileAttributes. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute {"first_name": "Dana"}, and then you call CustomerIO.instance.setProfileAttributes(traits: {"favorite_food": "pizza"});, the person will gain the favorite_food attribute but first_name attribute will still be Dana. CustomerIO.instance.setProfileAttributes(traits: { "first_name": "Cool", "last_name": "User", "is_premium": false, }); Device attributes By default (if you don’t set .autoTrackDeviceAttributes(false) in your config), the SDK automatically collects a series of attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. for each device. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. Along with these attributes, we automatically set a last_used timestamp for each device indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a person’s devices and each device’s attributes when you go to Journeys > People > Select a person, and click Devices.  Your integration shows device attributes in the context object When you inspect calls from the SDK (in your integration’s data inAn integration that feeds data into Customer.io. tab), you’ll see device information in the context object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in segmentsA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static.. For example, context.network.cellular becomes network_cellular. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person more broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const deviceAttributes = { "type" : "primary_device", "parentObject" : { "childProperty" : "someValue", }, }; CustomerIO.instance.setDeviceAttributes(attributes: deviceAttributes); Disable automatic device attribute collection By default, the SDK automatically collects the device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. defined above. You can disable the autoTrackDeviceAttributes setting to prevent the SDK from automatically collecting these attributes. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', autoTrackDeviceAttributes: false, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call CustomerIO.instance.registerDeviceToken(token). Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.instance.clearIdentify(); Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.instance.identify(identifier: "new.person@example.com", attributes: {"first_name": "New", "last_name": "Person"}); --- ## Mobile Lifecycle events URL: https://docs.customer.io/integrations/sdk/flutter/2.x/tracking/lifecycle-events/ By default, the Customer.io SDK automatically tracks lifecycle events for your users. These are events that represent the lifecycle of your app and your users' experiences with it. By default, we track the following lifecycle events: Application Installed: A user installed your app. Application Updated: A user updated your app. Application Opened: A user opened your app. Application Foregrounded: A user switched back to your app. Application Backgrounded: A user backgrounded your app or switched to another app. You might also want to send your own lifecycle events, like Application Crashed or Application Updated. You can do this using the track call. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our Mobile App Lifecycle Event specification. Lifecycle event examples A lifecycle event is basically a track call that the SDK makes automatically for you. When you look at your data in Customer.io, you’ll see lifecycle events as track calls, where the event properties are specific to the name of the event. For example, the Application Installed event includes the app version and build properties. { "userId": "app.installer@example.com", "type": "track", "event": "Application Installed", "properties": { "version": "3.2.1", "build": "247" } } Sending custom lifecycle events You can send your own lifecycle events using the track call. However, whenever you send lifecycle events, you should use the Application EventName convention that we use for our default lifecycle events. These semantic event names and properties represent a standard that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to. If you opt out of automatic lifecycle events, you can send your own track calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a track call for Application Crashed if your app crashes or Application Updated when people update your app. CustomerIO.instance.track( name: "Application Crashed", properties: [ "url": "urls://page/in/app" ] ) Disable lifecycle events We track lifecycle events by default. You can disable this behavior by passing the trackApplicationLifecycleEvents option in your configuration. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: '<your API Key>', region: Region.us, trackApplicationLifecycleEvents: false, inAppConfig: InAppConfig(siteId: '<your siteId>'), ), ); --- ## Anonymous activity URL: https://docs.customer.io/integrations/sdk/flutter/2.x/tracking/anonymous-activity/ Before you identify a person, calls you make to the SDK are associated with an `anonymousId`. When you identify that person, we reconcile their anonymous activity with the identified person. In Customer.io, you’ll see anonymous activity in the Activity Log, but we don’t surface anonymous profilesAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. in Customer.io. You won’t be able to find an “anonymous person” in your workspace, and an anonymous person can’t trigger campaigns or get messages (including push notifications) from Customer.io. When you identify a person, and we merge anonymous activity with the identified person, the previously-anonymous activity can trigger campaigns and cause your audience to receive messages. For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity with their identified profile, and their previously-anonymous screen view triggers the campaign you set up for people who visited the product page. You can return a person’s anonymous ID at ay time by calling CustomerIO.shared.anonymousId. flowchart LR a(Anonymous user opens app) a-->|track calls|z subgraph z [Anonymous activity] direction LR u(anonymous page view) y(anonymous event) end subgraph f [User profile] direction LR g(screen view) h(event) end z-->|User logs in: Ientify call merges events to profile|f f-->i{Did events happen in past 72 hours?} i-->|yes|j(Events trigger campaigns) i-.->|no|k(Events do not trigger campaigns) --- ## Screen tracking URL: https://docs.customer.io/integrations/sdk/flutter/2.x/tracking/screen-events/ Screen events track the screens people view in your app. They help you track the parts of your app your users engage with and are vital in determining where you display in-app messages. Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a title representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your they use. Screen events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using Route observer for automatic screen tracking. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> { void _sendScreenView(PageRoute<dynamic> route) { var screenName = route.settings.name; // track screen manually CustomerIO.screen(name: screenName ?? "N/A"); } @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPush(route, previousRoute); if (route is PageRoute) { _sendScreenView(route); } } @override void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); if (newRoute is PageRoute) { _sendScreenView(newRoute); } } @override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPop(route, previousRoute); if (previousRoute is PageRoute && route is PageRoute) { _sendScreenView(previousRoute); } } } // Usage class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(), navigatorObservers: [MyRouteObserver()], home: Screen1(), routes: { 'screen2': (context) => Screen2(), 'screen3': (context) => Screen3(), }, ); } } Screenview settings for in-app messages Customer.io uses screen events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends screen events to Customer.io’s backend servers. But, if you don’t use screen events to track user activity, segment your audience, or to trigger campaigns, these events might constitute unnecessary traffic and event history. If you don’t use screen events for anything other than in-app notifications, you can set the ScreenViewUse parameter to ScreenView.inApp. This setting stops the SDK from sending screen events back to Customer.io but still allows the SDK to use screen events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io! CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: "YOUR_CDP_API_KEY", // Required screenViewUse: ScreenView.inApp, ), ); Send your own screen events Screen events use the .screen method. Like other event types, you can add a properties object containing additional information about the screen or the user. CustomerIO.instance.screen(title: "screen-name", properties: {"property": "value"}); --- ## Track events URL: https://docs.customer.io/integrations/sdk/flutter/2.x/tracking/track-events/ Events are things people do in your app. They help you track your audience's behaviors, activity, and metrics. You can use them to segment your audience, trigger messaging campaigns, and see how people use your app. Track an event The track method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event properties—information about the person or the event that they performed. In Customer.io, you can use events to trigger campaigns and broadcasts. Those campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name (Required): The name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. properties (Optional): Additional information that you want to reference in messages or use to segment your audience, etc. You can reference event properties in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<properties>}}. CustomerIO.instance.track(name: "add-to-cart", properties: {"product": "shoes", "price": "29.99"}); Anonymous activity If you send a track call before you identify a person, we’ll attribute the event to an anonymousId. When you identify the person, we’ll reconcile their anonymous activity with the identified person. When we apply anonymous events to an identified person, the previously anonymous activity becomes eligible to trigger campaigns in Customer.io. Semantic Events Some actions don’t map cleanly to our simple identify, track, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. These are especially important in Customer.io for destructive operations like deleting a person. When you send an event with a semantic event name, we’ll perform the appropriate action. For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a Delete Person event. CustomerIO.instance.track(name: "User Deleted) --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/flutter/2.x/push-notifications/push-setup/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. This page helps you add push support to your app. How it works If you’ve followed our getting started instructions, you’re already set up to send push notifications to your Android audience. But you’ll need to add a bit of code to support your iOS users. Remember that a device/user can’t receive a push notification until you: (iOS) Register a device token for the device; code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. (Both iOS and Android) Check for notification permissions. If your app user doesn’t grant permission, notifications will not appear in the system tray. (Optional) Set up your app to report push metrics back to Customer.io.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Firebase Cloud Messaging (FCM). Set up push for Android If you followed our Getting Started instructions, you’re already set up to send push notifications to Android devices. You just need to set up iOS push support in your app. Next, you can set up deep links if you want your notifications to link into your app. Set or change your push icon You’ll set the icon that appears on push notifications as a part of the AndroidManifest.xml file in your app’s android folder. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Set up push for iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your Flutter project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project. Under Targets, select your main app. Click the Signing & Capabilities tab and click Capability. Add Push Notifications to your app. Select File > New > Target. Select Notification Service Extension and click Next. You should see a window such as this: You can leave many of the options in this window as their defaults, but you should: Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) Confirm that your main app is selected in the Embed in Application drop-down menu When you’re done, click Finish. When presented with the dialog below, click Cancel. This will help Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Open the file ios/Podfile and make the following modifications: target 'Runner' do # Look for the main app target. # Required by FCM push notification service use_frameworks! # Make all file modifications after these lines: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios' end  Do you need to fetch updates? See our update guide for full instructions on how to update the Flutter SDK, including how to update the Podfile. Run pod install --repo-update --project-directory=ios from the root directory of your Flutter project. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Update your AppDelegate.swift file to handle push notifications. import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Depending on how you install Firebase, // you may need to add functions to this file, like: // FirebaseApp.configure() // // Read the official Firebase docs to install Firebase correctly! MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) // Add line below only if you want to have custom control over notifications being presented and processed - Customer.io will handle those automatically UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) Messaging.messaging().apnsToken = deviceToken } } Open your NotificationService.swift file in XCode and modify it with the highlighted changes below: import CioMessagingPushFCM class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { MessagingPushFCM.initializeForExtension( withConfig: MessagingPushConfigBuilder(cdpApiKey: "YourCdpApiKey") // Optional: set your Customer.io account region (.US or .EU). Default: US .region(.US) .build() ) MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Now you can run your app on a physical device and send yourself push notifications with images and deep links to test your implementation. You’ll have to use a physical device because emulators can’t receive push notifications. Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep links URL: https://docs.customer.io/integrations/sdk/flutter/2.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: Set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. Visit Flutter’s documentation for more info on deep linking. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "myapp://home" --> <data android:host="home" android:scheme="myapp" /> </intent-filter> After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior (Android) When you initialize the SDK, you can set a pushConfig that controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. CustomerIO.initialize( config: CustomerIOConfig( // other config options pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehavior: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); The available options are: activityPreventRestart (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. activityNoFlags: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. resetTaskStack: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links Before you can follow this process, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Set up Universal Links Follow Flutter’s documentation to implement Universal Links in your app. --- ## Handling multiple push providers URL: https://docs.customer.io/integrations/sdk/flutter/2.x/push-notifications/multiple-push-providers/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If you use another push service alongside our SDK (like FlutterFire), then that other service takes over push handling by default and prevents your app from receiving rich push notifications from Customer.io. There are two ways to solve this problem, but we typically recommend the first option, because it’s more flexible and lets you process notifications through another service. The second option causes our SDK to take over push handling entirely. Option 1 (Recommended): Let Customer.io process notification payloads You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the onMessageReceived and onBackgroundMessageReceived methods for this purpose. A true value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A false value means that the SDK will only process the notification to track metrics but will not generate a notification on the device. App in foreground App in foreground CustomerIO.messagingPush().onMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); App in background App in background CustomerIO.messagingPush().onBackgroundMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); Imagine that you use FlutterFire (Firebase for Flutter) alongside our SDK. You might use the onMessageReceived and onBackgroundMessageReceived methods to handle notifications like this: FlutterFire foreground example FlutterFire foreground example FirebaseMessaging.onMessage.listen((RemoteMessage message) { CustomerIO.messagingPush().onMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); }); FlutterFire background example FlutterFire background example // Annotation is required only for Flutter version 3.3.0 or higher (to prevent removal during tree shaking in release mode) @pragma('vm:entry-point') Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); CustomerIO.messagingPush().onBackgroundMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); } void main() async { // Initialize required SDKs FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // Run the app } Option 2: Register Customer.io Messaging Service If you add another push service along with our SDK, like FlutterFire (Firebase for Flutter), it will take over push notification handling and prevent your app from receiving rich push notifications from Customer.io. To fix this issue, you need to add the following code under the <application> tag in the Manifest.xml file in your app’s Android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  This method causes the Customer.io SDK to handle all your push notifications If you use the code above: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture push metrics URL: https://docs.customer.io/integrations/sdk/flutter/2.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the Flutter SDK, you're ready to go. For more advanced use cases, see this document. Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. The SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. You don’t have to add any code to track opened push metrics or launch deep links.  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Read the sections below to see how you can add (optional) custom handling to various push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, add the following highlighted line of code in your AppDelegate.swift file: func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .showPushAppInForeground(true) // `true` will display the push when app in foreground .build() ) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like when you want to process custom fields in your push notification payload. For now, the Flutter SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { // Handle push notification that was clicked, when app was in the killed state }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { // Handle push notification that was clicked, when app was in the background state });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the Flutter SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives a callback using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.onMessage.listen((RemoteMessage message) { // Handle push notification received while app in foreground }); Manually track push metrics You can manually parse a push notification payload and send opened or delivered events to the SDK: // extracted from payload received in the push notification. const deliveryID = '123'; // extracted from payload received in the push notification const deviceToken = 'abc'; // or MetricEvent.delivered const event = MetricEvent.opened; CustomerIO.instance.trackMetric( deliveryID: deliveryID, deviceToken: deviceToken, event: event); --- ## Android channels URL: https://docs.customer.io/integrations/sdk/flutter/2.x/push-notifications/push-notification-channel/ Learn how to customize your Android push notification channels in your app's manifest. 🎉New in v2.4.0 Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about. Today, Customer.io supports a single channel per app, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. Learn more from the official Android developer docs. Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings. Channel setting Default Description Channel ID [your package name] The ID of the channel. Channel name [your app name] Notifications The name of the channel. Importance 3 The importance of the channel. Acceptable values are 0 (min), 1 (low), 2 (medium), 3 (default/high), and 4 (urgent). See the Android developer documentation for more about the behavior of each importance level. Channel configuration When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect. You’ll customize your channel in your app’s manifest. <manifest> <application> <meta-data android:name="io.customer.notification_channel_id" android:value="channel_id_value" /> <meta-data android:name="io.customer.notification_channel_name" android:value="Channel Name" /> <meta-data android:name="io.customer.notification_channel_importance" android:value="4" /> </application> </manifest> What channel settings can I change? When you first set up the Customer.io Flutter SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. (This is a limitation imposed by Android, not Customer.io.) If you released your app with a version of the Customer.io Flutter SDK prior to 2.4.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. The chart below shows what channel settings you can or can’t change: flowchart TD a{Is this a new integration with Customer.io?} a-->|yes|b{Are you migrating channels from another platform?} a-->|no|c{Were you integrated with Customer.io Flutter SDK v2.4.0 or earlier?} c-->|yes|d(You can delete your current channel and customize a new one.) b-->|no|e(You can customize your channel) b-->|yes|f(You can set your channel name. You cannot change your channel ID or importance.) c-->|no|f Delete a channel If you’ve released a version of your app with the Customer.io SDK earlier than v2.4.0, you can delete your old channel and create a new one with completely new settings per Android’s developer documentation. val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val id: String = context.packageName notificationManager.deleteNotificationChannel(id) --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/flutter/2.x/in-app-messages/in-app/ This page describes how to implement mobile in-app messages. How it works An in-app message is a message that people see in your app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the titles of your screens and which screen a person is on, so we can display in-app messages on the correct pages/screens in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open your app|d g-->|yes|h[user doesn't get your message] Set up in-app messaging In-app messages are disabled by default. Just pass the inAppConfig parameter in your CustomerIOConfig and add your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials. 1 2 3 4 5 6 7 8 9 10 11 import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', region: Region.us, inAppConfig: InAppConfig(siteId: 'siteId') ), ); Page rules When you send an in-app message, you can set Page Rules determining the page(s) where your audience can see your message. Before you can take advantage of page rules, you need to: Track screens in your app. See Screen Events for help sending screen events. Provide screen titles to whoever sets up in-app messages in Customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message! Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the title in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## Inline in-app messages URL: https://docs.customer.io/integrations/sdk/flutter/2.x/in-app-messages/inline-in-app/ Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates. How it works An inline message targets a specific widget in your app. You create a placeholder InlineInAppMessageView in your UI, and the SDK fills it with the content of your message. Inline messages let you show dynamic content without releasing a new version of your app. Unlike push notifications, banners, or modal in-app messages, an inline message looks and feels like part of your interface. 1. Add View to your app UI to support inline messages Add InlineInAppMessageView anywhere you want to display inline messages. The widget expands or contracts automatically when a message loads or when people interact with it.  See our sample apps for real-world implementations. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_widgets.dart'; class InlineExample extends StatelessWidget { const InlineExample({super.key}); @override Widget build(BuildContext context) { return InlineInAppMessageView( elementId: 'inline', // Use this ID in Customer.io when you build your message. onActionClick: ( InAppMessage message, String actionValue, String actionName, ) { // Handle button taps or other actions. debugPrint( 'Inline message action clicked: $actionName with value: $actionValue', ); }, ); } } View layout Avoid hard-coding a height. The widget manages its own height as messages load and change. You control layout—padding, margins, alignment—just like any other widget. 2. Build and send your message When you add an in-app message to a broadcast or campaign in Customer.io: Set Display to Inline. Enter the Element ID that matches the elementId you set in your widget. (Optional) If you send multiple messages to the same Element ID, set a Priority so we know which message to show first. Then design and send your message! Handling custom actions When you configure an in-app message, you decide what happens when someone taps a button or the message itself. For deep links, the SDK opens the link automatically. For other interactions—like showing a settings page—you can listen for action events and run your own code. 1. Compose a message with a custom action In Customer.io, add an action to your in-app message, choose Custom Action, and set the action Name and Value. The Name maps to actionName; the Value maps to actionValue in your callback. 2. Listen for events You have two ways to detect clicks in inline messages. Callback on the widget – Pass onActionClick (shown above) to handle actions for that specific inline view. Global listener – Subscribe to CustomerIO.inAppMessaging.subscribeToEventsListener to handle inline and modal message events in one place. final subscription = CustomerIO.inAppMessaging.subscribeToEventsListener( (InAppEvent event) { if (event.eventType == EventType.messageActionTaken) { // Perform your logic here. debugPrint('Action taken: ${event.actionName} / ${event.actionValue}'); } }, ); // Later, when you no longer need events subscription.cancel(); Handle responses to messages (event listeners) Just like modal messages, inline messages emit events you can react to: messageShown – The message appeared. errorWithMessage – We encountered an error rendering the message. messageActionTaken – Someone tapped an action. This only triggers if the inline view does not have an onActionClick callback. Inline messages have no dismiss concept, so there is no messageDismissed event. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/flutter/2.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. After you initialize the SDK, you can register an event listener to subscribe to in-app events. In the code below, event is an instance of InAppMessageEvent containing details about the in-app message, e.g. messageId, deliveryId. // subscribe to stream StreamSubscription subscription = CustomerIO.inAppMessaging.subscribeToEventsListener((InAppEvent event) { // cases for each event.eventType switch (event.eventType) { case EventType.messageShown: print("messageShown: ${event.message}"); break; case EventType.messageDismissed: print("messageDismissed: ${event.message}"); break; case EventType.errorWithMessage: print("errorWithMessage: ${event.message}"); break; case EventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. print("messageActionTaken: ${event.message}"); break; } }); // to unsubscribe from the event listener subscription.cancel(); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.inAppMessaging.dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## 2.x -> 2.2 URL: https://docs.customer.io/integrations/sdk/flutter/2.x/whats-new/2.2-upgrade/ Version 2.2 of the Customer.io Flutter SDK introduces a new `CioAppDelegateWrapper` pattern for iOS that simplifies push notification setup and eliminates the need for method swizzling. Key Changes The primary change in version 2.2 is the introduction of the wrapper pattern for handling push notifications on iOS. This change: Eliminates method swizzling: No more automatic method replacement Simplifies setup: Less boilerplate code required Improves reliability: More predictable behavior See the instructions below to update your Flutter app’s iOS configuration. Upgrading to SDK 2.2 Update your dependencies: Update your pubspec.yaml file: dependencies: customer_io: ^2.2.0 Run dependency update: flutter pub get && cd ios && pod install --repo-update && cd .. Update with FCM (Firebase Cloud Messaging) Update your AppDelegate.swift file to use the new CioAppDelegateWrapper pattern. See the Before sample to see what needs to change and the After sample to see the new pattern. Before (2.x) Before (2.x) import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) Messaging.messaging().delegate = self MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().setAPNSToken(deviceToken, type: .unknown); } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } After (2.2) After (2.2) import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {} class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Initialize push with wrapper - handles all push methods automatically MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) // Optional: Add only if you want custom control over notifications UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) Messaging.messaging().apnsToken = deviceToken } // No manual push methods needed - CioAppDelegateWrapper handles everything } Configuration Options You can customize the push behavior when you initialize your push package: MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .showPushAppInForeground(true) // Show push when app is in foreground .autoTrackPushEvents(true) // Automatically track push metrics .build() ) Important Notes Manual push handling methods are not required: the CioAppDelegateWrapper automatically records information from following methods. But you can still use these methods if you want to add custom push handling: didRegisterForRemoteNotificationsWithDeviceToken didFailToRegisterForRemoteNotificationsWithError All other push-related delegate methods The @main attribute - Must be on the wrapper class, not your AppDelegate. Flutter-specific: Your Flutter app’s main.dart file doesn’t need any changes. NotificationService.swift remains unchanged - Your notification service extension configuration works the same with both approaches. Troubleshooting If push notifications stop working after you update your implementation: Make sure that you’ve added the @main attribute to the wrapper class Verify that you’ve removed @main from your original AppDelegate Check that you’re calling MessagingPushFCM.initialize() Run flutter clean followed by pod install --repo-update --project-directory=ios Test on a physical device (push notifications don’t work on simulators) --- ## Upgrade to Flutter 2.x URL: https://docs.customer.io/integrations/sdk/flutter/2.x/whats-new/2.x-upgrade/ This page provides steps to help you upgrade from our Flutter 1.x SDK so you understand the development effort required to update your app and take advantage of the latest features. What changed? This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same. This move also adds two additional features: You’ll use SDK methods from an instance: Support for anonymous tracking: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a person when you identify them. Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you. New device-level data: the SDK captures the device name and other device-level context for you. Upgrade process You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules. As a part of this process, your credentials change. You’ll need to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io and get a new CDP API Key. But you’ll also need to keep your previous siteId as a migrationSiteId when you initialize the SDK. The migrationSiteId is a key helps the SDK send remaining traffic when people update your app. When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, identifier becomes userId, body becomes traits, and data becomes properties. 1. Get your new CDP API Key The new version of the SDK requires you to set up a new data inAn integration that feeds data into Customer.io. integration in Customer.io. As a part of this process, you’ll get your CDP API Key. Go to Integrations and click Add Integration. Select Flutter. Enter a Name for your integration, like “My Flutter App”. We’ll present you with a code sample containing a cdpApiKey that you’ll use to initialize the SDK. Copy this key and keep it handy. Test your connection and click Complete Setup. Or, if you don’t want to test your implementation yet, Save & Complete Later and then click Install Source to finish the setup process. In this case, Complete Later simply means that we haven’t seen any data from your Flutter app yet. Remember, you can also connect your Flutter app to services outside of Customer.io—like your analytics provider, data warehouse, or CRM. 2. Update the SDK initialization You’ll need to update the way you initialize the SDK—with a new key and configuration options for in-app and push. We show an example configuration below. You’ll find a complete list of configuration options on the Packages and Configuration Options page, but you’ll want to pay close attention to the following changes: You’ll initialize the SDK with a cdpApiKey. This is the key you’ll get when you create your Flutter integration in Customer.io. siteId becomes migrationSiteId. Your inAppConfig changes and requires your siteId. The optional pushConfig is now a part of your initialization call. You won’t set push settings as a part of a separate configuration like you did with the 1.x SDK. CustomerIO.initialize( config: CustomerIOConfig( cdpApiKey: 'cdpApiKey', migrationSiteId: 'migrationSiteId', region: Region.us, autoTrackDeviceAttributes: true, inAppConfig: InAppConfig(siteId: 'siteId'), // pushConfig is optional if you use default settings pushConfig: PushConfig( android: PushConfigAndroid( pushClickBehaviorAndroid: PushClickBehaviorAndroid.activityPreventRestart, ), ), ), ); 3. Update your podfile (iOS) For iOS, you’ll need to update your podfile with the correct iOS dependencies. Update your Runner and NotificationServiceExtension targets with the code below. target 'Runner' do pod 'customer_io/fcm', :path => '.symlinks/plugins/customer_io/ios' end target 'NotificationServiceExtension' do pod 'customer_io_richpush/fcm', :path => '.symlinks/plugins/customer_io/ios' end 4. Update your push notification handler In the previous version of the SDK, you had to initialize the SDK itself and the MessagingPushFCM package. You no longer need to initialize the SDK in your notification handler. You’ll also notice that all the configuration settings for push are a part of the SDK initialization itself. You won’t set push settings in your notification handler anymore. import UIKit import Flutter import CioMessagingPushFCM import FirebaseMessaging import FirebaseCore @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) // Depending on how you install Firebase, // you may need to add functions to this file, like: // FirebaseApp.configure() // // Read the official Firebase docs to install Firebase correctly! Messaging.messaging().delegate = self MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() .build() ) // This Sets a 3rd party push event handler for the app—rather than the Customer.io SDK and FlutterFire. // Setting the AppDelegate as the handler will internally use `flutter_local_notifications` to handle push events. UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().setAPNSToken(deviceToken, type: .unknown); } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } 5. Update your API calls You’ll now make your calls from .instance methods. There are also a few crucial differences between the 1.x API and the 2.x API: userId replaces identifier in your identify call. attributes has changed to traits in identify, setProfileAttributes, and setDeviceAttributes methods. properties replaces attributes in track, screen, and any other eventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. calls. Identify Identify //new call CustomerIO.instance.identify(userId: email, traits: { "name": user.displayName, "email": user.email, "age": user.age, }); //old call CustomerIO.identify(identifier: email, attributes: { "name": user.displayName, "email": user.email, "age": user.age, }); Track Track //new call CustomerIO.instance.track(name: "Movie Watched", properties: { "movie_name": "The Incredibles", "watch_time_in_minutes": 102, }); //old call CustomerIO.track(name: "Movie Watched", attributes: { "movie_name": "The Incredibles", "watch_time_in_minutes": 102, }); Screen Screen //new call CustomerIO.instance.screen(title: "Settings", properties: { "source": "Dashboard", "is_logged_in": true, }); //old call CustomerIO.screen(name: "Settings", attributes: { "source": "Dashboard", "is_logged_in": true, }); Configuration Changes As a part of this release, we’ve changed a few CustomerIOConfig configuration options when you initialize the SDK. The following table shows the changes to the configuration options. Field Type Default Description cdpApiKey string Replaces apiKey; required to initialize the SDK and send data to Customer.io. migrationSiteId string Replaces siteId; required if you’re updating from 2.x. This is the key representing your previous version of the SDK. trackApplicationLifeCycleEvents boolean true When true, the SDK automatically tracks application lifecycle events (like Application Installed). inAppConfig object Replaces the former enableInApp option, providing a place to set in-app configuration options. For now, it takes a single property called siteId. pushConfig object Optional push configuration settings directly in the CustomerIOConfig. For now, it only takes the android.PushClickBehavior setting. If you don’t set a pushConfig, we’ll use default push settings. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/flutter/2.x/whats-new/changelog/ Check out the release history for the Flutter SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Get Started URL: https://docs.customer.io/integrations/sdk/flutter/1.x/getting-started/ Before you can take advantage of our SDK, you need to install and initialize the SDK. This page also explains how the SDK prioritizes operations. This page is part of an introductory series to help you get started with the essential features of our SDK. The highlighted step(s) below are covered on this page. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style getting-started fill:#B5FFEF,stroke:#007069 style B fill:#B5FFEF,stroke:#007069 How it works Our SDKs provide a ready-made integration to identify people who use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io. sequenceDiagram participant A as Mobile User participant B as SDK participant C as Customer.io A--xB: User activity user not identified A->>B: Logs in (identify method) rect rgb(229, 254, 249) Note over A,C: Now you can Send events and receive messages B-->>C: Person added/updated in CIO A->>B: User activity (track event) B->>C: Event triggers campaign C->>B: Campaign triggered push B->>A: Display push A->>B: Logs out (clearIdentify method) end A--xB: No longer sending events or receiving messages You must identify a person before you can take advantage of most SDK features. You can send anonymous in-app messages in our latest updates, but you can’t send push notifications or capture event activity for anonymous devices/users. That means that you can’t track or respond to anything your audience does in your app until you identify them. In Customer.io, you identify people by id or email, which typically means that you need someone to log in to your app or service before you can identify them. While someone is “identified”, you can send events representing their activity in your app to Customer.io. You can also send the identified person messages from Customer.io. You send messages to a person through the Customer.io campaign builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered campaign to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload. Prerequisites Because our Flutter package relies on our native iOS and Android modules, you’ll need to make sure that you’re set up to support both iOS and Android in your environment. To support the Customer.io SDK, you must: Use Gradle 8.0 or later. Use Android Gradle plugin version 8.0 or later (8.2+ recommended). Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features). Set iOS 13 or later as your minimum deployment target in XCode Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33). Have an iOS 13+ device to test your implementation. You cannot test push notifications in a simulator. Before you begin: set up your development environment Before you get started, you’ll need to do the following things to support iOS and Android respectively: iOS Setup XCode (using deployment target to 13.0 or later). Make sure that you have XCode command line tools installed xcode-select --install. Get your Apple Push Certificate and enable push notifications for iOS in your Customer.io account. Have an iOS 13 to test with. You cannot test push notifications in an emulator. Android Download and install Android Studio Add your Google Firebase Cloud Messaging (FCM) key to Customer.io and enable push notifications for Android Make sure you use an appropriate version of the Android Gradle plugin. Have an Android device or emulator with Google Play Services enabled and a minimum OS version. Install the Flutter SDK This process involves setup for both iOS and Android. For Android, the directions below will guide you through the setup process for both Firebase Cloud Messaging (FCM) and our in-app messaging module; both are required to use the Flutter SDK.  Want to update to the latest version of the Customer.io Flutter SDK? See our update guide. Open your terminal and go to your project folder: cd <Root/path/to/your/app> Add our Flutter SDK package from the command line. flutter pub add customer_io This adds a line to your package’s pubspec.yaml dependencies: customer_io: ^4.0.1 In your terminal, run pod install --repo-update --project-directory=ios. This adds the required iOS dependencies to your project. When the process is complete, you’ll see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Make sure that your minimum deployment target is set to 13.0. You’ll have to do this in two places: Go to the ios subfolder and open your Podfile. Find the platform:ios line, and make sure that the version is set to 13.0 or later if it isn’t already. Open your project’s iOS directory in XCode, select the project under Targets, and set the Minimum Deployments target to 13.0 or later. Go to the Android subfolder and include google-services-plugin by adding the following lines to the project-level android/build.gradle file: buildscript { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } dependencies { // Add this line: classpath 'com.google.gms:google-services:<version-here>' // Google Services plugin } } allprojects { repositories { // Add this line if it isn't already in your build file: google() // Google's Maven repository } } Add the following line to android/app/build.gradle: apply plugin: 'com.google.gms.google-services' // Google Services plugin Download google-services.json from your Firebase project and copy the file to android/app/google-services.json. Now you’re ready to initialize the SDK and use it in your app. Initialize and configure the SDK After you install the SDK, you’ll need to initialize it in your app. In your main.dart file—or wherever you want to initialize the customer_io plugin—add this code. You’ll need Track API credentials to initialize the SDK—your Site IDEquivalent to the user name you’ll use to interface with the Journeys Track API; also used with our JavaScript snippets. You can find your Site ID under Workspace Settings > API Credentials and API KeyEquivalent to the password you’ll use with a Site ID to interface with the Journeys Track API. You can generate new keys under Workspace Settings > API Credentials, which you can find in Customer.io under Settings > Workspace Settings > API Credentials. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "YOUR_SITE_ID", apiKey: "YOUR_API_KEY", region: Region.us // Region is optional, defaults to Region.US. // Use Region.EU for EU-based workspaces. ), ); This makes the SDK available to use in your app. Note that you’ll still need to identify your app’s users before you can send them messages. Configure the SDK You can determine global behaviors for the SDK in the CustomerIOConfig object. You must provide configuration options before you initialize the SDK; you cannot declare configuration changes after you initialize the SDK. Import CustomerioConfig and then set configuration options to configure things like your logging level and whether or not you want to automatically track device attributes, etc. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "YOUR_SITE_ID", apiKey: "YOUR_API_KEY", region: Region.us, //config options go here autoTrackDeviceAttributes: true, enableInApp: true, logLevel: "debug" ), ); When you initialize the SDK, you can pass configuration options. In most cases, you'll want to stick with the defaults, but you might do things like change the logLevel when testing updates to your app. Option Type Default Description autoTrackDeviceAttributes boolean true Automatically gathers information about devices, like operating system, device locale, model, app version, etc autoTrackPushEvents boolean true The SDK automatically generates delivered and opened metrics for push notifications sent from Customer.io autoScreenViewBody strings When autoTrackScreenViews is true, use this to override the the body of automatic screen view events. See automatic screen tracking for more information. backgroundQueueMinNumberOfTasks integer 10 See the processing queue for more information. This sets the number of tasks that enter the processing queue before sending requests to Customer.io. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. backgroundQueueSecondsDelay integer 30 See the processing queue for more information. The number of seconds after a task is added to the processing queue before the queue executes. In general, we recommend that you don't change this setting, because it can impact your audience's battery life. enableInApp boolean false Enables in-app messaging. See in-app messaging for more details. logLevel string error Sets the level of logs you can view from the SDK. Set to debug to see more logging output. Securing your credentials To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security. To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers. The Processing Queue The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests. The queue holds requests until any one of the following criteria is met: There are 20 or more tasks in the queue. 30 seconds have passed since the SDK performed its last task. The app is closed and re-opened. For example, when you identify a new person in your app using the SDK, you won’t see the created/updated person immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated person in your workspace. How the queue organizes tasks The SDK typically runs tasks in the order that they were called—unless one of the tasks in the queue fails. Tasks in the queue are grouped by “type” because some tasks need to run sequentially. For example, you can’t invoke a track call if an identify call hasn’t succeeded first. So, if a task fails, the SDK chooses the next task in the queue depending on whether or not the failed task is the first task in a group. If the failed task is the first in a group: the SDK skips the remaining tasks in the group, and moves to the next task outside the group. If the failed task is 1+n task in a group: the SDK skips the failed task and moves on to the next task in the group.** The following chart shows how the SDK would process a queue where tasks A, B, and C belong to the same group. flowchart TD a["Task inventory [A, B, C], D"]-->b{Is task A successful} b-.->|Yes|c[Continue to task B] b-.->|No|d[Skip to task D] c-.->|Whether task B succeeds or fails|E[Continue to task C] Using the SDK as a data source The SDK uses our Legacy Track API API, but it can also double as a source of data for other integrations without additional development work. To do this, we translate calls from the SDK to our newer Data Pipelines API format before we send them to your destinations. In general, we recommend that you upgrade your app to use a newer version of the SDK. Our newer versions rely on the Data Pipelines API, so you can take advantage of your mobile data without without us translating it for you. It can make it easier to trace data from your app to your destinations and troubleshoot issues as they arise. Our newer SDKs also support more features, like anonymous tracking. --- ## Identify people URL: https://docs.customer.io/integrations/sdk/flutter/1.x/identify/ Use `CustomerIO.identify()` to identify a person. You need to identify a mobile user before you can send them messages or track events for things they do in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't identify people before you initialize the SDK! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style identify fill:#B5FFEF,stroke:#007069 Identify a person Identifying a person: Adds or updates the person in your workspace. This is basically the same as an identify call to our server-side API. Saves the person’s information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person. Associates the current device token with the the person. You can only identify one customer at a time. The SDK “remembers” the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK “forgets” person A and assumes that person B is the current app user. You can also stop identifying a person, which you might do when someone logs off or stops using your app for a significant period of time. An identify request takes two parameters: identifier (required): The unique value representing a person—an ID, email address, or the cio_idAn identifier for a person that is automatically generated by Customer.io and cannot be changed. This identifier provides a complete, unbroken record of a person across changes to their other identifiers (id, email, etc).. attributes (Optional): An object containing attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. that you want to add to, or update on, a person. import 'package:customer_io/customer_io.dart'; // Call this method whenever you are ready to identify a user CustomerIO.identify(identifier: "person@example.com", attributes: {"first_name": "Dana"}); Update a person’s attributes You store information about a person in Customer.io as attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. When you call the CustomerIO.identify() function, you can update a person’s attributes on the server-side. You can update a person’s attributes after you identify them using setProfileAttributes—like when a person updates their preferences or provides additional information about themselves that you want to store in Customer.io. You only need to pass the attributes that you want to create or modify to setProfileAttributes. For example, if you identify a new person with the attribute {"first_name": "Dana"}, and then you call CustomerIO.setProfileAttributes(attributes: {"favorite_food": "pizza"}); after that, the person’s first_name attribute will still be Dana. const profileAttributes = { "favouriteFood": "Pizza", "favouriteDrink": "Mango Shake", }; CustomerIO.setProfileAttributes(attributes: profileAttributes); Device attributes When you register a device token to a person, we automatically collect device attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. You can use these attributes in segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. and other campaign workflow conditions to target the device owner, just like you would use a person’s other attributes. You cannot, however, use device attributes to personalize messages with 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}}. yet. For each device, we automatically collect the device platform attribute. Within your workspace, we also automatically set a last_used timestamp indicating when the device owner was last identified, and the last_status of a push notification you sent to the device. By default, we also automatically capture a series of attributes, like the device’s operating system, model, push_enabled preference. You can add custom attributes to the attributes object. id string Required The device token. Set custom device attributes You can also set custom device attributes with the setDeviceAttributes method. You might do this to save app preferences, time zone, or other custom values specific to the device. Like profile attributes, you can pass nested JSON to device attributes. However, before you set custom device attributes, consider whether the attribute is specific to the device or if it applies to the person more broadly. Device tokens are ephemeral—they can change based on user behavior, like when a person uninstalls and reinstalls your app. If you want an attribute to persist beyond the life of the device, you should apply it to the person rather than the device. const deviceAttributes = { "type" : "primary_device", "parentObject" : { "childProperty" : "someValue", }, }; CustomerIO.setDeviceAttributes(attributes: deviceAttributes); Manually add device to profile In the standard flow, identifying a person automatically associates the token with the identified person in your workspace. If you need to manually add or update the device elsewhere in your code, call the method CustomerIO.registerDeviceToken(token). const registerDevice = () => { // Customer.io expects a valid token to send push notifications // to the user. const token = 'token' CustomerIO.registerDeviceToken(token) } Stop identifying a person When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them. Use clearIdentify() to stop identifying the previously identified person (if there was one). CustomerIO.clearIdentify(); Identify a different person If you want to identify a new person—like when someone switches profiles on a streaming app, etc—you can simply call identify() for the new person. The new person then becomes the currently-identified person, with whom all new information—messages, events, etc—is associated. CustomerIO.identify(identifier: "new.person@example.com", attributes: {"first_name": "New", "last_name": "Person"}); --- ## Track events URL: https://docs.customer.io/integrations/sdk/flutter/1.x/track-events/ Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger campaigns, and capture usage metrics in your app. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't send events before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style track-events fill:#B5FFEF,stroke:#007069 Track a custom event After you identify a person, you can use the track method to send events representing their activities to Customer.io. When you send events, you can include event data—information about the person or the event that they performed. You can use events to trigger campaigns, add people to segments, etc. Your event-triggered campaigns might send someone a push notification or manipulate information associated with the person in your workspace. Events include the following: name: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team. A data object (Optional): Additional information that you might want to reference in messages or use to segment your audience, etc. You can reference data attributes in messages and other campaign actionsA block in a campaign workflow—like a message, delay, or attribute change. using 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}}. in the format {{event.<attribute>}}. CustomerIO.track(name: "add-to-cart", attributes: {"product": "shoes", "price": "29.99"}); Screen view events Screen views are events that record the pages that your audience visits in your app. They have a type property set to screen, and a name representing the title of the screen or page that a person visited in your app. Screen view events let you trigger campaignsCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria. or add people to segmentsA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently people used your app. Enable automatic screen tracking We’ve provided some example code below using Route observer for automatic screen tracking. If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you send screen events manually. class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> { void _sendScreenView(PageRoute<dynamic> route) { var screenName = route.settings.name; // track screen manually CustomerIO.screen(name: screenName ?? "N/A"); } @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPush(route, previousRoute); if (route is PageRoute) { _sendScreenView(route); } } @override void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); if (newRoute is PageRoute) { _sendScreenView(newRoute); } } @override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPop(route, previousRoute); if (previousRoute is PageRoute && route is PageRoute) { _sendScreenView(previousRoute); } } } // Usage class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(), navigatorObservers: [MyRouteObserver()], home: Screen1(), routes: { 'screen2': (context) => Screen2(), 'screen3': (context) => Screen3(), }, ); } } Send your own screen events Screen events use the .screen method. Like other event types, you can add a data object containing additional information about the event or the currently-identified person. CustomerIO.screen(name: "screen-name", attributes: {"property": "value"}); --- ## Update Flutter SDK URL: https://docs.customer.io/integrations/sdk/flutter/1.x/update/ This page explains how to update the Flutter SDK to the latest version. Assuming that you have followed the install instructions, this section covers the steps to take to successfully update the Customer.io Flutter SDK to the latest version. Inside of your pubspec.yaml, update the version of the Customer.io Flutter SDK (customer_io) to the latest version found here. If you followed the instructions for setting up push notifications on iOS and made modifications to your ios/Podfile, open your ios/Podfile file and make the following updates: # For every line of code that contains 'pod CustomerIO...', update the version number. # Here is an example. # Before: target 'App' do pod 'CustomerIO/MessagingPushFCM', '~> ...' end # After: target 'App' do # 2.14.2 is the latest version at the time of writing. # Install the latest version found here: https://github.com/customerio/customerio-ios/releases/latest pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' end # Note: You may need to update multiple lines of code in your `Podfile`, not just one. Important: After updating your ios/Podfile, run the command: pod update --repo-update --project-directory=ios from the root directory of your Flutter app. You should see a message Pod installation complete! when the update is finished. You have successfully updated the Customer.io Flutter SDK! Try to compile your app and see if you encounter any issues. If you do, please send our support team an email at win@customer.io. In your message, include the following files: ios/Podfile pubspec.yaml ios/Podfile.lock Also, copy the output of the pod update --repo-update --project-directory=ios command. --- ## Set up push notifications URL: https://docs.customer.io/integrations/sdk/flutter/1.x/push-notifications/push/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive push notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style push fill:#B5FFEF,stroke:#007069 How it works Under the hood, our Flutter SDK takes advantage of our native Android and iOS SDKs. This helps us keep the Flutter SDK up to date. But, for now, it also means you’ll need to add a bit of code to support your iOS users. For Android, you’re ready to go if you followed our getting started instructions. Before a device can receive a push notification, you must: (iOS) Register a device token for the device; code samples on this page help you do that. Identify a person. This associates a token with the person; you can’t send push notifications to a device until you identify the recipient. (for both iOS and Android) Check for notification permissions. If your app user doesn’t grant permission, notifications will not appear in the system tray. (Optional) Set up your app to report push metrics back to Customer.io.  Did you already set up your push providers? To send, test, and receive push notifications, you’ll need to set up your push notification service(s) in Customer.io. If you haven’t already, set up Firebase Cloud Messaging (FCM). Set up push on Android If you followed our Getting Started instructions, you’re already set up to send standard push notifications to Android devices. Next, you can set up deep links if you want your notifications to link into your app. You can also move on to set up iOS. Set or change your push icon You’ll set the icon that appears on normal push notifications as a part of the AndroidManifest.xml file in your app’s android folder. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest. <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorNotificationIcon" /> Set up push on iOS You’ll need to add some additional code to support push notifications for iOS. You’ll need to add push capabilities in XCode and integrate push capabilities in your app. Add push capabilities in Xcode Before you can work with push notifications, you need to add Push Notification capabilities to your project in XCode. In your Flutter project, go to the ios subfolder and open <yourAppName>.xcworkspace. Select your project. Under Targets, select your main app. Click the Signing & Capabilities tab. Click Capability. Add Push Notifications to your app. Select File > New > Target. Select Notification Service Extension and click Next. You should see a window such as this: You can leave many of the options in this window as their defaults, but you should: Enter a product name, like NotificationServiceExtension (which we use in our examples on this page) Confirm that your main app is selected in the Embed in Application drop-down menu After you have completed this, click Finish. When presented with the dialog below, click Cancel. This will help Xcode continue debugging your app and not just the extension you just added. Now you have another target in your project navigator named NotificationServiceExtension. We’ll configure this extension when we Integrate Push Notifications in the following section. Integrate push capabilities in your app Open the file ios/Podfile and make the following modifications: target 'YourApp' do # Look for the main app target. # Required by FCM push notification service use_frameworks! # Make all file modifications after these lines: config = use_native_modules! # Add the following line to add the Customer.io native dependency: pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' end # Next, copy and paste the code below to the bottom of your Podfile: target 'NotificationServiceExtension' do use_frameworks! pod 'CustomerIO/MessagingPushFCM', '~> 2.14.2' end  Want to automatically get the latest versions? The example above includes the full version number. If you remove the patch and/or minor version numbers, you’ll always get the latest minor release when you run pod update --repo-update --project-directory=ios. See Updating iOS Dependencies for information about updating your Podfile.  Are you trying to update the Customer.io SDK in the Podfile? See our update guide for full instructions on how to update the Flutter SDK, including how to update the Podfile. Run pod install --repo-update --project-directory=ios from the root directory of your Flutter project. When dependencies finish installing, you should see a message like this: Pod installation complete! There are X dependencies from the Podfile and Y total pods installed. Update your AppDelegate.swift file to handle push notifications. 🎉Updated in version 2.11 of the native iOS SDK.  Using an older version of the native iOS SDK in your app? As of version 2.11, the iOS SDK automatically handles push clicks. Follow our upgrade guide to remove unnecessary code and increase compatibility with 3rd party SDKs in your app. import Flutter import CioMessagingPushFCM import CioTracking import FirebaseMessaging import FirebaseCore @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) FirebaseApp.configure() // Set FCM messaging delegate Messaging.messaging().delegate = self // This line of code is required in order for the Customer.io SDK to handle push notification click events. // We are working on removing this requirement in a future release. // Remember to modify the siteId and apiKey with your own values. // let siteId = "YOUR SITE ID HERE" // let apiKey = "YOUR API KEY HERE" CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: Region.US) { config in // To confirm that `delivered` push metrics are tracked, set this to true. config.autoTrackPushEvents = true } // Initialize Customer.io push features after you initialize the SDK: MessagingPushFCM.initialize { config in // Automatically register push device tokens to the Customer.io SDK config.autoFetchDeviceToken = true // When your app is in the foreground and a push is delivered, show the push config.showPushAppInForeground = true } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().setAPNSToken(deviceToken, type: .unknown); } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { MessagingPush.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } } extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { MessagingPush.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) } } Look in Xcode for a file: NotificationService.swift. It should look similar to this: import UserNotifications class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { ... } override func serviceExtensionTimeWillExpire() { ... } } Modify this file to look like the following: import UserNotifications import CioMessagingPushFCM import CioTracking class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { // It's required that you initialize the Customer.io SDK in this file, even though you also did so in your app. CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: .US) { config in // To confirm that `delivered` push metrics are tracked, set this to true. config.autoTrackPushEvents = true } MessagingPush.shared.didReceive(request, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { MessagingPush.shared.serviceExtensionTimeWillExpire() } } Now you can run your app on a physical device and send yourself a push notification with images and deep links to test your implementation. You’ll have to use a physical device because simulators can’t receive push notifications. Sound in push notifications (iOS Only) When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. Test your implementation After you set up rich push, you should test your implementation. Below, we show the payload structure we use for iOS and Android. In general, you can use our regular rich push editor; it’s set up to send messages using the JSON structure we outline below. If you want to fashion your own payload, you can use our custom payload. iOS FCM payload iOS FCM payload { "message": { "apns": { "payload": { "aps": { // basic iOS message and options go here "mutable-content": 1, "alert": { "title": "string", //(optional) The title of the notification. "body": "string" //(optional) The message you want to send. } }, "CIO": { "push": { "link": "string", //generally a deep link, i.e. my-app://... or https://yourwebsite.com/... "image": "string" //HTTPS URL of your image, including file extension } } }, "headers": { // (optional) headers to send to the Apple Push Notification Service. "apns-priority": 10 } } } } message object Required The base object for all FCM payloads. apns object Required Defines a payload for iOS devices sent through Firebase Cloud Messaging (FCM). headers object Headers defined by Apple’s payload reference that you want to pass through FCM. payload object Required Contains a push payload. CIO object Contains properties interpreted by the Customer.io iOS SDK. push object Required A push payload for the iOS SDK. Custom key-value pairs* any type Additional properties that you've set up your app to interpret outside of the Customer.io SDK. Android payload Android payload { "message": { "data": { "title": "string", //(optional) The title of the notification. "body": "string", //The message you want to send. "image": "string", //https URL to an image you want to include in the notification "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world } } } message Required The parent object for all push payloads. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Required Contains all properties interpreted by the SDK. android object Contains properties that are not interpreted by the SDK but are defined by FCM. You need to write your own code to handle these Android push features. data object Contains the link property (interpreted by the SDK) and additional properties that you want to pass to your app. notification object Required Contains properties interpreted by the SDK except for the link. --- ## Deep Links URL: https://docs.customer.io/integrations/sdk/flutter/1.x/push-notifications/deep-links/ Deep links are links that send a person from push notifications to pages in your app. If you set a deep link when you send your push notification, users can tap the notification to go to the place you specify. How it works Deep links are the links that directs users to a specific location within a mobile app. When you set up your notification, you can set a “deep link.” When your audience taps the notification, the SDK will route users to the right place. Deep links help make your message meaningful, with a call to action that makes it easier, and more likely, for your audience to follow. For example, if you send a push notification about a sale, you can send a deep link that takes your audience directly to the sale page in your app. However, to make deep links work, you’ll have to handle them in your app. We’ve provided instructions below to handle deep links in both Android and iOS versions of your app. Android: Set up deep links Deep links provide a way to link to a screen in your app. You’ll set up deep links by adding intent filters to the AndroidManifest.xml file. Visit Flutter’s documentation for more info on deep linking. <intent-filter android:label="deep_linking_filter"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "amiapp://home" --> <data android:host="home" android:scheme="amiapp" /> </intent-filter> After you set up intent filters, you can test your implementation with the Rich Push editor or the payloads included for Testing push notifications. Push Click Behavior The pushClickBehaviorAndroid config controls how your app behaves when your audience taps push notifications on Android devices. The SDK automatically tracks Opened metrics for all options. The available options are: ACTIVITY_PREVENT_RESTART (Default): If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app is not in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen. ACTIVITY_NO_FLAGS: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were. RESET_TASK_STACK: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button. CustomerIO.initialize( config: CustomerIOConfig( // other config options pushClickBehaviorAndroid: PushClickBehaviorAndroid.ACTIVITY_PREVENT_RESTART ), ); iOS: Set up deep links Deep links let you open a specific page in your app instead of opening the device’s web browser. Want to open a screen in your app or perform an action when a push notification or in-app button is clicked? Deep links work great for this! Setup deep linking in your app. There are two ways to do this; you can do both if you want. Universal Links: universal links let you open your mobile app instead of a web browser when someone interacts with a URL on your website. For example: https://your-social-media-app.com/profile?username=dana—notice how this URL is the same format as a webpage. App scheme: app scheme deep links are quick and easy to setup. Example of an app scheme deep link: your-social-media-app://profile?username=dana. Notice how this URL is not a URL that could show a webpage if your mobile app is not installed. Universal Links provide a fallback for links if your audience doesn’t have your app installed, but they take longer to set up than App Scheme deep links. App Scheme links are easier to set up but won’t work if your audience doesn’t have your app installed. Setup App Scheme deep links However, before you can do this, you need to set up your app link scheme for iOS. Learn more about URL schemes for iOS apps. Open your project in Xcode and select your root project in the Project Navigator. Go to the Info tab. Scroll down to the options in the Info tab and expand URL Types. Click to add a new, untitled schema. Under Identifier and URL Schemes, add the name of your schema. Set up Universal Links Follow Flutter’s documentation to implement Universal Links in your app. --- ## Handling Multiple Push Providers URL: https://docs.customer.io/integrations/sdk/flutter/1.x/push-notifications/multiple-push-providers/ Our Flutter SDK supports push notifications over FCM—including rich push messages with links and images. Use this page to add support for your push provider and set your app up to receive push notifications. How to handle multiple push providers If you use another push service alongside our SDK (like FlutterFire), then that other service takes over push handling by default and prevents your app from receiving rich push notifications from Customer.io. There are two ways to solve this problem, but we typically recommend the first option, because it’s more flexible and lets you process notifications through another service. The second option causes our SDK to take over push handling entirely. Option 1 (Recommended): Let Customer.io process notification payloads You can pass the payloads of other message services to Customer.io whenever a device receives a notification, so our SDK can process it for you. The SDK exposes the onMessageReceived and onBackgroundMessageReceived methods for this purpose. A true value (the default) means that the Customer.io SDK will generate the notification and track associated metrics. A false value means that the SDK will only process the notification to track metrics but will not generate a notification on the device. App in foreground App in foreground CustomerIO.messagingPush().onMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); App in background App in background CustomerIO.messagingPush().onBackgroundMessageReceived(payload).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); Imagine that you use FlutterFire (Firebase for Flutter) alongside our SDK. You might use the onMessageReceived and onBackgroundMessageReceived methods to handle notifications like this: FlutterFire foreground example FlutterFire foreground example FirebaseMessaging.onMessage.listen((RemoteMessage message) { CustomerIO.messagingPush().onMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); }); FlutterFire background example FlutterFire background example // Annotation is required only for Flutter version 3.3.0 or higher (to prevent removal during tree shaking in release mode) @pragma('vm:entry-point') Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); CustomerIO.messagingPush().onBackgroundMessageReceived(message.toMap()).then((handled) { // handled is true if notification was handled by Customer.io SDK; false otherwise return handled; }); } void main() async { // Initialize required SDKs FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // Run the app } Option 2: Register Customer.io Messaging Service If you add another push service along with our SDK, like FlutterFire (Firebase for Flutter), it will take over push notification handling and prevent your app from receiving rich push notifications from Customer.io. To fix this issue, you need to add the following code under the <application> tag in the Manifest.xml file in your app’s Android folder. <service android:name="io.customer.messagingpush.CustomerIOFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>  This method causes the Customer.io SDK to handle all your push notifications If you use the code above: Your app will receive all simple and rich push notifications from Customer.io. When your app is in the background, it can receive push notifications with a notification payload from other services. Your app cannot receive data-only push notifications from another service. --- ## Capture Push Metrics URL: https://docs.customer.io/integrations/sdk/flutter/1.x/push-notifications/push-metrics/ If you've already set up rich push capabilities with the Flutter SDK, you're ready to go. For more advanced use cases, see this document.  Upgrade iOS SDK! Beginning in version 2.11 of the native iOS SDK, the SDK automatically handles push notifications from Customer.io and tracks opened and delivered metrics for you. We recommend that you upgrade to simplify your code! Automatic push handling Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: delivered when a push notification is received by the app and opened when a push notification is clicked. Beginning in version 2.11 of our native iOS SDK, the SDK automatically tracks opened and delivered events for push notifications originating from Customer.io after you configure your app to receive push notifications. No more code is required for your app to track opened push metrics or launch deep links!  Do you use multiple push services in your app? The Customer.io SDK only handles push notifications that originate from Customer.io. Push notifications that were sent from other push services or displayed locally on device are not handled by the Customer.io SDK. You must add custom handling logic to your app to handle those push events. Read the sections below to see how you can add (optional) custom handling to various push events. Choose whether to show push while your app is in the foreground If your app is in the foreground and the device receives a Customer.io push notification, your app gets to choose whether or not to display the push. To configure this behavior, add the following highlighted line of code in your AppDelegate.swift file: func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... MessagingPushFCM.initialize { config in config.showPushAppInForeground = true // `true` will display the push when app in foreground } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } If the push did not come from Customer.io, you’ll need to perform custom handling to determine whether to display the push or not. Custom handling when users click a push You might need to perform custom handling when a user clicks a push notification—like when you want to process custom fields in your push notification payload. For now, the Flutter SDK does not provide callbacks when your audience clicks a push notification. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives callbacks when users click a push using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { // Handle push notification that was clicked, when app was in the killed state }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { // Handle push notification that was clicked, when app was in the background state });  Do you use deep links? If you’re performing custom push click handling on push notifications originating from Customer.io, we recommend that you don’t launch a deep link URL yourself. Instead, let our SDK launch deep links to avoid unexpected behaviors. Custom handling when getting a push while the app is foregrounded If your app is in the foreground and you get a push notification, your app gets to choose whether or not to display the push. For push notifications originating from Customer.io, your SDK configuration determines if you show the notification. But you can add custom logic to your app when this kind of thing happens. For now, the Flutter SDK does not provide callbacks when a push notification is received and your app is in the foreground. But you can use one of the many popular Flutter push notification SDKs to receive a callback. For example, the code below receives a callback using FlutterFire. Be sure to follow the documentation for the push notification SDK you choose to use to receive callbacks with. import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; FirebaseMessaging.onMessage.listen((RemoteMessage message) { // Handle push notification received while app in foreground }); Manually track push metrics  Avoid duplicate push metrics If you manually track your own metrics, you should disable automatic push tracking to avoid duplicate push metrics. You can manually parse a push notification payload and send opened or delivered events to the SDK: const deliveryID = '123'; // extracted from payload received in the push notification. const deviceToken = 'abc'; // extracted from payload received in the push notification const event = MetricEvent.opened; // or MetricEvent.delivered CustomerIO.trackMetric( deliveryID: deliveryID, deviceToken: deviceToken, event: event); Disabling automatic push tracking After you set up push notifications in your app, modify your AppDelegate.swift file to disable automatic push notification tracking: func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { CustomerIO.initialize(siteId: "YourSiteID", apiKey: "YourAPIKey", region: Region.US) { config in config.autoTrackPushEvents = false } } --- ## In-app messages URL: https://docs.customer.io/integrations/sdk/flutter/1.x/in-app-messages/in-app/ This page describes how to implement mobile in-app messages. This page is part of a setup flow for the SDK. Before you continue, make sure you've implemented previous features—i.e. you can't receive in-app notifications before you identify people! graph LR getting-started(Install SDK) -->B(Initialize SDK) B --> identify(identify people) identify -.-> track-events(Send events) identify -.-> push(Receive push) identify -.-> in-app(Receive in-app) click getting-started href "/integrations/sdk/flutter/getting-started/#install" click B href "/integrations/sdk/flutter/getting-started/#initialize-the-sdk" click identify href "/integrations/sdk/flutter/identify" click track-events href "/integrations/sdk/flutter/track-events/" click register-token href "/integrations/sdk/flutter/push" click push href "/integrations/sdk/flutter/push" click rich-push href "/integrations/sdk/flutter/rich-push" click in-app href "/integrations/sdk/flutter/in-app" click test-support href "/integrations/sdk/flutter/test-support" style in-app fill:#B5FFEF,stroke:#007069 How it works An in-app message is a message that people see within the app. People won’t see your in-app messages until they open your app. If you set an expiry period for your message, and that time elapses before someone opens your app, they won’t see your message. You can also set page rules to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to use screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app. graph LR a[app user triggers in-app message]-->d{is the app open?} d-->|yes|f[user gets message] d-->|no|e[hold message until app opens] e-->g{did the message expire?} g-->|no, wait for user to open the app|d g-->|yes|h[user doesn't get the message] Set up in-app messaging In-app messages are disabled by default. Just set enableInApp to true in your CustomerioConfig(), and your app will be able to receive in-app messages. 1 2 3 4 5 6 7 8 9 10 11 12 import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "919a7e12107bd03155f6", apiKey: "86344654754f1c48d32b", region: Region.us, enableInApp: true, ), ); Page rules You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to: Track screens in your app. See the Track Events page for help sending screen events. Provide page names to whomever sets up in-app messages in fly.customer.io. If we don’t recognize the page that you set for a page rule, your audience will never see your message. Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the name in your screen events. If you’re targeting your website, your page rules should always be lowercase. --- ## In-app event listeners URL: https://docs.customer.io/integrations/sdk/flutter/1.x/in-app-messages/in-app-actions/ In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you'll need to handle the action yourself and dismiss the resulting message when you're done with it. How it works In-app messages often have a call to action. Most basic actions are handled automatically by the SDK. For example, if you set a call-to-action button to open a web page, the SDK will open the web page when the user taps the button. But you can also set up custom actions that require your app to handle the response. If you set up custom actions, you’ll need to handle the action yourself and dismiss the resulting message when you’re done with it. Handle responses to messages (event listeners) You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button. You can listen for four different events: messageShown: a message is “sent” and appears to a user messageDismissed: the user closes the message (by tapping an element that uses the close action) errorWithMessage: the message itself produces an error—this probably prevents the message from appearing to the user messageActionTaken: the user performs an action in the message. Once the SDK is initialized you can subscribe to in-app events. // subscribe to stream StreamSubscription subscription = CustomerIO.subscribeToInAppEventListener((InAppEvent event) { // complete cases for event.eventType switch (event.eventType) { case EventType.messageShown: print("messageShown: ${event.message}"); break; case EventType.messageDismissed: print("messageDismissed: ${event.message}"); break; case EventType.errorWithMessage: print("errorWithMessage: ${event.message}"); break; case EventType.messageActionTaken: // event.actionValue => The type of action that triggered the event. // event.actionName => The name of the action specified when building the in-app message. print("messageActionTaken: ${event.message}"); break; } }); // to unsubscribe from the event listener subscription.cancel(); Handling custom actions When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting. In these cases, you’ll want to use the messageActionTaken event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code. When you add an action to an in-app message in Customer.io, select Custom Action and set your Action’s Name and value. The Name corresponds to the actionName, and the value represents the actionValue in your event listener. Register an event listener for MessageActionTaken, and listen for the actionName or actionValue you set up in the previous step.  Use names and values exactly as entered We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your Custom Action. When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action. Dismiss in-app message You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions. CustomerIO.MessagingInApp().dismissMessage(); Deep links You can open deep links when a user clicks actions inside in-app messages. Setting up deep links for in-app messages is the same as setting up deep links for push notifications. --- ## Update iOS URL: https://docs.customer.io/integrations/sdk/flutter/1.x/updates-and-troubleshooting/migrate-upgrade/ This page explains how to upgrade the native iOS SDK to version 2.11. While these changes aren't breaking—you don't _need_ to make these changes—they will simplify your integration and improve the reliability of your metrics and deep link handling on iOS devices. Upgrade native iOS SDK to 2.11+ As of version 2.11, the native iOS Customer.io SDK automatically handles push clicks. These features simplify your SDK integration while improving the reliability of opened metrics tracking and increasing compatibility with other push modules you may have installed in your app. Follow our native iOS update guide to update the Flutter and iOS SDK to use the latest version. Open your AppDelegate file and review all of the highlighted code below in this sample. import Flutter import CioMessagingPushFCM import CioTracking import FirebaseMessaging import FirebaseCore import UserNotifications @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { ... // Delete this line of code: UNUserNotificationCenter.current().delegate = self return super.application(application, didFinishLaunchingWithOptions: launchOptions) } ... // Delete this function: override func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { // Call the Customer.io SDK to handle the push notification let handled = MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) // If the Customer.io SDK does not handle the push, it's up to you to handle it and call the // completion handler. If the SDK did handle it, it called the completion handler for you. if !handled { completionHandler() } } // Delete this function: func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: **@escaping** (UNNotificationPresentationOptions) -> Void ) { completionHandler([.list, .banner, .badge, .sound]) } } Now that your app’s code has been simplified, it’s time to enable these new SDK features. To do this, you’ll need to initialize the MessagingPush module. Follow the latest push notification setup documentation to learn how to do this. --- ## Troubleshooting URL: https://docs.customer.io/integrations/sdk/flutter/1.x/updates-and-troubleshooting/troubleshooting/ If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues. Basic troubleshooting steps Make sure your app meets our prerequisites: Attempting to use our SDK in an environment that doesn’t match our supported versions may result in build errors. Update to the latest version: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK. Try running our MCP server: Our MCP server includes an integration tool that can provide immediate help with your implementation, including problems with push and in-app notifications. See Use our MCP server to troubleshoot your implementation below. Enable debug logging: Reproducing your issue with loglevel set to debug can help you (or us) pinpoint problems.  Don’t use debug mode in your production app Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you set loglevel to error in your publicly available, production app. Try our test image: Using an image that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages. If you need to contact support We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster. Share information about your device and environment: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution. Provide comprehensive debug logs: When sharing logs with our support team, please ensure your logs include: SDK initialization: Show that the SDK was initialized with your site ID and API key Profile identification: Show that a profile was identified in your app Issue reproduction: Capture the exact issue you’re experiencing Unfiltered logs: Provide complete, unfiltered logs—don’t remove or filter out any log entries Debug level enabled: Make sure loglevel is set to debug when capturing logs for support For push notification issues: Use live push examples: If your issue relates to push notifications, provide logs from a live push notification sent through a campaign or API call, not a test send. Live pushes show the actual payload that was delivered to the profile. Test in different app states: Test and document the issue in various app states: Foreground: App is open and active Background: App is running but not in focus Killed/Terminated: App is completely closed Include the push payload: Share the complete push notification payload that you sent. Grant access to your workspace: It may help us to see exactly what triggers a campaign, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time. Try running CIO SDK Tools Our CIO SDK Tools library can help diagnose problems with your SDK implementation. This is a node package that you can run from inside or outside your app’s project folder. After you install it, you can run the doctor command to check your SDK configuration and get tips to fix problems. npx cio-sdk-tools@latest doctor /path/to/project Capture logs Logs help us pinpoint the problem and find a solution. To capture logs, you should install Flutter DevTools if you haven’t already. Enable debug logging in your app.  You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store. import 'package:customer_io/customer_io.dart'; import 'package:customer_io/customer_io_config.dart'; import 'package:customer_io/customer_io_enums.dart'; await CustomerIO.initialize( config: CustomerIOConfig( siteId: "919a7e12107bd03155f6", apiKey: "86344654754f1c48d32b", region: Region.us, //config options go here logLevel: CioLogLevel.debug ), ); Build and run your app on a physical device or emulator. Open the Logging view in your development application. If you use Android Studio, select View > Tool Windows > Logcat to see your logs. Filter for CIO in the top to find log messages specific to the Customer.io SDK. Export your log and send it to our Support team at win@customer.io. In your message, describe your problem and provide relevant information about: The version of the SDK you’re using. The type of problem you’ve encountered. An existing GitHub issue URL or existing support email so we know what these log files are in reference to. Push notification issues Problems with rich push notifications (images, delivered metrics, etc) If you have trouble with rich push features, like images not showing up in your push notifications, delivery metrics not being reported when a push notification is visible on the device, and so on, it’s possible that you either need to re-create your NSE target to support rich notifications your you may not have embeded the NotificationServiceExtension (NSE) at all. Remove your current NSE extension. In XCode, select your project. Go to the Signing & Capabilities tab. Click the NotificationServiceExtension target; it has a bell icon next to it. Click the minus sign to remove the target Confirm the Delete operation. Remove existing NSE files. Right click the NotificationServiceExtension folder in your project and select Delete. Confirm Move to Trash. Recreate the notification service extension, following instructions for your framework. When You create your target NSE file, make sure you select your app’s name from the Embed in Application dropdown. Then add the required files: React Native Flutter Expo (does this automatically) iOS After all files are added, go to the NSE target and, under the General tab, check Deployment Target and set it to a value that is identical to your host app’s iOS version. When you create a new target, by default, XCode sets the highest version of deployment target version available. While testing if your device’s iOS version is lower than this deployment target, then the NSE won’t be connected to the main target and you won’t receive rich push notifications. Then you can build and run your app to test if you can receive a rich push notification. Why aren’t devices added to people in Production builds? If you see devices register successfully on your Staging builds, but not in Production or TestFlight builds, there might be an issue with your project setup. Check that the Push capability is enabled for both Release and Debug modes in your project. You might also need to enable the Background Modes (Remote Notifications) capability, depending on your project setup and messaging needs. Image display issues If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image. Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG). iOS Android In-App (all platforms) Format JPEG, PNG, BMP, GIF JPEG, PNG, BMP JPEG, PNG, GIF Maximum size 10 MB* 1 MB Maximum resolution 2048 x 1024 px 1038 x 1038 px *For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image. Why didn’t everybody in my segment get a push notification? If your segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions. doesn’t specify people who have an existing device, it’s likely that people entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were people in your segment. Why are messages sent but not delivered or opened? The sent status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked delivered or opened when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a person turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.  Make sure you’ve configured your app to track metrics If your app isn’t set up to capture push metrics, your app will never report delivered or opened metrics! Why don’t my messages play sounds? When you send a push notification to iOS devices that uses our SDK, you can opt to send the Default system sound or no sound at all. If your audience’s phone is set to vibrate, or they’ve disabled sound permissions for your app, the Default setting will cause the device to vibrate rather than playing a sound. In most cases, you should use the Default sound setting to make sure your audience hears (or feels) your message. But, before you send sound, you should understand: Your app needs permission from your users to play sounds. This is done by your app, not our SDKs. Here’s an example from our iOS sample app showing how to request sound permissions. iOS users can go into System Settings and disable sound permissions for your app. Enabling the Default setting doesn’t guarantee that your audience hears a sound when your message is delivered!  We don’t support custom sounds yet If you want to send a custom sound, you’ll need to handle it on your own, outside the SDK and use a custom payload when you set up your push notifications. FCM SENDER_ID_MISMATCH error This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project. Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error. Verify that the Sender ID in your app matches the Sender ID in your Firebase project. If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error. In some cases, we may make fixes in our iOS push packages that fix downstream issues in the Flutter SDK. Before you contact support, you might want to [update your iOS dependencies](/Page(/integrations/sdk/flutter/push/#update-ios-dependencies) to get the latest packages and see if that fixes the issue. You can also check out our latest iOS changes to see if we’ve already fixed the issue or check out open issues to see if you’re experiencing a known issue. Deep links on iOS only open in a browser It sounds like you want to use universal links—links that go to your app if a person has your app installed and to your website if they don’t. Universal links are a bit different than your average deep link and require a little bit of additional setup. Notifications not coming through when app is in background If your app does not receive push notifications when it’s in the background, check the following: Ensure that you have implemented the _firebaseMessagingBackgroundHandler as suggested by the Firebase Messaging documentation. This handler is responsible for processing messages received while the app is in the background or closed. Verify that the handler is set correctly and receiving callbacks. Double-check the implementation to ensure it’s properly registered within your app’s code. Confirm the Flutter version you are using. For Flutter version 3.3.0 or higher, you will need to add @pragma('vm:entry-point') to the handler function for it to work correctly. In-App message issues My in-app messages are sent but not delivered People won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages. --- ## Changelog URL: https://docs.customer.io/integrations/sdk/flutter/1.x/updates-and-troubleshooting/changelog/ Check out the release history for the Flutter SDK. Stable releases have been tested thoroughly and are ready for use in your production apps. show --- ## Account Verification URL: https://docs.customer.io/accounts-and-workspaces/account-verification/ **Before you can send emails** to your audience through Customer.io, you'll need to request verification of your account. We verify all accounts to filter out bad actors that might try to abuse our services. The result of this process is that you get great deliverability and peace of mind, while we maintain our standard of high quality and integrity as an email service provider.  Emails send to your test delivery address before you’re verified You can activate a campaign or send a broadcast, but emails will send to your test delivery address, not your audience, before your account is verified. All other message types will send normally. Request verification Go to Workspace Settings > Email > Account Verification. Let us know your company website URL, your primary technical contact, and how you plan to use Customer.io. We’d love to know who your customers are and what types of messages you plan to send them! Then click Verify my account to notify us. You must be an account admin to submit a verification request. If you’re not an admin, contact someone with admin privileges to request verification. When your account is approved, you’ll see the following message within the same tab: Information we review We typically require the following before we can verify your account: Your company website, which you submit before requesting account verification. The process people take to subscribe to your messaging, if it’s not clear on your website, to ensure that we can accommodate your use-case per our Anti-Spam Policy. An example of a message you intend to send through Customer.io. This can be a message you create in your workspace or a screenshot/PDF of a message from your previous messaging platform. Domain verification, which you can accomplish through Workspace Settings > Email A member of our support team will notify you of approval or request additional information to move forward with the verification process. And that’s it! Happy sending! FAQs What can I do before my account is verified? While you can’t send email to your audience before your account is verified, you can: Send SMS, in-app messages, push notifications, and Slack messages Add people to your account and update their profile attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. using our API Create segments to group your users Create campaigns Craft emails Add delays and time windows to your campaign workflows Construct SMS messages Craft in-app messages and push notifications Customize your own email layouts Create a newsletter - a one-time broadcast to a group of people Pass eventsSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. into your account using our API Install our JavaScript snippet to start logging PageViews for your signed in users Set up your subscription center  Emails send to your test delivery address before you’re verified You can activate a campaign or send a broadcast, but emails will send to your test delivery address, not your audience, before your account is verified. All other message types will send normally. How long does it take? We do our best to respond to account verification requests as quickly as possible, as we know you’re eager to start sending! We don’t currently have a set time frame for responses as it will depend on the information you submit. What happens if I’m not automatically approved? Not to worry - this just means that we need more information to verify that you are who you say you are! We’ll reach out directly and request a few additional pieces of information so we can move forward with approval. --- ## Audit logs URL: https://docs.customer.io/accounts-and-workspaces/audit-logs/ Audit logs help you track changes made to your account, workspaces, messages, and other assets so you can keep your account secure and troubleshoot issues. To track changes to your workspace data, like updates to people or custom objects, go to your Activity Logs. Overview As an Account Admin, you have access to two audit logs: Go to Account settings > Audit logs to see Account-level audit logs. These provide security-related information like team member session data, changes in permissions, and deletion of API keys. Go to Workspace settings > Audit logs to see Workspace-level audit logs. These provide information about changes that team members and Customer.io made to your workspace, including imports and exports. Each entry in the audit log shows an Activity and a Timestamp. Click to see more details about an activity. You’ll see different activities depending on whether you’re viewing account or workspace logs. Filter your audit logs You can filter your logs by Date Range, but your plan determines the maximum available time period. On an Essentials plan, you can view, filter, and export data from the past 30 days. On a Premium or Enterprise plan, you can view and filter for data over any time period, but you can only export up the past year of activities. You can filter your logs by Date Range, Team member, the IP address of users who perform activities, and Event type. The Event type is a category of activities. The System team member Under Team member, the System entry represents Customer.io. We do some things automatically, like adding pre-built segments when you create a new workspace. These activities are attributed to System. Share your audit logs You can share your audit logs outside of Customer.io in a few ways: Export to CSV Copy raw data Copy metadata To share an individual activity with another Account Admin, click and then Copy link. Export to CSV Click Export to CSV, and then choose the date range to export and which workspace to save the download to. Keep in mind, if you’re on the Essentials plan, you can export up to the past 30 days of logs. On the Premium or Enterprise plans, you can export up to the last year. Copy log data To copy log data, click : Select View raw data to copy the event payload. Select View metadata to copy the metadata like IP address and API used in the request. Show diff To see how the event changed your account or workspace data, click and then Show diff. Here’s an example of a diff for a campaign in a workspace log. You can see what was removed in red and added in green: Account audit log activities Activities are grouped into Event Type values that represent a category of activities. For example, Administration activities are related to account-level settings and permissions. Activity Description Administration Account The main Customer.io account settings and configuration Workspace Separate environments within the account (e.g., production, staging) API & Integrations API Credentials API keys used to authenticate with Customer.io Track APIs. We currently don't surface data for Pipelines integrations. App API Key API keys used to authenticate with Customer.io App APIs Dedicated IP Address Custom IP Addresses associated with the account Permissions Role Custom roles defining permission sets for team members Team Member Users with access to the Customer.io account Team Member Workspace Permissions Workspace-specific permissions assigned to team members Team Member Account Permissions Account-wide permissions assigned to team members Security Log in/out User authentication events SCIM Configuration SSO user provisioning with System for Cross-domain Identity Management (SCIM) SCIM Token Authentication tokens for SCIM provisioning SSO Configuration Single Sign-On identity provider settings, like SAML Whitelisted IP IP addresses allowed for restricted access Workspace audit log activities Activities are grouped into Event Type values that represent a category of activities. For example, Content activities are related to images, files, and media uploaded to the workspace. Activity Description Content Asset Images, files, and media uploaded to the workspace Layout Email layout templates that wrap content (available for emails made in the rich text or code editors only) Saved Row Reusable content rows for email templates (available for emails made with the drag-and-drop editor only) Snippet Reusable content blocks (headers, footers, etc.) Template Email, SMS, push, or in-app message templates. This captures content changes across all workflows: campaigns, broadcasts, newsletters, transactional messages, and anonymous messages. Data & Imports Collection Non-people data like product catalogs you can relate to people. See below for Objects, a similar but separate feature from Collections. Export Data export jobs and configurations Import Data import jobs (CSV uploads, etc.) SQL Import Database syncs: these represent activities related to queries and sync frequency when you import data from a database. SQL Import Database Definition Database connection configurations for SQL imports Design Studio Design Studio Element (Folder, Component, Template or Email) Individual content blocks/components in Design Studio Design Studio Global Styles Shared styling (colors, fonts) across Design Studio templates Design Studio Version Version history of Design Studio templates Integrations Custom Form Form configurations for capturing user data Custom SMTP Custom email sending server configuration Integration Integrations with third-party services like Slack, Twilio, etc. This activity covers integrations available in your workspace settings. It does not include activities for most items under Integrations. Webhook Configuration Outbound webhook endpoints for external integrations Messaging Anonymous Message One-time in-app messages sent to unidentified users Campaign Automated messaging workflows triggered by events, segments, and more Campaign Action Individual steps/actions within a campaign (email, webhook, delay, etc.) People & Objects Object Deletion Deletion of custom objects Object Type Custom object definitions (e.g., companies, orders, products) Profile Deletion Individual customer profile deletions Profile Merge Merging of duplicate customer profiles Profile Shortcut Quick access shortcuts to specific customer profiles Segments & Audiences Ad Audience Google & Facebook Ad Sync Ad Audience Credentials Authentication for Data Pipelines audience connections Segment Dynamic groups of customers based on attributes/behavior Settings Domain Email sending domains and DNS configuration Sender Identity Verified phone or email sender addresses and names Subscription Center Settings Customer preference center configuration Subscription Topic Email subscription categories customers can opt in/out of Tag Labels for organizing campaigns, segments, and templates What data’s not available? For now, you’ll have to reach out to support to audit events for most items under Integrations. The Integrations category mostly accounts for activities related to workspace-level settings, like message integrations (SMS/Twilio, Email/SMTP, Webhooks, and so on). Click Need help? and choose Get help with an issue to submit a request. Send us feedback Click Feedback to let us know what you need to better audit your account and workspaces. --- ## Tasks: Workspace performance URL: https://docs.customer.io/accounts-and-workspaces/workspace-tasks/ Use the Tasks page to monitor background tasks that might take some time to complete, like segment creation and imports. This lists processes for your workspace, not your account as a whole. Access tasks In your workspace’s top navigation bar, click and then choose Tasks. You’ll see your latest background tasks and their status. Click See all tasks to drill in further. From the Tasks page, you can find more information on each task by clicking . Check when the task started, how long it was queued for, and how long it took to complete. How we process priority levels You’ll also see a priority within the details of a task. Priority levels represent separate processing queues, not which tasks are processed first; a high-priority task runs in parallel with a low-priority task. High-priority tasks have more dedicated processing capacity, but they don’t preempt or delay other tasks. Enable auto-refresh to monitor long-running tasks To monitor long-running tasks, turn on Auto-refresh. The page will automatically refresh every 10 seconds; otherwise, you have to manually refresh. Filter tasks You can filter tasks by status and task type to quickly find the tasks you’re looking for. Status Choose a status to filter by Success, Failure, etc. This helps you monitor long-running tasks and check when jobs are completed. Tasks with the status of Canceled or Failure include error messages. Click to find more info on the status. Task type Filter by task type to monitor specific actions. Click Filters to get started. Learn more about how we process priority levels above. Task type Priority How long it took to… API Triggered Broadcast High To send a broadcast to all matching audience members when triggered via the API API Triggered Broadcast Including Custom Data Medium To process a per-user data file CSV with custom Liquid/merge data per recipient provided when triggering a broadcast via API Archive Campaign High To exit all active journeys and transition the campaign to an archived state Backfill Campaign High To evaluate the trigger conditions against all people and enter matches into the campaign. Triggered when a campaign is started or when trigger conditions change on a running campaign Build Segment Medium To rebuild segment membership after saving/updating segment conditions Change Campaign Frequency High To update or clear the restart/re-entry interval scheduler for people in a campaign Clear Manual Segment Medium To remove all members from a manual static segment Delete Action Low To clean up journeys when workflow actions like emails, wait untils, branches, etc. are deleted from a campaign Export Data Medium To export people data, metrics, or deliveries Import Collection Medium To import catalog/collection data from a file Import Preprocess Medium To validate and upload a CSV import file before processing Import Process Medium To process a validated CSV import Manage Deliveries High To perform bulk delivery operations: send drafts, delete drafts, retry failed deliveries, or delete deliveries Prepare Newsletter Medium To send a broadcast/newsletter to its audience SQL Sync Medium To run a SQL-based sync through any Reverse ETL integration Snowflake, BigQuery, etc. Stop Campaign High To end all journeys in the campaign and transition the campaign to a stopped state. This task does not run if you choose to let journeys finish naturally. Update Action High To re-evaluate an action for people currently waiting at it. Triggered by: shortening a delay, rescheduling an action, changing a time window, wait until or branch condition --- ## How We Bill URL: https://docs.customer.io/accounts-and-workspaces/how-we-bill/ Your subscription determines how we bill for profiles and the integrations available to you. You can find the details of your subscription on the [Plans & Billing page](https://fly.customer.io/settings/billing/your-plan). How it works When you sign up for Customer.io, you choose a plan—essentials, premium, or enterprise. Your plan determines how many emails and profilesThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. you’re entitled to. Profiles are the peopleAn instance of a person. Generally, a person is synonymous with their profile; there should be a one-to-one relationship between a real person and their profile in Customer.io. You reference a person’s profile attributes in liquid using customer—e.g. {{customer.email}}. and objectsAn instance of an object. An object is synonymous with its profile; there should be a one-to-one relationship between an object and its profile in Customer.io. Each has a unique identifier. in your workspace. We don’t differentiate between people and objects for billing purposes; we count them together to make things easy. In any month, you’re charged for your plan fee for that month plus overages from the previous monthly billing period. For example, when you’re billed in March, you’re charged the plan fee for March plus any overages from February. If you have overages in March, you’ll be charged for those in April. We charge different rates for email and profile overages: Type Essentials Rate Premium Rate Enterprise Rate Profiles $0.009 per profile Custom, starting at $0.004 per profile Custom, starting at $0.004 per profile Emails* $0.12 per 1,000 emails $0.12 per 1,000 emails $0.12 per 1,000 emails *Any email that reaches your email service provider (ESP) counts towards billing. This includes emails that bounce or are suppressed. See ESP suppression for more information. You can find the details of your subscription on the Plans & Billing page.  You must be an Account Admin or Member with the Access billing & account info permission to see billing details. How to calculate monthly overages During each monthly billing period, we calculate overages based on: The highest number of profilesThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. in your account Emails sent from your account For emails, this is simple: we count the number of emails you send in a month. For profiles (people and objects), it’s a bit more complex, because you can add and delete them throughout the month. If a profile exists at any point during the billing period, it counts towards your bill for that month—even if you delete it. For example, imagine that you begin a monthly billing cycle with 3000 profiles in your account. During the billing period, you add 250 people and delete 50. At the end of the billing period: You have 3200 profiles in your account You’re billed for 3,250 profiles (3000 + 250), because that’s how many total profiles were active in your account throughout the month. The next month’s billing cycle begins with fresh with 3,200 profiles.  Check out our tips to help you manage your profile count and reduce overages. Make sure your integrations don’t inadvertently re-add deleted profiles If you delete profiles but your integrations automatically re-add them during the same billing cycle, you’ll be charged for both the original profiles and the re-added profiles—effectively doubling your count for those profiles. This is a common source of unexpected billing increases. For example, imagine that you have 100,000 billable profiles. You delete 20,000 people, but your integration automatically re-adds those same 20,000 people the next day. Your billable profile count for that month becomes 120,000 (the original 100k + the 20k re-added profiles) because the original 20,000 were in your account to start the billing period and you added 20,000 people during the same billing period. Find your billing information and account summary In your Account Summary, you’ll see counts for current profiles (which excludes deleted profiles) and billable profiles (which includes profiles that you deleted in the billing period). Your next month’s bill will be your plan fee plus any overages based on billable profiles and emails sent during the previous month. Your account settings shows information across workspaces The Plans & Billing and Account Summary pages count people and objects across ALL workspaces, including ones with a name of “test,” “dev,” or “sandbox.” If the same profileThe representation of a person or group in Customer.io. People and custom objects both have their own profiles, but we bill based on the total number of profiles in your account. exists in multiple workspaces, it counts as multiple profiles for billing. If you delete a profile and add it back in the same billing period, it counts as two profiles for billing. Parent and child accounts Usually, you’ll just have one or more workspaces in a single account, but sometimes, you’ll have multiple accounts linked together, too. If your profile count doesn’t add up the way you’d expect, it could be because you connected a “child account” to your “parent account” in the past, which is simply another Customer.io account whose billing is connected to your main Customer.io account. We currently do not specify in Account Settings whether you have multiple accounts linked together for billing purposes, so you’ll need to reach out to your CSM or our Billing team if you need confirmation. Non-email messages and fair use Your plan includes unlimited in-app messages, push notifications, and webhook messages. We don’t charge for these kinds of messages. However, while these messages are unlimited, we have a fair use policy of sixty (60) times the number of profiles in your plan each month (unless otherwise stated in your contract) to protect against abuse. This policy is meant to be generous and exceeds the current sending volume of our most active customers (check your current usage here). However, if you exceed this policy, we might block or delay your messages. You may receive a warning or be suspended from the platform if you continually exceed this policy without communicating with Customer.io. If you have any questions about our fair use policy, please contact us at win@customer.io. For SMS messages, you’re either billed for the messages you send through Customer.io or Twilio. If you’re billed through Customer.io, see our SMS billing page for more information. For WhatsApp messages, Customer.io doesn’t charge you, but Meta and Twilio (if you use Twilio) do. See WhatsApp billing for details. Payment methods Each month, on your billing date, we will charge the card on file for your subscription for the upcoming billing period. You’ll find billing information in your Account Settings under Plans and Billing. If you have a premium or enterprise plan, you can choose to pay annually. If you pay annually, we’ll email an invoice to your account’s invoice contact each year. We accept payment for invoices via domestic(US) wire, ACH, or credit card. Additional usage costs for your account are calculated monthly and charged to the credit card on file. Taxes Local, state and federal sales tax may be added to your bill if they are applicable in your location. If your organization is exempt from this for any reason then please email billing@customer.io. How to keep track of upcoming bills Check your Plans & Billing page to see an estimate of your upcoming bill. If you have questions about your upcoming bill(s), reach out to our billing team. Where to find receipts When your billing date rolls around, your invoice or receipt will be sent to the invoice contact on file. You can change who receives these emails by updating your invoice contact on the Account Information page. If you are an Account Admin, you can also access all historical receipts in your Billing History and view or download them as PDFs. Mistakes in your bill? We make every effort to ensure your bill accurately reflects your usage. But you should contact us right away if you think we’ve made a mistake on your bill! Our standard grace period for reviewing payments is three months but we appreciate that each situation is different. --- ## Billing for SMS messages URL: https://docs.customer.io/accounts-and-workspaces/sms-billing/ We partner with Twilio to send SMS messages. If you're billed for SMS messages directly through Customer.io and not Twilio, then your plan includes a monthly number of SMS *segments*, where a *segment* is a single SMS message of up to 160 characters. If you exceed that limit, you'll pay an additional fee for each SMS message segment that you send.  This page is for accounts billed for SMS activity through Customer.io Most users currently have their own Twilio account and manage their phone numbers and bill directly with Twilio. If this is the case for your account, you should see Twilio’s billing information instead. How it works SMS billing is mostly concerned with message “segments,” where each segment is up to 160 characters—what you might sometimes see as a single chat bubble in traditional SMS applications. Your plan includes a base number of SMS segments per month. If you use more than your allotted segments, you’ll pay an additional fee for each segment you send. For example, if you send an SMS from Customer.io containing 200 characters, that consumes two “segments”; it may even appear to your audience as two separate chat bubbles. We help you calculate the number of segments you’ll consume to send an SMS message. There are also other one-time fees that you can encounter as you register new phone numbers, brands, and campaigns. Where can I see my available SMS segments and usage? If you’re billed through Customer.io, you can see your SMS credits on the Plans & Billing page. If you’re billed through Twilio, you’ll see your allotment of SMS segments and usage in the Twilio Console. What constitutes an SMS segment? Generally, SMS segments are limited to 160 characters. However, if you use special characters like emojis in your message, you’ll be limited to 70 characters. Learn more about SMS message encoding. MMS messages—messages that contain images or other media—consume three segments per message. When you write a message in Customer.io, we’ll help you calculate the number of segments you’ll use per message. Overage fees If you exceed the SMS limits for your plan, you’ll incur additional fees, which are automatically charged to the payment card on your account in your regular monthly billing cycle. In general, if you only send messages for your own account—you run a single business entity and you send messages directly to your users or customers—you’ll only see SMS overages. If you’re a business with different sub-entities or you’re an agency that manages accounts for multiple clients who send messages to their customers or users, you may see additional fees like Sender or One-Time coverage fees. SMS message overages Each SMS segment over your plan limit costs $0.0120. Where a segment is up to 160 characters, typically represented as an individual chat bubble going to a single recipient. So, if you send a message from Customer.io containing 200 characters, that appears to your audience as two messages—two chat bubbles—and consumes two segments per recipient. If you send that message to 100 recipients, you’ll use 200 segments, and you’ll be charged $2.40. Sender, campaign, and one-time overage fees If you manage multiple SMS senders on behalf of your clients, you may encounter overages for sender numbers, campaigns, and A2P 10DLC brand registrations. Learn more about A2P 10DLC brand registrations below. Note that the word “campaign” here is not the same as a Customer.io campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria.. This is a regulatory term that refers to the kinds of messages that you send. One-time fees are charged to the payment card on your account in your regular monthly billing cycle. Sender number fees Toll-free numbers: $2.50 per additional number per month Long code numbers (10DLC): $2.50 per additional number per month Short code numbers: $1,000 per additional number per month Campaign registration fees: $12.50 per additional campaign per month One-time overage fees: 10DLC brand registrations: $50 per additional brand Campaign use case registrations: $20 per additional campaign STOP message responses By default, Twilio will respond to a STOP message with a message indicating that the user has been opted out of receiving messages. While your bill reflects these messages, opt-out response messages don’t go through Customer.io and aren’t logged by us. You can customize opt-out responses in the Twilio console using Twilio’s Advanced Opt-Out feature. What is A2P 10DLC? A2P 10DLC stands for Application to Person 10-digit Long Code. It’s a regulatory standard in the United States to regulate communications between applications—like Customer.io or Twilio—and recipients to prevent spam and unsolicited messaging. When you register for a 10DLC number, you’ll register for at least one brand and one campaign, where the brand is who sends your messages and the campaign is the “kinds” of messages that you send. A brand can have multiple campaigns and an account can have multiple brands! Registering your brands and campaigns helps Twilio and cellular carriers understand who sends messages and why. This helps carriers and customers trust the businesses that send messages. What is a brand? Think of the brand as “who” sends your messages. If you’re a business that sends SMS messages directly to your customers, then you’re a single brand. Your customers or users recognize you as a single entity. But imagine that you’re a business with multiple entities: like a parent company called Acme with different subsidiaries called Acme Anvils, Acme Rocket Skates, and Acme Pianos. Your audience can register for messages from the parent or any of the subsidiaries, meaning you have four brands. This means that it’s likely you’ll have at least four phone numbers, one for each brand. See Twilio’s documentation for more about brands and brand types. What is a campaign? As it relates to A2P 10DLC, a campaign is a regulatory term, and it’s not the same as a Customer.io campaignCampaigns are automated workflows you set up to send people messages and perform other actions when they meet your criteria.. Think of a campaign as the kinds of SMS messages you want to send. For example, you might send two-factor authentication codes and promotional content. These might represent two campaigns. Someone may want a two-factor authentication code but they may not want to receive promotional content. A brand can register up to five different campaigns. See the complete list of campaign use cases. What are one-time fees? Whenever you register a new brand or a new campaign, you’ll incur a one-time registration fee as a part of your regular monthly billing cycle. What is a sender number? A Sender number is a phone number that you use to send SMS messages. You can use a toll-free number, a long code number, or a short code number. You can have multiple sender numbers, and you can use them to send messages to different audiences. Each phone number is typically associated with a brand and a campaign. --- ## Billing for WhatsApp messages URL: https://docs.customer.io/accounts-and-workspaces/whatsapp-billing/ **Customer.io does not charge for WhatsApp messages**, but Meta and Twilio—the partners who handle WhatsApp messaging—do. Learn about the costs you can expect when sending WhatsApp messages. Meta’s pricing Meta, WhatsApp’s parent company, charges for WhatsApp messaging on a per-message basis. Pricing varies by template category and the recipient’s country. They also offer volume-based pricing tiers. For example, a message based on a “utility” template to a recipient in the United States costs $0.0034 per message. See Meta’s WhatsApp pricing page for the full list of rates for templates, countries, and message volume tiers. Template categories include: Marketing: Promotional messages, offers, and announcements you initiate Utility: Transactional messages like order confirmations or account updates you initiate Authentication: One-time passcodes and verification messages you initiate Twilio’s per-message pricing As of February 2026, Twilio charges $0.005 per WhatsApp message. If you send WhatsApp messages through Twilio, you’ll pay this amount in addition to Meta’s fees. See Twilio’s WhatsApp pricing for more information. --- ## AI credits URL: https://docs.customer.io/accounts-and-workspaces/ai-credits/ AI credits are the currency for LLM actions in campaigns. Learn how credits work, about our pricing model, and how to manage your AI credit usage. How it works AI credits are the units that LLM actions consume when the action calls a Large Language Model (LLM). Each time a person uses an LLM action in your campaign, the LLM runs and uses credits based on the model and complexity of the request.  AI credits only apply to LLM actions Only LLM actions consume AI credits in your account. Other AI features—including the Agent, segment builder, content analysis, and in-app message suggestions—don’t use AI credits. Pricing As part of our Q2 launch, every customer on a paid plan will receive a one-time grant of 100,000 AI credits.* These credits expire 90 days after they are granted. These initial credits give you a chance to explore the power of LLMs natively in your workflows, test your use cases with different models, and better understand how AI can support your campaigns. This one-time grant is offered between April 8 and June 30, 2026, and the credits expire after 90 days. Learn more about purchasing additional credits. Existing customers on a paid plan will receive 100,000 AI credits on April 8, 2026, which will expire on July 8, 2026. New customers who start a paid plan between April 8, 2026 and June 30, 2026 will receive 100,000 AI credits that expire 90 days later (For example, a plan starting mid-June would have AI credits that expire mid-September). You can track your usage and purchase additional credits in your Account Settings: AI credits are applied at the account level and shared across all workspaces. So if you use 75,000 credits in one workspace and 25,000 in another, you’ll have spent all of your AI credits and need to purchase more. AI credits are not available to customers on Trial plans. * Use of AI features is subject to applicable Promotional Credit Terms and Feature Terms. Purchase additional credits You can purchase additional AI credits for $10 per 100,000 credits. You can only purchase additional credits in increments of 100,000. If you purchase additional AI credits during the Q2 introductory period, your account will use the introductory AI credits before applying any purchased credits. While the introductory credits expire after 90 days, purchased credits do not expire. To purchase additional credits, go to Account Settings > Plans & Billing. Click Buy Credits. Fill in your credit card info, billing address, and the number of credit packs you want to purchase. (1 pack = 100,000 credits). Confirm your purchase. You must be an Account Admin or a Member with billing access to purchase additional credits. Learn more about roles and permissions in Team members. Credit usage by model Models consume credits at different rates. Reasoning models use more credits, while quick models use fewer. Reasoning models produce higher-quality results for complex tasks but use more credits per run. Quick models are faster and more cost-efficient, using fewer credits per run. When you set up an LLM action, you choose which model to use. You’ll see which ones are considered most cost-efficient to help you make a decision. Learn more about choosing a model in LLM actions. How LLMs consume AI credits Credits provide a consistent way to track and manage cost across models, even though the underlying costs vary depending on the LLM in use. The number of credits consumed by an LLM action varies based on three factors: The model you choose The size of the input (your prompt) The output (the model’s response) Each model has different pricing based on input and output tokens, where input tokens represent the information being processed and output tokens represent the information being generated, so a more uniform credit system makes it easier to communicate costs. The credit burn rate below shows how credit consumption scales relative to the base model you can use in LLM actions: Gemini 2.5 Flash-Lite. Because input and output tokens carry different weights, the actual burn rate will vary slightly from the approximations shown. Provider Model Credit burn rate Google Gemini 2.5 Flash-Lite 1x (Base model) Google Gemini 2.5 Flash ~3x Google Gemini 3.0 Flash ~5x Google Gemini 2.5 Pro ~12.5x Google Gemini 3.0 Pro ~20x Anthropic Claude Haiku 4.5 ~10x Anthropic Claude Sonnet 4.6 ~30x Anthropic Claude Opus 4.6 ~50x For example, Claude Haiku 4.5 uses about 10x more credits than Gemini 2.5 Flash-Lite, though it could be slightly higher depending on the prompt and model’s response. Note that credit consumption is based on varying factors, including model selection and usage patterns, and is subject to change at our discretion. Previews, tests, and live campaigns use credits Your account uses up credits in these situations: When a customer reaches an LLM action during their journey through a campaign When you click Preview Response to test your LLM action The amount of credits used depends on the model you choose and the complexity of the request. Monitor credit usage You can monitor AI credit usage in your account’s Plans & Billing page. Customer.io will message you when your AI credit balance is running low, and then again when it’s exhausted. Use this period to purchase more credits and review how LLM actions are performing. Fallback values kick in when credits run out When your account runs out of AI credits, LLM actions use their configured fallback values instead of calling the model. Your campaigns continue to run, and any conditions or messages that rely on an LLM action won’t fail because they’ll use your fallback values. --- ## Reducing billing overages URL: https://docs.customer.io/accounts-and-workspaces/reduce-billing-overages/ Every month, we count the number of people and objects in your account for billing purposes. If you exceed your plan's monthly limits, we'll charge you for the overage. By accounting for your plan's limits and deleting unnecessary profiles, you can reduce overages and keep your billing predictable. You should develop a cadence to delete unnecessary people and objects from your account to reduce overages and keep your billing predictable. Regularly deleting unused or defunct profiles can help you control your costs. This is probably something you should do monthly or every few months, rather than once or twice a year! 1. Review your integrations Overall, we recommend you review your integrations and ensure that you only add people and objects that provide value to your company/organization. Adding fewer empty or unwanted profiles in the first place will reduce the number of profiles that count toward your bill. You should also make sure your integrations won’t automatically re-add deleted profiles after you delete them. If you delete profiles but your integrations automatically re-add them during the same billing cycle, you’ll be charged for both the original profiles and the re-added profiles—effectively doubling your count for those profiles. 2. Adopt a sunset policy to delete unengaged profiles Adopting a sunset policy is a great way to identify unengaged people and contributes to good list hygiene. Limiting your messages to engaged users can positively impact your sending reputation. In your workspaces, create segments with conditions based on actions that target inactivity, like “has not opened any email within the last 180 days.” Then you can delete the people that match your criteria so you accurately reflect churn and reduce your overall total of billable profiles. 3. Delete profiles before your billing date To keep from going over your profile limit in future billing cycles, you should delete unnecessary people and objects 1-2 days before your billing date. This ensures that we don’t count unnecessary profiles in your next billing cycle. You might not need an object if it doesn’t have relationships to your customers. You can quickly find them by filtering for objects without relationships, and then delete them if appropriate. 4. Delete profiles while you can account for growth To keep your current billing cycle from surpassing your profile limits, delete unwanted profiles while you still have enough room to account for growth. For example, on the Essentials plan, you have up to 5,000 unique profiles available to you. If, over the course of the billing period, you expect to add 250 people, you need to start deleting profiles before you hit a billable people count of 4,750. This leaves room for the expected growth and ensures you will stay under the 5k limit. You can see your billable profile counts, overage rates, and any overages on your account’s Plans & Billing page. Keep in mind, this shows your totals across ALL workspaces. On your Account Summary, you can view your current profile counts (excludes deletions) and billable profile counts (includes deletions) for that billing period across your workspaces. You might not need an object if it doesn’t have relationships to your customers. You can quickly find them by filtering for objects without relationships, and then delete them if appropriate. --- ## Payment Problems URL: https://docs.customer.io/accounts-and-workspaces/payment-problems/ We understand that unexpected things happen and our billing team is here to help. Send us an email at [billing@customer.io](mailto:billing@customer.io). To help you plan, we've provided an outline of how we approach payment issues. We understand that each situation is different so please reach out to our billing team to discuss your options. If your payment fails: If your account payment fails repeatedly, our billing team will try to reach out to your team to talk about the issue. It’s important you keep your invoice contact up to date so that these messages reach the right person. We’ll also display a banner in-app to let you know there’s a problem. If you see this, don’t panic! Reach out to our billing team on billing@customer.io and they will be happy to work with you to resolve the issue. If your account is suspended: If we haven’t heard back from you after 10 days then user access to your account will be suspended. Your account is still active while it’s suspended, your data is safe; your campaigns will continue to run; and your messages will continue to send. If your account is suspended, and you’re an Account Admin, we’ll redirect you to the Plans & Billing page and ask you to update your card when you log in. If you’re not able to do this then please contact our billing team. If your account is canceled: If we’re not able to reach you and we aren’t able to take payment after 30 days, then we will notify all team members and close the account. If this happens then messages will stop sending and after 60 days your data will be scheduled for deletion. If you want to re-activate your account: Account data is scheduled for deletion 60 days after an account is closed. During this period you can re-activate your account by reaching out to our billing team on billing@customer.io. Depending on the circumstances and timing of your cancelation we may ask you to pay any overdue invoices before we re-activate. --- ## Canceling Your Account URL: https://docs.customer.io/accounts-and-workspaces/canceling/ We really hate to see you go, but we understand sometimes things change. But, before you leave, we'd love it if you'd [get in touch with our team](https://customer.io/contact/) and **let us know if there is something we can do better**. If something isn't working right or you find a feature difficult to use, we want to make it better! We really hate to see you go, but we understand sometimes things change. Before you part, though, we’d love it if you’d get in touch with our team and let us know if there is something we can do better. If something isn’t working right or you’re finding a feature difficult to use, we want to make it better! How to cancel How you cancel depends on your plan type. Essentials and Startup plans If you’re on an Essentials or Startup plan, you can cancel directly from your account’s Plans & Billing page. Look for the option to cancel your account at the bottom of the page.  To cancel your account, you must be an Account Admin or Member with the account-level permission “Access billing & account info.” Premium and Enterprise plans If you’re on a Premium or Enterprise plan, contact your Customer Success Manager (CSM) to cancel your account. Once your account is canceled Once an account is canceled, messages will stop sending and user access will be immediately removed. Please make sure you’ve saved any information you need before you cancel. Your account data will be scheduled for deletion after 60 days. Once your account data is deleted, your account will be unrecoverable. What happens if you change your mind Great news! If you would like to restart your account then please reach out to our billing team at billing@customer.io. If you are still within the 60 day window, they will be happy to help you get back up and running; otherwise, we will get you set up with a new account. --- ## Plan Features URL: https://docs.customer.io/accounts-and-workspaces/plan-features/ Customer.io offers different billing plans. These plans determine how much information you can store before incurring overages, and, in some cases, what features you can use. How it works Customer.io offers 3 plans: Essentials, Premium, and Enterprise. You’ll find a detailed list of the differences between plans on our pricing page, but your plan determines whether or not you can use some features in the system. These are typically advanced features that take more resources to support (in terms of storage, processing, and people). General features Feature Essentials Premium Enterprise Description Extra workspaces2 Unlimited UnlimitedWorkspaces hold your people, campaigns, etc. Different workspaces provide a mechanism to manage separate business entities, groups of people, and so on. HIPAA Compliance On premium and enterprise plans, you can sign a Business Associate Agreement (BAA) with Customer.io. Audit logs On essentials, you have access to the past 30 days of logs. On premium and enterprise, you can view logs over any time period, but only export up to the last year of logs. Messaging features Feature Essentials Premium Enterprise Description Collections Collections provide a mechanism to keep data independently of people. You can query this data as a part of a campaign and associate it with people to populate additional information in messages. Dedicated IPs You can request up to 3 dedicated IPs at no extra cost. Anonymous in-app messages Create and send in-app messages to unidentified people on your app or website. integrations Some integrations and integration features are only available on premium and enterprise plans. Feature Essentials Premium Enterprise Description Premium integrations Bring in data from Salesforce. Send data out to Hubspot, Amazon S3, Google BigQuery, Snowflake, and more. See Premium Integrations for a complete list. Data replay The ability to "replay" historical data from a source to a new destination. This is particularly useful when migrating vendors (like going from Amplitude to Mixpanel). Data warehouse integrations You can export your Journeys data to your data warehouses—Amazon Redshift, Google BigQuery, MySQL, Snowflake etc—for reporting and advanced analysis. Premium & Enterprise integrations The following integrations are only available on premium and enterprise plans. We mark these integrations in our documentation with these badges: Premium and Enterprise. Integration Source (inbound) Destination (outbound) Salesforce ✅ ✅ Hubspot ✅ ✅ Amazon S3 ✅ Amazon RedShift ✅ Azure Blob Storage ✅ Google BigQuery ✅ Google Cloud Storage ✅ Snowflake ✅ Free trials You can sign up for a free, 14-day trial to try out Customer.io. You can send up to 5,000 messages across all channels (email, SMS, push, in-app, slack, and webhooks) during this time. You can email a test message to up to three recipients at a time, and you can send no more than 50 test messages per day. To access features available on our Premium plans, upgrade to a premium trial. Upgrade to a premium trial  To manage your plan, you must be an Account Admin or Member with the account-level permission, “Access billing & account info.” If you’re new to Customer.io, you might start out on our Essentials plan. You can request an upgrade if you want to try out our premium features. You can request an upgrade to a premium trial: on any of our pages representing premium features—like the Collections page (under Configure data, click More) on any of our Data Warehouse integration pages from your Account Settings > Plans & Billing page Downgrade from a premium trial  To manage your plan, you must be an Account Admin or Member with the account-level permission, “Access billing & account info.” If you’re on a premium trial and enabled any premium features, you’ll need to disable or remove some of these features before you can downgrade. When you try to downgrade, we’ll let you know what you need to change. You’ll find instructions to disable or delete premium features below. Delete extra workspaces  You must be an Account Admin to delete workspaces. The Essentials plan is limited to two workspaces. If you have more than two workspaces and want to downgrade, you’ll need to delete the excess workspaces. Deleting workspaces removes all data associated with the workspace—campaigns, messages, people, etc. This action is not recoverable. You may want to export your people data before you delete you workspaces. In the upper-right corner, where you see your workspace name, click and select Manage all workspaces. Click Delete for the workspaces you want to remove. Remember, you only need to remove workspaces until there are two or fewer! Delete collections If you used collections, you’ll need to remove them from your workspace before you can move to our Essentials plan. However, you can’t delete collections until you remove Query Collection blocks from your campaigns. We don’t currently have a way to find where you’ve used Query Collection actions in campaigns and broadcasts. Under Configure data, click More > Collections. For the collection you want to remove, click and select Delete. If your collection is used in a campaign or broadcast, you’ll receive an error. You must find and delete Query Collection actions in campaign or broadcast workflows that are associated with your collection before you can delete the collection itself. Disable your data warehouse integrations  You must be an Account Admin to disable data warehouse integrations. If you want to downgrade to our Essentials plan, you’ll need to delete your data warehouse integrations that export data from Customer.io to your data warehouses—Amazon Redshift, Amazon S3, Google BigQuery, Google Cloud Storage (GCS), Microsoft Azure, or Snowflake. To disable these integrations: Go to Integrations and select your data warehouse. Click Disconnect Sync. This disables your integration. It doesn’t affect the data already stored in your storage buckets or data warehouse. --- ## Builder plan URL: https://docs.customer.io/accounts-and-workspaces/builder-plan/ The Builder plan provides messaging infrastructure for early stage builders and transactional senders: set up and test your messaging for free, go live for $10, and upgrade to the full platform when you're ready! How it works The Builder plan is a low-cost way to set up and start sending transactional messagesMessages that your audience implicitly opts into—like purchase receipts, password reset requests, shipping updates, two-factor authentication codes, etc. and one-time broadcasts with Customer.io. You can send emails, push notifications, in-app messages, and webhooks via our CLI, MCP server, or web app. You can also send SMS and WhatsApp if you integrate with Twilio. Unlike our other offerings, the Builder plan is not a subscription model and has no monthly fees, no trial periods, and no send limits. It’s free to build and test! When you’re ready to send to your customers, you add funds, starting at just $10, and pay $0.40 per 1,000 messages. To create marketing campaigns or set up out-of-the-box integrations with services like Salesforce, check out our subscription-based plans instead. Who is the Builder plan for? Anyone can sign up for the Builder plan, but it’s intended for: Developers and founders at startups in the early stages who need reliable transactional messaging and aren’t yet ready to sign up for a monthly plan. Mobile developers who want to test push notifications and in-app messages before committing to a monthly plan. AI-native builders working with LLMs, agents, and other tools who want messaging as infrastructure. You don’t need a GUI; you use our CLI or MCP server to support your workflows.  If you need behavioral workflows, audience segmentation, or multi-channel automation, start with the Essentials plan. What’s included in the Builder plan The Builder plan includes the following features. Those not included are available on our Essentials and higher plans. Workflows Available in Builder Plan Transactional messagesMessages that your audience implicitly opts into—like purchase receipts, password reset requests, shipping updates, two-factor authentication codes, etc. Broadcasts (Newsletters only) API-triggered broadcasts Campaigns Message channels Available in Builder Plan Details Email Send for free to up to 10 verified recipients before sending to customers. Up to 2 sending domains per workspace. Push notifications In-app messages Inbox messages Webhooks SMS or WhatsApp through Twilio SMS or WhatsApp through direct integrations LINE Slack Anonymous in-app messages Data and integrations Available in Builder Plan Details People Add verified, test recipients and customers to your workspace. Manual segmentsA segment of people you maintain manually. You must explicitly add people to, or remove people from, the segment. Dynamic, data-driven segments Custom objectsAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course. Mobile SDKs and libraries Native integrations for services like Salesforce or Hubspot Analytics across workflows and message channels Set up & test your account If you’re new to Customer.io, you can sign up for the Builder plan here or through our CLI. You can sign up without payment information and start setting up your message channels and integrations right away. Dive in and get started! Should I integrate via Customer.io’s MCP or CLI? CLIs are optimized for agentic communications and developer-oriented use cases. Our CLI helps you build workflows and automations from your terminal. You use this in conjunction with terminal-based coding agents like Claude Code or Codex. If you want to send messages by interacting with a third-party agent interface, like Claude Desktop or ChatGPT, you’ll want to integrate via MCP instead. This is typically for marketers or others who aren’t primarily developers. This way you have a GUI to interact with, rather than the command line. Authenticate via CLI If you signed up through our CLI, you’ll already have a key and can skip this step. If you signed up through our site, go to Home in your workspace to create a Service Account API key. Learn more about getting started with our CLI. Authenticate via MCP To get started with our MCP server, go to Personal Settings > MCP and follow the instructions for your client of choice. You can find more info in our docs too. Set up your message channels If you plan to send emails, add your sending domain to your workspace to start the verification process. Your domain must be verified to send emails to anyone, including your verified recipients. Otherwise, you can set up other message channels through our CLI, MCP server, or Workspace Settings. Add verified recipients Verified recipients are test users that have opted into receiving emails from you. You can add up to 10 verified recipients to your workspace. Account Admins are automatically added as verified recipients. Verified recipients are only available on the Builder plan. Add recipients from Home When you add a verified recipient from Home, this person automatically receives an email asking them to opt-in to receiving emails. You can’t send to verified users until they’ve opted in. Add recipients from the People page You can also add individuals or groups of people via CSV from the People page. After you add people, select each person you want to verify. Then choose Actions > Invite verified recipient to send them an opt-in confirmation email. Check opt-in status To check whether your verified recipients have opted-in to receiving emails, review their profile: Go to People. Search for and select your recipient. Check Verified user status. Pending means they have not opted in yet. Verified means they have opted in and can now receive emails. Send to verified recipients Send emails to verified recipients to check how your emails look before going live. Your sending domain must be verified before you can send to anyone, including verified recipients. Send to customers: add funds When you’re ready to send to your customers, you purchase funds to go live. Funds don’t expire, and you can track usage from Home or your Plans & Billing page. Details Minimum purchase $10 (~25,000 sends) Maximum funds per purchase $1,000 Cost $0.40 per 1,000 messages Message rate Flat across all channels—email, in-app, push, webhooks, and SMS or WhatsApp through Twilio Profiles Unlimited people profiles, no objects Click Add funds. Enter your payment information and the amount you want to purchase. The minimum purchase is $10, and you can add funds at any time.  Sending stops when your funds run out When your funds run out, sending stops and you need to add funds manually. We’ll email your Account Admins when you’re low on funds so you know to take action. Manage your plan If you’re new to Customer.io, you can sign up for the Builder plan. Otherwise, learn more about upgrading and downgrading below. You can upgrade to Essentials or higher by reaching out to our team at win@customer.io. When you upgrade, everything you’ve built carries over—your message channels, profiles, and content. No migration is required. To downgrade from another plan, you’ll also reach out to our team at win@customer.io. --- ## How to add team members URL: https://docs.customer.io/accounts-and-workspaces/intro-account-access/ Account Admins can add team members and assign roles to grant the right level of access. If you're on a Premium or Enterprise plan, you can create custom roles to grant specific permissions to team members to meet your business' needs. Overview In Account Settings > Team Members, you can add team members to collaborate on campaigns, help with integrations, and more across your workspaces. Customer.io does not charge for team members, so feel free to grant access to anyone that you need! Only Account Admins can add team members. And each account can have up to 300 team members across all workspaces. For each team member, you’ll need to decide the level of access they should have to your account and to each of your workspaces. Account-level permissions are defined in Account Admin and Member roles. Workspace-level permissions are defined by standard or custom roles. In this example, Eve will be invited as a Member with the ability to grant Customer.io access to the account to help troubleshoot. She’ll be a Viewer with limited access to data for Workspace 1, have a custom role for Workspace 2, and have no access to Workspace 3. Account-level roles Every team member has an account-level role—Account Admin or Member. Account Admins have all permissions available across your account and every workspace. Think of them as account owners—they can manage billing, team members, API credentials, integrations, workspace data, create campaigns, and more. Account admins are always Workspace Admins; they have full permissions to the account and all workspaces. Members have partial access to your account. They can view but not manage team members or cookie settings. You can optionally give them the power to access billing and account info, manage API credentials, and enable access for support teams at Customer.io. To give them access to a workspace, you must assign a workspace-level role. Workspace-level roles After you decide the account-level permissions for your team member, you can assign them access to one or more workspaces. Choose from a list of standard roles to grant them predefined sets of permissions: Workspace Admin Author Viewer If you’re on a Premium or Enterprise plan, you can also create and assign your own custom roles with specific sets of permissions. Each team member can have access to one or more workspaces, but can only have one role per workspace. For instance, you can assign them as a Workspace Admin for your demo workspace but a Viewer of your production workspace. Hide sensitive data Account Admins and Workspace Admins can mark data as sensitive in the Data Index then choose which team members to hide this info from. This helps secure your data and comply with privacy regulations. How you hide sensitive data depends on the type of workspace-level role a team member has. For standard roles, admins choose “Hide sensitive attributes” when assigning the role. For custom roles (only available on Premium or Enterprise plans), admins choose “Hide sensitive attributes” when creating the role. This means everyone with the same custom role has the same visibility into data, whereas everyone with the same standard role could have different visibilities into workspace data. If that’s an issue, consider creating the standard roles as custom roles. Compare Account Admin to Member permissions Permission Account Admin Member Company Details View check_circle optional, must grant permission "Access billing & account info" Update check_circle optional, must grant permission "Access billing & account info" Workspaces View check_circle highlight_off Create check_circle highlight_off Update check_circle highlight_off Delete check_circle highlight_off Two-factor Authentication View check_circle highlight_off Update check_circle highlight_off SSO View check_circle highlight_off Update check_circle highlight_off API Credentials Create check_circle optional, must grant permission "Manage API credentials" View check_circle optional, must grant permission "Manage API credentials" Delete check_circle optional, must grant permission "Manage API credentials" Team Members View check_circle check_circle Create check_circle highlight_off Update check_circle highlight_off Delete check_circle highlight_off Plan Details View check_circle optional, must grant permission "Access billing & account info" Update check_circle optional, must grant permission "Access billing & account info" Cancel check_circle optional, must grant permission "Access billing & account info" Billing History View check_circle optional, must grant permission "Access billing & account info" Export check_circle optional, must grant permission "Access billing & account info" Cookie Settings View check_circle check_circle Update check_circle highlight_off Support Team Access View check_circle check_circle Update check_circle optional, must grant permission "Enable access for support teams" Experimental Features View check_circle check_circle Update check_circle check_circle Privacy & Data settings View check_circle check_circle Update check_circle highlight_off View team members In Account Settings, the Team Members table shows you: Who has access to your account When they last logged in Whether two-factor authentication (2FA) is enabled Their account-level roles Which workspaces they have access to Under Account, hover over to see additional account-level permissions each Member has. Under Workspaces, click the value to see a list of each role a team member has for each workspace. Add team members To add or remove team members, you must be an Account Admin.  Do you want to create a custom role? Custom roles are available on Premium and Enterprise plans. Before adding a team member, consider whether our standard workspace roles meet your needs. If not, create your own custom role THEN proceed to add a team member and assign the role. Go to Account Settings > Team Members. Click Invite team member. Enter the new team member’s first name, last name, and email address. An email address can only belong to one account, but people can switch between accounts if they have logins for multiple accounts. Grant account-level access. By default, we assign people the Member role. Members cannot manage team members or cookie settings, but you can optionally grant them the following permissions: Access billing & account info Manage API credentials Enable access for support teams If this person should have access to all account settings and workspaces, choose Account Admin instead of Member. If you’re adding a Member, decide how to grant workspace-level permissions. You can assign them one of these standard roles to each workspace: Workspace Admin, Author, or Viewer. If you created a custom role, you can also assign that here. Choose “All” if you want to give this person the same access across all workspaces. Then select the role from the dropdown. Choose “Custom” if this person should have different roles across multiple workspaces. Check the box next to a workspace then choose a role from the dropdown. For Authors and Viewers, decide whether to hide attributes. You can also assign the same role to multiple workspaces at once. Click Send invite. You’ll see the person you just created. Their last login will show as “Invite sent” until they log in. Your new team member will receive an email with a link to set a password. The link in the invitation expires after seven days. When the new team member sets their password, they can log in and get started with Customer.io! Note, if you’ve enabled SSO, they will not have to set a password. Assign the same role to multiple workspaces at once You can assign or remove a role across multiple workspaces for a single person. You cannot assign roles and permissions across multiple team members at once though. Go to Account Settings > Team Members. Click Invite team member to add a team member or click then Edit to modify an existing member. Locate workspace-level permissions at the bottom. Select Custom. Select the workspaces you want to assign the same access for. Click Select role. In the panel, select the role. For Authors and Viewers, decide whether to hide sensitive attributes. You can also assign “No access.” Click Confirm. The table will update to reflect your selection. Click Send invite or Save. If you created a new team member, they’ll receive an email invitation. If you updated a team member, they will not receive an email. Edit team members Any team member can edit their own name in Personal Settings. Account Admins can edit other team members’ names, email addresses, roles, and the workspace(s) they have access to in Account Settings. Go to Account Settings > Team Members. On the right, click then choose Edit and make your changes. Click Save. Changes to permissions take effect immediately. If you reduce someone’s permissions while they’re logged in, they may lose their work. For instance, if a team member is logged in as an Author and editing a campaign, then you assign them a Viewer role, they could lose unsaved changes to the campaign. Make sure you communicate with your team before editing their roles. Delete team members To delete or remove a team member, you must be an Account Admin. You might need to remove team members if they leave your organization. Removing a person does not impact content in your account—anything a person did as a member of your account will remain after you remove them from your team. If you only need to change someone’s settings—their name, email address, roles, or the workspace(s) they have access to—you can edit the team member instead. Go to Account Settings > Team Members. Find the team member you want to remove. Click then Delete. Confirm the action. Export team members To help you audit access to your account, Account Admins can export a list of team members and their level of access to each of your workspaces. Click Export to CSV at the top of Team Members. We’ll log this action in Exports of the first workspace created in your account (not necessarily the workspace you just accessed). You’ll also receive an email to download the CSV. Reset a team member’s password If a team member forgets their password or their password is compromised, you can reset it on behalf of a team member. Go to Account Settings > Team Members. Find the team member. Click then Reset password. We’ll send the team member an email with instructions to reset their password. FAQs Having trouble accepting an invitation? Did you create a trial account under your email address or did you previously belong to a different Customer.io account? Your email address can only belong to a single account in Customer.io. So, before you can accept the invitation, we need to free up your email address. If you created a trial account under your email address, cancel your trial. Contact us to disconnect your email address from the trial account. If you need access to multiple accounts, you can log into each one with a different email address and switch between them without logging out. Why can’t I see all of our team members in my list? Account Admins see all team members. Members only see team members assigned roles in one or more workspaces they have access to. Why can’t I see all of our workspaces in my list? Workspace Admins, Authors, Viewers, and people assigned custom roles only see the workspaces they can access. Ask an Account Admin to add or remove workspaces from a team member’s list. --- ## Assign standard roles URL: https://docs.customer.io/accounts-and-workspaces/assign-standard-roles/ Account Admins grant team members access to workspaces by assigning them workspace-level roles. A standard role comes with a defined set of permissions. To create roles with custom permission sets, check out [Custom roles](/accounts-and-workspaces/create-roles/). Overview You can assign standard roles to team members from Workspace Settings or from Account Settings > Team Members. A workspace-level role controls the set of permissions a team member has in a single workspace. We offer three standard roles: Workspace Admin (full access) Author (partial access) Viewer (view-only access) When you assign Author or Viewer roles, you choose whether to limit their access to sensitive data. This helps you keep your account as secure as possible. Remember that before you specify workspace-level persmissions, you have to choose an account-level permission: Account Admin or Member. Account Admins are always Workspace Admins in every workspace. This ensures Account Admins have full rights across each of your workspaces. Members can have different workspace-level roles for each workspace they have access to. Workspace Admin Workspace Admins have full access to all settings and features in a workspace. This is the only workspace-level role that can: Import or export user data Manage integrations Manage Webhook configurations Create, edit, or delete collections Mark attributes as sensitive in the Data Index and hide them from Authors or Viewers They cannot create or delete workspaces; only Account Admins can. Author Authors have partial access to workspace settings and features. They can manage some features like content and campaigns, but only view others like collections. They can create individual webhooks in campaigns, but can’t create or manage reusable webhooks in workspace settings. Account and Workspace Admins can decide whether to hide sensitive attribute data from authors.  Authors with sensitive data hidden can’t edit people Authors with sensitive data hidden have the same functionality as authors who can view all data, with one exception: they can’t edit people. Viewer Viewers have no access to workspace settings and partial access to workspace features. They have view-only permissions to all workflows, content, and data in a workspace. Account and Workspace Admins can decide whether to hide sensitive attribute data from viewers. Compare permissions across standard roles Workspace settings Permission Workspace Admin Author Viewer General Workspace Settings View check_circle check_circle highlight_off Update check_circle highlight_off highlight_off Messaging Settings View check_circle check_circle highlight_off Update check_circle check_circle highlight_off Email Suppression List View check_circle check_circle highlight_off Unsuppress check_circle highlight_off highlight_off Export check_circle highlight_off highlight_off Language Settings View check_circle check_circle highlight_off Update check_circle check_circle highlight_off Merge Options View check_circle check_circle highlight_off Update check_circle highlight_off highlight_off Message Limit View check_circle check_circle highlight_off Update check_circle check_circle highlight_off Subscription Center View check_circle check_circle highlight_off Update check_circle check_circle highlight_off Time Zone Match Test check_circle check_circle highlight_off URL Parameters View check_circle check_circle highlight_off Update check_circle check_circle highlight_off Webhook configuration View check_circle highlight_off highlight_off Create check_circle highlight_off highlight_off Update check_circle highlight_off highlight_off Delete check_circle highlight_off highlight_off Journeys Permission Workspace Admin Author Viewer Send messages check_circle check_circle highlight_off Campaigns Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Assign/Unassign Tags check_circle check_circle highlight_off Broadcasts Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Assign/Unassign Tags check_circle check_circle highlight_off Transactional Messages Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Assign/Unassign Tags check_circle check_circle highlight_off Deliveries & Drafts View check_circle check_circle check_circle People Create check_circle highlight_off highlight_off View check_circle check_circle check_circle Update1 check_circle check_circle1 highlight_off Delete check_circle highlight_off highlight_off Object Types Create check_circle highlight_off highlight_off View check_circle check_circle check_circle Update check_circle highlight_off highlight_off Delete check_circle highlight_off highlight_off Objects and Relationships Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Segments Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Import CSV check_circle check_circle highlight_off Assign/Unassign Tags check_circle check_circle highlight_off Ad Audiences Create check_circle check_circle highlight_off View check_circle check_circle check_circle Delete check_circle check_circle highlight_off Integrate check_circle highlight_off highlight_off Pause check_circle check_circle highlight_off Resume check_circle check_circle highlight_off Assign/Unassign Tags check_circle check_circle highlight_off Activity Logs View check_circle check_circle check_circle Data Index View check_circle check_circle check_circle Update attribute descriptions check_circle highlight_off highlight_off Export check_circle highlight_off highlight_off Tags in the data index Create check_circle check_circle highlight_off Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Assign check_circle highlight_off highlight_off Remove check_circle highlight_off highlight_off Integrations Create check_circle highlight_off highlight_off View check_circle highlight_off highlight_off Update check_circle highlight_off highlight_off Delete check_circle highlight_off highlight_off Data Import/Export Import check_circle highlight_off highlight_off Export check_circle highlight_off highlight_off Message Library View check_circle check_circle check_circle Assets View check_circle check_circle check_circle Delete check_circle check_circle highlight_off Upload check_circle check_circle highlight_off Email Layouts Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off In-app Message Library View check_circle check_circle check_circle Snippets Create check_circle check_circle highlight_off View check_circle check_circle check_circle Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Collections Create check_circle highlight_off highlight_off View check_circle check_circle check_circle Update check_circle highlight_off highlight_off Delete check_circle highlight_off highlight_off Design Studio - Styles View check_circle check_circle check_circle Create check_circle check_circle highlight_off Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Design Studio - Files View check_circle check_circle check_circle Export check_circle check_circle check_circle Create check_circle check_circle highlight_off Update check_circle check_circle highlight_off Delete check_circle check_circle highlight_off Design Studio - Feedback Feedback mode check_circle check_circle check_circle 1Authors with sensitive attributes hidden cannot edit people Hide sensitive attributes Premium This feature is available for Premium plans. Enterprise This feature is available for Enterprise plans. If you’re on a Premium or Enterprise plan, then Account and Workspace Admins can mark profile attributes as “sensitive” in the Data Index and decide whether to hide this data from team members. This redacts values but not attribute names from the workspace. Mark attributes as sensitive Account admins and workspace admins can mark profile attributes as sensitive in the Data Index. This redacts values but not attributes names from the workspace and helps ensure data privacy across team members. If you have a custom role that includes the Edit permission for the Data Index, you can also mark attributes as sensitive. You can also mark event attributes as sensitive independently. In the Events tab, select an event to find its attributes and mark them as sensitive. Profile and event attributes are separate—marking a profile attribute as sensitive doesn’t automatically redact event attributes with the same name. In the Attributes tab, click an attribute. Click Edit in the panel. Click “Make sensitive.” To unhide sensitive attributes, select the box to uncheck it.  Not seeing Make sensitive? Check that you’re an Account Admin or Workspace Admin in Team Members. If you are, then check whether you’re on a Premium or Enterprise plan or reach out to someone with billing access. Otherwise, you’ll have to upgrade for access. Click Save. Next, assign “Hide sensitive attributes” to team members. Choose “Hide sensitive attributes” when assigning standard roles After an admin marks attributes as sensitive in the Data Index, they must update Authors or Viewers so they can’t view these sensitive attributes: Go to Workspace Settings. Scroll to the bottom section “Who should have access?” Change the dropdown from “Show all attributes” to “Hide sensitive attributes” for each team member. If you’re an Account Admin, you can also assign this from Team Members: Under Workspace level permissions, specify Author or Viewer for workspaces. Choose Hide sensitive attributes from the dropdown. Save or invite your team member. These team members will now see values redacted for sensitive attributes. If they send test messages or webhooks from your workspace, those messages and responses will also contain redacted values.  Authors with sensitive data hidden can’t edit people Authors with sensitive data hidden have the same functionality as Authors who can view all data, with one exception: they can’t edit people.  Do you see the option Hide all attributes? If so, this is a legacy feature and you can learn more about how this limits authors access to data and functionality. --- ## Create & assign custom roles URL: https://docs.customer.io/accounts-and-workspaces/create-roles/ Account Admins can grant team members access to workspaces by assigning them workspace-level roles. If you're on a Premium or Enterprise plan, you can create roles with custom permission sets. Otherwise, you'll assign one of our [standard roles](/accounts-and-workspaces/assign-standard-roles/) with predefined permissions. Overview Customer.io has two types of workspace-level roles: standard roles and custom roles. Standard roles come with a predefined set of permissions, while custom roles allow you to specify your own set of permissions across content, integrations, and more. Account Admins can create custom roles in Account Settings > Roles. You can create roles from scratch or start from an existing role. A few things to keep in mind: Custom roles must minimally have all view permissions. When you create a role, you’ll see the checkboxes in the Permissions table reflect the minimum permissions needed. Permissions accumulate; to grant a permission on the right, you need all the permissions on the left. For instance, to delete campaigns, you must also be able to view, edit, and create them. Before you finish creating a role, make sure you specify whether people with this role should see sensitive data. Compare permissions Customer.io breaks permissions into four categories: Messaging & Content: Manage which workflows and content people have access to. People, Objects, & Activity: Manage the data in your workspace. Integrations, Imports, & Exports: Programmatically manage data, including Collections. Visibility of sensitive data across the workspace is controlled by the feature Hide sensitive attributes. Workspace & Message Channel Configuration: Manage workspace settings, integration of message channels, and your subscription center.  Every role must have view permissions These are checked when you first create a role. Messaging & Content Feature Permissions Campaigns View View all aspects of a campaign: settings, workflow, metrics, etc. Can export messages. Edit Edit all aspects of a campaign's workflow and settings, including messages. Permissions include:Create any message type. To create messages with Design Studio, see the related permission.Send or delete drafted messages.Send test messages.Manually end a person's journey. Create Create or duplicate campaigns. Schedule (start and stop) campaigns. Archive campaigns. Delete Delete campaigns. Broadcasts & Newsletters View View all aspects of a broadcast or newsletter: settings, messages, metrics, etc. Can export messages. Edit Edit a broadcast or newsletter, including messages:Create any message type. To create messages with Design Studio, see the related permission.Send test messages. Create Create or duplicate broadcasts or newsletters. Schedule (start and stop) broadcasts or newsletters. Delete Delete broadcasts or newsletters. Transactional Messages View View all aspects of a transactional message: settings, messages, metrics, etc. Can export messages. Edit Edit a transactional message, including the content:Create message content. To create messages with Design Studio, see the related permission.Send test messages. Create Create or duplicate transactional messages. To send a transactional message, you need an App API key. See Integrations for more info. Delete Delete transactional messages. Anonymous In-App Messages View View all aspects of an anonymous in-app message: settings, messages, metrics, etc. Edit Edit an anonymous message. Create Create or duplicate anonymous in-app messages. Schedule (start and stop) anonymous in-app messages. Delete Delete anonymous in-app messages. Content Library & Design Studio View View all aspects of the Content Library: assets, layouts, snippets, message library, and in-app messages. View Design Studio files: templates, emails, components, and styles. Can export Design Studio files. Edit Edit assets, layouts, snippets and Design Studio files. Can both create and edit global styles for Design Studio messages. Create Organize files and create or duplicate components, emails, and templates in Design Studio. Can upload files to Assets or Design Studio. Delete Delete files. People, Objects, & Activity These permissions control whether you can manage people, objects, segments, and ad audiences in the UI of your workspace. They do not control whether you can programmatically manage them through our APIs. See Integrations, Imports, & Exports for more. To redact people’s data from team members, you must mark profile attribute as sensitive in the Data Index and assign “Hide sensitive attributes” to the custom role. See Hide sensitive attributes to learn more. Feature Permissions People View View people including their attributes, devices, and more. Edit Edit people and their data. Manage subscription preferences and profile merges. Add or update attributes. Create Create people manually. To import a CSV of people, add Import permissions. To programmatically add people, add Integration permissions. Delete Delete people. Custom Objects View View all objects and object types. Edit Add/edit object attributes. Create Create objects. Add/edit relationships between objects and people. To import a CSV of objects, add Import permissions. To programmatically add objects, add Integration permissions. Delete Delete objects or relationships. Custom Object Types View View object types in Workspace Settings. You must also grant View permissions for Workspace Settings. Edit Edit object type details. Enable/disable object types in Workspace Settings. Create Create object types. Delete Delete object types. Segments View View all segments. Edit Edit segment details and conditions. Create Create or archive segments. Import/add existing people to manual segments. Delete Delete segments. Ad Audiences View View ad audiences (part of segments). Edit Edit ad audiences. Create Create ad audiences within segments. To integrate your ad network, add Integration permissions. Delete Delete ad audiences. Activity Logs View View all logged activities in your workspace. Integrations, Imports, & Exports Permissions for API keys For complete access to API keys, you need two permissions: The account-level permission “Manage API credentials”—gives you the ability to create and delete keys for all of our APIs: Track, App, and Pipelines The Edit permission for “Integrations”—gives you the ability to view and copy Pipelines API keys You assign “Manage API credentials” under Account-level permissions when adding or editing a team member. People will only see keys to the workspaces they have access to. You need the account-level “Manage API credentials” permission to access and copy Track and App API credentials; you don’t need this permission to access and copy Pipelines API keys. To view Pipelines API keys, you only need the Edit permission for “Integrations”. For more on the differences between our APIs and when to use each, check out our comparison article. Each key gives you access to the complete API, regardless of permissions set on your custom role. For instance, if you have the “Manage API credentials” permission, then you can create people and send events even if you don’t have the Edit or Create permissions for People. Permissions for data-out integrations You may need permissions under “Export data” and “Integrations” depending on the service you want to integrate: For Mixpanel or Amplitude, you need “Export data” permissions to manage them. For data warehouses like Amazon Redshift or Google BigQuery, you need “Export data” permissions and permissions for “Integrations”. For all other data-out integrations including reporting webhooks, you won’t need Export permissions, only permissions for “Integrations”. Breakdown of integration permissions Feature Permissions Integrations View View the integrations page, including the list of services your workspace is integrated with and our directory of options. Edit Manage existing integrations:Enable/disable sources and destinations.Create, edit, delete, enable, or disable actions that send data out of your workspace.View/copy Pipelines API keys.Create, edit, enable or disable reporting webhooks. Create Add integrations to your workspace. To add reporting webhooks, you only need the Edit permission. Delete Remove/delete integrations from your workspace, including reporting webhooks. Import data View View the Imports page (in Configure data, click More). Edit Edit imports labeled as "Action needed:" for instance, you can edit the mapping of CSVs to fields in your workspace but can't actually start an import. Create Import people, events, objects, and relationships and send test events in the UI:Import CSVs from the Imports page (in Configure data, click More). To import manual segments, add Segment permissions. You don’t need additional permissions to import people, events, objects, or relationships.Manually send events to test campaigns and more. Delete Cancel imports. Export data View View export logs on the Exports page (in Configure data, click More). Edit Edit data warehouse exports/syncs or your integrations with Mixpanel and Amplitude. Create Export data in your workspace and integrations:Download CSVs for segments, people, events, objects, and relationships.Export metrics for campaigns, broadcasts, and transactional messages.Create data warehouse exports.Integrate Mixpanel or Amplitude with your workspace. Delete Cancel exports. Collections View View all collections. Edit Edit collection details and upload new files. Create Add collections. Delete Delete collections. Data Index View View lists of attributes for people, objects, and relationships in your workspace. View list of events. Edit Edit attribute details like descriptions and tags. Mark profile attributes as sensitive. Workspace & Message Channel Configuration Feature Permissions Workspace Settings View View workspace settings, including your business context, subscription center configuration, and URL parameters. Edit Update workspace details like name, how to identify people, and your business context. Other permissions include: Hide sensitive attributes from Authors and Viewers.Manage automatic merge settings.Edit message limit settings.Edit Time Zone & Geolocation Settings.URL parameters: add/edit parameters and enable/disable them across your workspace. Message Channel Settings View View message channel settings and configuration in workspace settings. Edit Update message configurations in workspace settings. Add, verify, and delete email sending domains. Edit language settings, which includes the attribute Customer.io uses to identify a person's language preferences. Create Enable and configure SMS, in-app messages, push notifications, or Slack. Email configuration is controlled by the Edit permission. Delete Disable and remove SMS, in-app messages, push notifications, or Slack. Email configuration is controlled by the Edit permission. Subcription Center & Topics View View your subscription center in workspace settings. Edit Update your workspace's subscription center, including:CopyBrandingLocalization: Add, update, make live/not liveEnable/disable it across your workspaceAdd/edit topicsTo assign topics to campaigns, broadcasts, and newsletters, add edit permissions. Create N/A—The Edit permission controls the ability to add to the subscription center. Delete Delete topics or translations. Create a custom role To create a custom role, you must be an Account Admin on a Premium or Enterprise plan. Every custom role must have a minimum set of view permissions. By default, we check these for you when you start. Go to Account Settings > Roles. Click Create role. Enter a Name for the role. This is what you see when assigning roles to team members. (Optional) Add a Description so you remember what this role is for. (Optional) Under Quick Setup, decide whether to start from a standard role or previously made custom role. This will populate the permissions table. Edit permissions by checking or unchecking the boxes. Click the check box to the left of a permission to grant full access to it. Click individual checkboxes to the right to give granular access. If you want to hide sensitive data from team members with this role, choose Hide sensitive attributes from the dropdown. Click Submit. Next, assign the role to a team member. Hide sensitive attributes If you’re on a Premium or Enterprise plan, then Account and Workspace Admins can mark profile attributes as “sensitive” in the Data Index and decide whether to hide this data from team members. This redacts values but not attribute names from the workspace. Members with custom roles can also mark attributes as sensitive and hide them from team members depending on their level of access. If the team member has the Edit permission for the Data Index, they can mark attributes as sensitive. Then if they have the Edit permission for Workspace Settings, they can manage which Authors or Viewers can see these attributes. Note, this permission only lets them manage sensitive data for Authors and Viewers, not team members with custom roles. Mark attributes as sensitive Account admins and workspace admins can mark profile attributes as sensitive in the Data Index. This redacts values but not attributes names from the workspace and helps ensure data privacy across team members. If you have a custom role that includes the Edit permission for the Data Index, you can also mark attributes as sensitive. You can also mark event attributes as sensitive independently. In the Events tab, select an event to find its attributes and mark them as sensitive. Profile and event attributes are separate—marking a profile attribute as sensitive doesn’t automatically redact event attributes with the same name. In the Attributes tab, click an attribute. Click Edit in the panel. Click “Make sensitive.” To unhide sensitive attributes, select the box to uncheck it.  Not seeing Make sensitive? Check that you’re an Account Admin or Workspace Admin in Team Members. If you are, then check whether you’re on a Premium or Enterprise plan or reach out to someone with billing access. Otherwise, you’ll have to upgrade for access. Click Save. Next, assign “Hide sensitive attributes” to team members. Choose “Hide sensitive attributes” when creating custom roles To redact sensitive data from team members, you must be an Account Admin: While editing a role, choose “Hide sensitive attributes” from the dropdown in the Permissions table. Click Save. Any team members with that role will now see values redacted for sensitive attributes. If they send test messages or webhooks from your workspace, those messages and responses will also contain redacted values. Edit a custom role You must be an Account Admin to edit a custom role. Go to Account Settings > Roles. Click the name of the role. Modify the name, description or permissions as you see fit. You can also change whether to hide sensitive data. Click Save. Changes to permissions take effect immediately. If you reduce permissions and the role is assigned to someone logged in, they may lose their work. For instance, if you edit a role down to view only permissions for a campaign and the team member is currently editing an email in a campaign, they won’t be able to save their changes. Make sure you communicate with your team before editing roles in use. You can find who will be impacted by permission changes on the Roles page under Users. We do not notify people if their permissions changed. Duplicate a custom role You must be an Account Admin to edit a custom role. Duplicating a role only creates the role; you must manually assign it to team members. Go to Account Settings > Roles. Click and choose Duplicate. Click the new role to edit it. It will have “(Copy)” in front of the original role’s name. Click Save. Delete a custom role To delete a custom role, you must be an Account Admin. You must remove the role from all team members before you can delete it. Go to Account Settings > Roles. Click and choose Delete. Confirm your action. --- ## Switch between accounts URL: https://docs.customer.io/accounts-and-workspaces/manage-accounts/ Accounts in Customer.io are tied to your email address. If you have multiple Customer.io accounts, you can log into them simultaneously and switch between them without logging out and back in. You might manage multiple Customer.io accounts if you’re a partner managing client accounts, you work for a company with multiple independent subsidiaries, or you maintain separate test and production accounts. You can login to up to 30 accounts and switch between them from the dropdown in the upper left corner of the header. Each account requires a separate email address—you can’t use the same email for multiple accounts. Log in to another account Click the workspace selector in the top-left corner of the header. Click your account at the top of the list and select Add another account. Log in with the email address of your other account. After you log in, the new account appears in the account switcher dropdown, and you’ll be able to switch to and from it without logging out. Switch accounts Accounts remain logged in for up to 14 days—though the exact timeframe depends on your session expiration policy. If you try to switch to an account and it doesn’t appear in the dropdown, your session may have expired. You’ll need to add it again. Click the workspace selector in the top-left corner of the header. Under Accounts, select the account you want to switch to. Logging out When you add multiple accounts, logging out requires you to log out of all of your accounts. The next time you log in, you’ll need to add your accounts again. Things to know Each account needs a different email address. You can’t log into multiple accounts with the same email. Workspaces are still per-account. You’ll only see the workspaces for the account you’ve selected. SSO accounts work too. If an account uses SSO, you can add it to the switcher the same way. Up to 30 accounts. You can have up to 30 accounts in the switcher at once. Sessions expire independently. Each account’s session follows its own session expiration policy. By default, sessions last 14 days, but an account’s admin can set this to as little as 1 hour. When a session expires, it’ll disappear from the account switcher and you’ll need to log in to switch to it again. --- ## Edit Account Information URL: https://docs.customer.io/accounts-and-workspaces/editing-account-information/ You can edit your account information, including your account's primary technical contact, billing address, and logo.  To edit this information, you must be an Account Admin or Member with the account-level permission, “Access billing & account info.” All of these items can be edited in your Account Information. Uploading a logo In your Account Settings, click Edit Account Information. Under Basic information, you can add, replace, or delete a logo: Please note that the file must be more than 200x200 pixels. The logo image will be automatically cropped to a square. We accept BMP, JPG, JPEG, PNG, and GIF files. We do not support SVG. After you save, the logo will appear in the top-left of each workspace in the navigation bar: Changing your invoice email address In Account Settings, one of the contact details you can customize is the “Invoice email address.” This is the email to which we send your account’s invoices. By default, this address is the one you signed up with. If you want to change who we send invoices to, click Edit Account Information in the top right, change the address, then add it once more in the confirm field. Click Save changes at the bottom right. If you don’t customize the address or if you leave the field empty, invoices will go to the email address you signed up with when you created your Customer.io account. Changing your legal contact By default, your legal contact is the same as the email address you signed up with. If you want to change who we send legal updates and information to, go to Account Information, click Edit Account Information in the top right, change the address, then add it once more in the confirm field. Click Save changes at the bottom right. If you don’t customize the address or if you leave the field empty, invoices will go to the email address you signed up with when you created your Customer.io account. Specifying a technical contact Also in Account Settings, the “Primary technical contact” is the person to whose attention we bring any engineering-related problems. By default, we let all your account admins know. If you click “Specify email address,” you can specify someone else we should contact. For example, if there’s an operational issue with your Customer.io setup, we can directly notify the person you think is best-placed to deal with it. To do this, go to Account Information, click Edit Account Information in the top right, then add and confirm your technical contact’s email address. Click Save changes at the bottom right. If you don’t specify a custom email address (or leave the field empty), we’ll notify all account admins. --- ## Manage your API credentials URL: https://docs.customer.io/accounts-and-workspaces/managing-credentials/ You can find your API credentials under *Account Settings* > *API Credentials*. You'll use different API credentials to send data to, and get data from, each workspace in your account. It's generally a good security practice to use different credentials for each integration and rotate credentials regularly.  To access your credentials, you must be an Account Admin or Member with the account-level permission, “Manage API credentials.” Track API Keys vs App API Keys There are two types of API keys in each account. Track API Keys Track API keys are used to send behavioral tracking activity (/events and attribute updates) into your workspace. API requests using the https://track.customer.io/api/v1/ endpoint use these tracking API keys. App API Keys App API keys are used to trigger messages and broadcasts, or programmatically retrieve data from your workspace for analysis, troubleshooting, or reporting. API requests to https://api.customer.io/v1/api/ use App API Keys. Because App API Keys can be used to trigger messages and access people attributes and delivery information, these keys are shown only once when created and stored as hashed values in Customer.io. Make sure to store these keys (and all API keys) in a safe and non-public location. Restrict API access by IP Address By default, access to the API is not restricted by IP address. On the Manage API Credentials page, you can create an allow list. When you create an allow list, any addresses on the list with valid credentials can use the API. Addresses that are not on the list are denied access to the API, even with valid API keys. To create an allow list: Go to the Manage API Credentials page. Click Add IP Address. Enter the IP Address you want to allow and the Workspace you want to grant access to. Click Add IP Address. Repeat this process for the addresses you want to allow access to and the workspaces you want to grant each IP address access to. Where can you find them? You can find your API keys by navigating to Account Settings > API Credentials. In this settings area, you can add new API credentials for any workspace, as well as see when all of your credentials were created and last used. You can also rename or delete/deprecate them should you need to. Adding new credentials Whenever you create a new workspace, we create a default set of “primary” API credentials for you. You cannot delete these until you create a new set. To do this, click the button to add a new set of credentials, then give them a name and select a workspace. When you add them, you’ll be able to see them on your Integrations page for that workspace, and in the settings area. Renaming or deleting existing credentials You can rename or delete a set of credentials. Editing is just a matter of renaming and saving. If you want to delete a set of credentials, we ask you to enter their name once more, just to be sure. Keep in mind that this cannot be undone, and that if any currently-running integrations use those credentials, you update those integrations before deleting! Keep in mind that you need at least one set of Track API credentials for each workspace. If you only have one set left, we’ll let you know that you need to create a new one before you delete that set: Choosing integration credentials JavaScript snippet When sending data to Customer.io for a given workspace, we give you the option of choosing which credentials you’d like to use. You can use the dropdown to select them for either the JavaScript snippet or the API: Segment If you’re connecting your Segment account to a Customer.io workspace and you have multiple key pairs, you need to select a set of API credentials to enable that connection: Need help? If you have any questions about API credentials in Customer.io, please let us know! --- ## Security Best Practices URL: https://docs.customer.io/accounts-and-workspaces/security-best-practices/ From our side, we’re working hard to keep [your data safe](/journeys/security/), but there are also a few measures you, as an Account Admin, can take to make your Customer.io account security bulletproof. 1. Do not share login credentials Add named accounts for all of the people in your team who will be collaborating on your messaging content and strategy. Customer.io allows you to add as many team members as you need - with different levels of access. 2. Limit your team to the access level they need When you add team members, you can set their permissions at the account and workspace levels. If you manage a large team, you’ll likely want to limit the access levels that people have to ensure that they can only see and edit what they need to. At the Account level, you probably want to limit the number of Admin-level team members. Account Admins have full access to your account and all your workspaces. You can limit access by setting your teammates account roles to Member. Then you can determine which workspaces each member has access to, and the level of access they have within each workspace. 3. Ensure your login id is a valid email address If your Customer.io account still uses an old email address you no longer have access to, update it as soon as possible. You run the risk of being locked out of your account in case of an attack, and you’re also missing out on important notifications sent by our team. 4. Use a secure password It’s crucial to use a strong password that cannot be easily guessed or cracked by malevolent individuals trying to retrieve your customer database. While remembering long, complicated passwords might be difficult to do on your own, a password manager can make this process painless. 5. Enable Single Sign-on (SSO) If you use a common single sign-on provider like Google Suite, Okta, or Microsoft Entra ID, you can enable SSO to manage access to your Customer.io account. This adds an extra layer of protection to team members logging in to the account and removes the risk of managing extra passwords. We also support SSO with SAML 2.0 for Cloudflare, Jumpcloud, and Okta. If you use one of these providers, you can enable SAML to manage access to your Customer.io account. 6. Notify other Account Admins ASAP if you’ve lost access to your account If you lost your 2FA device and are worried someone else might use it to gain access to your company’s Customer.io account, ask another Account Admin to remove your user and re-add it. The initial 2FA authentication method will become invalid and you can add a new one. If you’re the only Account Admin, our technical support team can verify your identity and reset your access. 7. Review your team’s exports regularly This will allow you to detect any suspicious activity early on and take action against the offender. If you need additional information regarding the downloads performed by a particular team member like the IP address and which exports they retrieved, reach out to our support team and we’ll be happy to help. 8. Remove team members who leave your company or change roles It’s best practice to audit access to your account to keep your account secure. Have you removed team members that are no longer with your company? Did someone change departments and no longer needs access? To remove access, go to Team Members, click , and then select Delete next to that person’s email address. If you’re an account admin, you can export team members to help you audit access to your workspaces. Click Export to CSV at the top of Team Members. You can also periodically review the Last Login date on the Team Members page to determine if your teammates still need access to your account. 9. Keep your contact information up to date Periodically check your legal and technical email address on a regular basis so we can notify you about potential issues with your account. You’ll find your contact information under Account Settings > Account. 10. What to do about compromised credentials Following the best practices in this document should help prevent (and minimize the impact of) unauthorized access to your account. But, if you suspect your API credentials have been compromised or an unauthorized individual has gained access to your account, you should: Remove the malicious actor. This may be a former team member or someone whose access to your account should have lapsed! Rotate your API credentials. This ensures that the bad actor can’t affect your data. --- ## Two-Factor Authentication URL: https://docs.customer.io/accounts-and-workspaces/two-factor-auth/ Two-Factor Authentication (2FA) is an additional layer of security on your Customer.io account. By default, we require you verify your login attempt through a magic link sent to your email. You can also enable 2FA through an authentication app. What is two-factor authentication? Two-Factor Authentication (2FA) is an additional layer of security on your Customer.io account. By default, we require you verify your login attempt through a magic link sent to your email. You can alternatively enable 2FA through an authentication app. Why do you need it? If your regular password is ever compromised or stolen, 2FA ensures that only you can log into your account because only you have the magic link or authentication code. This in turn secures your messaging system, preventing bad actors from spamming your customers. We require 2FA for all team members who use Customer.io as an identity provider (non-SSO).  Managing authentication with SSO If you and your team members use SSO, we do not require 2FA. Rather, you must manage authentication settings with your SSO provider. 2FA via email link (default) By default, all non-SSO accounts must verify their login attempt via email links. Alternatively, Account Admins can require 2FA via authenticator app. Check your account settings to view and manage your 2FA method. After submitting your username and password, you will receive an email from Customer.io with a link that signs you in.  You must click this link on the same device and browser that you submitted your username and password on. You cannot start in Safari and finish in Chrome or start on your laptop and finish on your phone. 2FA via auth app Only Account Admins can set a Customer.io account to require 2FA via auth app for all team members. Once it’s required, team members will have to set up their auth app to continue using Customer.io. Install an authentication app First, make sure you have a two-factor authentication app installed. We support anything that uses Time-Based One Time Passwords (TOPT). Some well known examples are: iOS: Google Authenticator, Microsoft Authenticator, Authy, Duo Mobile, and 1Password Android: Google Authenticator, Microsoft Authenticator, Authy, Duo Mobile, and 1Password Windows Phone: Microsoft Authenticator, Duo Mobile Desktop: 1Password, Authy (Chrome ext.) Visit your account settings For Account Admins Account Admins must first enable 2FA via auth app on their own account. Go to Settings > Account Settings > Team Members then click Edit your settings. Click Manage to start the process, and have your authentication app at the ready. The click Enable. Make sure you download your recovery codes. After setting up your personal account, you must then go to Settings > Account Settings > Security and click Enable Auth App to require all team members use an auth app.  Any team members actively using Customer.io who have not setup 2FA will be redirected to set it up. They will not be able to continue using Customer.io until they do. For team members Once an Account Admin requires 2FA via auth app, Customer.io directs team members to download their recovery codes and set up their auth app. Download your recovery codes (and keep them safe)! At the beginning of the process, you’ll get ten recovery codes. Download, print or copy these and don’t lose them! You’ll need them to regain account access if you ever lose access to your device. Once you’ve done this, press “Next”. Scan the QR code, and enter your authentication code You will then see a QR code; scan it with your app, and enter the authentication code in the input box. You can also enter this code into your app manually. Success! That’s it! Two-factor authentication is set up. You can find your backup codes or generate new ones from your personal account settings. Click Edit your settings followed by Manage under 2FA via auth app. Just remember to get rid of the old codes if you do generate new ones. Frequently asked questions Can I enable two-factor authentication for the rest of the users in my account? 2FA via email link is automatically enabled across all accounts using Customer.io as an identity provider. As an Account Admin, you can enable 2FA via auth app on your personal account and then enable it for all team members. If you are an Account Admin, you can see which type of 2FA is enabled on your team member accounts, but it’s not currently possible to enable or disable 2FA for individual team members. I lost my device/I’m locked out! What do I do? No problem! We’ve got a few options to get you back in: 1. Use a recovery code Grab your backup codes from wherever you’ve saved or printed them, and use one of those at this login screen instead of your authentication code: Note that once you use a code, you can’t use it again. 2. Have a team member remove and re-add you If you have other team members with Account Admin privileges, have one of them remove your account and re-add you on the Team Members page. You’ll have to re-set a password and set up two-factor authentication again, but you’ll regain access. Team member accounts have no account data associated so it’s completely safe to be removed and re-added. 3. Contact us If you don’t have your backup codes or other Account Admin team members, you’ll need to email our customer support team (at win@customer.io) from the email address associated with your login, and we’ll work with you to verify your account details and identity. This option may take longer, but we have this process in place to help keep your account secure from social engineering attacks. --- ## Single Sign-on (SSO) URL: https://docs.customer.io/accounts-and-workspaces/login-with-sso/ Organizations that need enhanced security requirements can configure their Customer.io account to use Single Sign-on (SSO).  To enable/disable SSO for your account, you must be an Account Admin. How to set up SSO The process for configuring SSO will depend on your specific identity provider (IdP). Customer.io has dedicated integrations with the following providers and protocols: SSO with Google for organizations SSO with Google for consumers SSO with Okta—see SSO with SAML 2.0 for another option SSO with Microsoft Entra SSO with OpenID SSO—works with a variety of providers SSO with SAML 2.0—works with Okta, Cloudflare, and JumpCloud  We only support Service Provider (SP)-initiated SSO This means your teammates must start the login process from Customer.io’s login page at fly.customer.io/login, not from their identity provider’s app card or dashboard. Frequently Asked Questions What is OpenID Connect and how does it differ from SAML? OpenID Connect is a security standard for logging into applications, built on the OAuth 2.0 protocol. It uses an additional JSON Web Token (JWT), called an ID token, to standardize areas that OAuth 2.0 leaves up to choice, such as scopes and endpoint discovery. It is specifically focused on user authentication and is widely used to enable user logins on consumer websites and mobile apps. Learn more about OpenID Connect. Security Assertion Markup Language - SAML - is also a widely used authentication protocol for logging into apps but built on the SAML 2.0 specification. Unlike OpenID Connect, SAML allows you to include various attributes in the SAML statement sent to the SaaS application based on the mapping of attributes in your Identity Provider (IdP). How do I require 2FA with SSO? When you use SSO with Customer.io, you must enable 2FA within your identity provider. (Before you can enable SSO in Customer.io, you must disable Customer.io’s 2FA feature.) Require 2FA through Google Enable 2FA in Okta Enable MFA in Microsoft Entra I’m able to log in with Google. Is that the same as Google SSO? No, it is not. “Log in with Google” is an option on the Customer.io sign-in page to quickly and securely log in, but team members can still use their email and password during sign-in. To block team members from signing in with an email/password, you must enable Google SSO on the Account Security page. Manage team members How do I add a new team member to my account after enabling SSO? For most integrations, you’ll need to add/remove team members from your IdP and add/remove them from Customer.io. Your team members must have matching email addresses across Customer.io and your IdP. However, when you set up a SAML integration and enable SCIM, team members you add or remove from your IdP will automatically be added or removed from Customer.io. You can only manage roles and permissions within Customer.io. In either case, a new team member will receive an email prompting them to log in. They won’t need to set a password, just enter their email into the Customer.io login page. Is there any sync between my Identity Provider (IdP) and the team member list in Customer.io? By default, no. You have to maintain access in both your IdP and Customer.io. However, if you enabled SCIM 2.0 on a SAML integration, your team member list will update based on those added and removed from your IdP automatically. Keep in mind, you can only manage roles and permissions within Customer.io. Make sure your team members have the right access. Can I manage team member roles through my Identify Provider (IdP)? It’s not possible to define a user’s permissions via your IdP. You can only manage a user’s permissions in Customer.io. Manage team permissions Reach out to support at win@customer.io if you’re still experiencing issues with enabling SSO. I have two (or more) Customer.io accounts. Can I link both to my Identity Provider (IdP) account? Yes, you can, by adding two Customer.io applications within your IdP account. Repeat the steps that apply to your provider for each Customer.io account. Make sure the usernames in each app in your IdP match the corresponding usernames in Customer.io: Account Settings > Team Members. How do I force an account to log out? To invalidate someone’s active sessions in your Customer.io account, delete the team member from Account settings > Team Members. Do you support SLO? We do not support Single Logout (SLO) - the ability for someone to log out of one application which triggers a log out from all applications using the same credentials. For instance, your team member can’t log out of Customer.io and then trigger a log out of Salesforce. SSO with Google You can either set up organization-level SSO or consumer-level SSO with Google. If your company uses Google Workspace to manage your employees’ identities, you can set up SSO with Google for organizations. If you use consumer-level Google accounts, like gmail addresses, check out SSO with Google for consumers. Learn more about Google identity management. SSO with Google for organizations If you’re using Google Workspace (formerly G Suite) to manage your employees’ identities, then you can enable organization-level SSO for teammates who use Customer.io. If you have a consumer-level Google account (like a gmail address), go to SSO with Google for consumers. To get started, you must: Have a Google Workspace account Be an Account Admin in Customer.io Disable 2FA for logging into Customer.io  Ensure your team members have Google Workspace identities After you enable Google SSO, only team members in both your Google Workspace account and Customer.io will be able to log in. Any team members with an external email address won’t be able to log in until they are given an identity (email address) in Google Workspace and updated accordingly in Customer.io. To enable SSO with Google Workspace: Make sure your team members have email addresses that match those in your Google Workspace account. Go to Account Settings > Security > Enable Single Sign-On and click Google SSO. At the bottom of the page, click Sign in with your Google account. This will open a Google authorization window asking you to choose the account you’d like to use with Customer.io. Make sure to choose the Google email account used by you and your team to log in—anyone with a different email domain will not be able to log in. After your complete setup, our system logs out all team members in Customer.io. They must sign in with Google moving forward; they can no longer sign in with username/password. SSO with Google for consumers If you don’t have organization-level Google SSO, you can sign in with a consumer account. If you own a gmail address like alice@gmail.com, then your gmail account is a consumer account. Similarly, if you use the Create account link on the Google Sign-In page and during signup you provide a custom email address that you own, such as alice@example.com, then the resulting account is also a consumer account. You can sign up to Customer.io using this method. But if you signed up using a username and password, you can link your Google account later. To enable SSO with Google for your consumer account: Go to Settings > Personal Settings and click Link Google Account. Enter your password. For future logins, you won’t be able to sign in with a username/password. Rather, you’ll click Sign in with Google to proceed. By default, you will have to sign in with two-factor authentication via email. But you can change this to 2FA with an authentication app in settings instead. SSO with Okta Requirements To configure SSO with Okta, you must have: an existing Okta account, an Account Admin role in Customer.io, and Disable “Require 2FA” for your Customer.io account. Supported Features This implementation supports User Authentication. After a team member is added to your Customer.io account, they’ll be asked to authenticate with Okta in order to log in. No other features (like profile sync, provisioning, etc.) are supported at this time. Okta SSO Configuration Steps Setting up Okta SSO with Customer.io is a two-step process. You’ll first add the Customer.io Application to your Okta account. Then, you’ll configure your Customer.io security settings to connect to Okta.  After setup is complete, team members will be immediately required to re-login to Customer.io using their Okta credentials. Their current work may be interrupted. Part 1: Add Customer.io Application to Okta Add Customer.io to your Okta account by going to your Applications page, clicking Browse App Catalog and searching for Customer.io. On the opened page, click Add to install the Customer.io integration. You’ll be asked to provide an Application label (Customer.io) and configure whether the application should display to users or auto-submit with the browser plugin. Select your preference and click Next (these can be changed later). Next, you’ll see Step 2: Sign-On Options. Select OpenID Connect and click Done. After you click Done, the application will be added to your Okta org and is ready to be assigned to your team members. Click Assign to add the team members or groups who will be accessing Customer.io, including yourself! Once you’ve added People, keep the Okta window open and move to Step 2 below. Part 2: Configure Okta SSO in Customer.io Open a new window and get ready to set up SSO in your Customer.io account. Log in to Customer.io and navigate to the Security page of Account Settings. On the Security page, select Configure SSO. Select Okta SSO with OpenID Connect to show the configuration settings. In the Configuration form, enter the following information: Okta Organization URL: This can be found in your Okta dashboard header and typically follows the format of https://[companyname].okta.com. Learn more about Okta Org URLs. Okta Application Client ID and Client Secret: Go back to your Okta window and look for the Client ID and Client Secret on the Sign On tab of the Customer.io Application. Click Authenticate your Okta account to confirm the connection and enable SSO. Once the connection is authenticated, you’ve successfully enabled SSO for you and your team members. SSO with Microsoft Entra Requirements To configure SSO with Entra ID, you must: Have an existing Azure account Have an Account Admin role in Customer.io, and Disable “Require 2FA” for your Customer.io account Register a new app You can find more info on setup in Microsoft Entra’s Quick Start Guide. Log into your Microsoft Azure account and go to Microsoft Entra ID. On the Overview page, click Add at the top and select App registration. Enter a display Name for your application. This helps you distinguish between your registered apps. This will not appear in Customer.io. Click Register to complete initial setup. Configure your app To finish configuring your registered app, go to Authentication > Add a platform. Select Web. Add this redirect URI: https://fly.customer.io/oauth2/redirect. Select Configure. Add credentials Select Certificates & secrets > Client secrets > New client secret. Enter a Description for your secret. Change the Expiration time period if you need.  Keep track of your expiration timeline Customer.io doesn’t know when your client secret will expire. You’ll need to track your client secret’s expiration date outside Customer.io to maintain a smooth sign-in process Select Add. Your client secret is under Value. Keep your Microsoft Azure account open to finish integrating with Customer.io. Finish setup in Customer.io Go to Account Settings > Security > Enable Single Sign-On (SSO). Select Entra ID SSO with OpenID Connect. Back in Microsoft Entra ID, select Overview from the left-hand menu. Then click Endpoints from the top menu. Copy the OpenID Connect metadata document and paste into OpenID configuration document URL in Customer.io. The URL should follow this pattern: https://login.microsoftonline.com/{tenant_id}/v2.0/well-known/openid-configuration. Copy the Application/Client ID and paste into Azure Active Directory Application Client ID in Customer.io. In Microsoft Entra ID, select Certificates & Secrets. Copy the Value of your client secret and paste into Azure Active Directory Application Client Secret in Customer.io. Your configuration will not authenticate if you use the Secret ID; make sure you use the Value. In Customer.io, select Authenticate your Microsoft Azure Active Directory account. You will be prompted to sign in using Azure. You will see a success banner upon completion or information to help you remedy any issues in your configuration. SSO with OpenID You can enable SSO for providers beyond Google, Okta, and Azure using our generic OpenID SSO option in Account Settings. OpenID SSO works with any provider that is compliant with OpenID Connect, such as OneLogin and Auth0. Requirements Like with other IdPs, these are the general requirements to get started: Have an existing account with the provider Have an Account Admin role in Customer.io, and Disable “Require 2FA” for your Customer.io account Set up OpenID SSO Configure your IdP: Register your app with your IdP. Configure the app. Create a client secret. Set up the login URL and redirect URI to send a user to the correct place after your identity provider verifies your users’ identities. The image below is an example from OneLogin. Login URL: https://fly.customer.io/login Redirect URI: https://fly.customer.io/oauth2/redirect Set up your Customer.io account: Go to Account Settings > Security > Enable Single Sign-On (SSO). Select OpenID SSO with OpenID Connect. Fill in the following using the equivalent fields in your IdP: OpenID Configuration Documentation URL Client ID Client Secret Select Authenticate. You’ll be prompted to sign in using your IdP. You will see a success banner upon completion or information to help you remedy any issues in your configuration. SSO with SAML You can enable SSO for some providers using our SAML option in Account Settings. The table below shows the providers we’ve tested and whether or not they work with Customer.io. Because our solution complies with the SAML 2.0 specification, it may work with other providers. Contact us if you need help with a specific provider. Provider Supported Cloudflare ✅ JumpCloud ✅ Okta ✅ Google ❌ Azure ❌ Auth0 Not Tested Requirements To get started, you must be an Account Admin in Customer.io, and disable 2FA for your Customer.io account. Set up SAML Follow these steps for JumpCloud or Cloudflare. For Okta, check out their specific instructions below. Register your app with Cloudflare or the IdP JumpCloud. Set SAML Subject NameID Format to email address, just like the NameID. Set up your Customer.io account: Go to Account Settings > Security > Enable Single Sign-On (SSO). Select SAML 2.0. Paste in the Metadata URL from the equivalent field in JumpCloud or Cloudflare. Select Generate your SAML SSO metadata. Finish configuring your IdP: Copy these fields from Customer.io and paste them into the corresponding fields in the SSO app you just made in your IdP: Service Provider (SP) Entity ID ACS URL Click Export SP Certificate in Customer.io, and then upload this to SP Certificate in your SSO app. Save your changes. Set up SAML for Okta Set up your Customer.io account: Go to Account Settings > Security > Enable Single Sign-On (SSO). Select SAML 2.0. Add a temporary link to the Metadata URL. We’ll update this later. It must start with https://. Select Generate your SAML SSO metadata. You’ll see a banner indicating that team members must log in with SSO now. Note, if they’re already logged in, they won’t be kicked out. Create a SAML app in Okta’s Admin Console. Go to Applications > Applications, and click on Create App Integration. Select SAML 2.0, and click Next. Enter an App name so you know it’s for Customer.io, and click Next. In SAML Settings, set the Single sign-on URL as the ACS URL from Customer.io. Set the Audience URI as the SP Entity ID from Customer.io. Change Application username to Email, and then click Next. Click Finish. Finish configuring Okta in Customer.io. Click Update metadata URL, and type in the value from your Okta app. Click Save. If applicable, continue to Enable SCIM.  You do not need to export the SP certificate from Customer.io to input into Okta. Provision access After you’ve set up SAML SSO, decide how you want to manage team member access: Add your colleagues to both your IdP and Customer.io for them to successfully log into Customer.io or Set up System for Cross-domain Identity Management (SCIM) 2.0 provisioning so you only have to add/remove team members from your IdP. In either case, your team members must log in from Customer.io, not from within your IdP.  We do not support Just-in-Time (JIT) provisioning. Set up auto-provisioning After authenticating your SSO provider, you can set up SCIM 2.0 provisioning to automate the management of your team members in Customer.io. Once enabled, you can automatically add and remove team members in Customer.io when they are added or removed from your identity provider. JumpCloud In Account Settings > Security > Enable Single Sign-On (SSO), go to your authenticated identity provider, and click Enable SCIM. In JumpCloud, go to Applications > Identity Management > Configuration Settings. Choose SCIM API under API Type. Choose SCIM 2.0 under SCIM Version. Copy the Base URL and Token from Customer.io and paste them into the corresponding fields in your IdP. Enter a valid email address for your SSO under Test User Email. For the test to work, this address cannot already be a team member in Customer.io. Click Test Connection in the upper right. The test user will be created and deleted from Team Members in Customer.io. Toggle OFF Group Management; this is not supported through Customer.io’s implementation. Under Attribute Mapping, click exclude for passwords. Set the following fields and click include to the right of each: UserName = Company Email Name.FamilyName = Last Name Name.GivenName = First Name Click Activate in the top right. SCIM 2.0 is now enabled on your account! Back in Customer.io, go to Team Members and you’ll see the users you’ve provisioned in your SAML app. Change their roles and permissions from this screen; their level of access is managed within Customer.io, not JumpCloud. Cloudflare In Account Settings > Security > Enable Single Sign-On (SSO), go to your authenticated identity provider, and click Enable SCIM. Go to Cloudflare One’s docs for more information on Generic SAML applications. Back in Customer.io, go to Team Members and you’ll see the users you’ve provisioned in your SAML app. Change their roles and permissions from this screen; their level of access is managed within Customer.io, not Cloudflare. Okta In Account Settings > Security > Enable Single Sign-On (SSO), go to your authenticated identity provider, and click Enable SCIM. In Okta’s Admin Console, go to Applications > Applications, and click on your app. Go to the General tab, and select SCIM under Provisioning. Then click Save. Click the Provisioning tab, choose Integration, *and fill in the info: SCIM connector base URL: paste in the Base URL from Customer.io. Unique identifier field for users: type in email. Supported provisioning actions: select Import New Users and Profile Updates, Push New Users, and Push Profile Updates. Authentication Mode: change to HTTP Header. Authorization: paste in the Token from Customer.io. Click Save. Click To App within the Provisioning tab, and choose Edit. Select these three options: Create Users Update User Attributes Deactivate Users Click Save. Finally, click the Assignments tab, and assign users as needed. User assignments sync to Customer.io. Note, Okta does not automatically provision users assigned to the Okta application from before SCIM provisioning was in place. It’s easiest to remove then re-add the user to the application to provision them. Back in Customer.io, go to Team Members and you’ll see the users you’ve provisioned in your SAML app. Change their roles and permissions from this screen; their level of access is managed within Customer.io, not Okta. Login After you provision access for your team members, they must: Accept the invite email from Customer.io to join your account Go to Customer.io and log in; team members cannot log into Customer.io from within your IdP. Disable SSO  Disabling SSO will affect all of your team members After you disable SSO, we log your team members out which may interrupt their work and cause them to lose unsaved changes. Go to Account Settings > Security > Enable Single Sign-On (SSO). Click Disable and confirm the action. All team members will need to use Customer.io credentials to sign in moving forward.  Do not sign up for a new account after SSO is disabled If any of your team members do not have or remember their Customer.io credentials after disabling SSO, send password reset emails from Team Members. You must have the Account Admin role to do this. Troubleshooting I’m getting an error when I click Authenticate. If you’re still getting an error after double checking your organization URL, client ID and client secret, check to see that you’ve added yourself to the Customer.io app. I’m getting an error when trying to log in using my identity provider’s app card. This is expected behavior. Customer.io only supports Service Provider (SP)-initiated SSO, which means you must start the login process from Customer.io’s login page at fly.customer.io/login. You cannot log in directly from your identity provider’s app card or dashboard. I’m using an aliased email (like ami+cio@customer.io) as my Customer.io login. Can I still SSO? Yes. Simply update your username in your IdP to your aliased email in the scope of the Customer.io app. For Google SSO: you can login using ami@customer.io and have access to CIO accounts linked to ami@customer.io, ami+cio@customer.io, etc. For Okta, Azure, and other IdPs: it has to be a 1-to-1 match. If your account has ami+cio@customer.io, your CIO account must be ami+cio@customer.io (not ami@customer.io). I’m unable to log in after SSO was enabled. What do I do? The email address you use to log into Customer.io must match the email registered in your IdP. An Account Admin on your account can verify or update your email in Customer.io on the Team Management page. Reach out to support at win@customer.io if you’re still experiencing issues logging in. --- ## Account Regions (US and EU) URL: https://docs.customer.io/accounts-and-workspaces/data-centers/ When you create an account with Customer.io, you select your region, which determines where we store your data: in the United States (US) or European Union (EU). Our EU data center is based in Belgium. We ensure that all of your customers’ data is stored in the region that you choose. So, if you select the EU region, all of the information about your customers, known as People, is stored exclusively in data centers within EU member countries. You cannot have workspaces in different regions. All customer data in your account—all of your workspaces—resides within the region you select when you set up your account. Does my region affect feature availability? Your region does not affect feature availability for our products. The only difference between regions is where your data resides. We release features for Journeys to both our US and EU regions simultaneously. While Automatic Geolocation Data Collection is available in both regions, it’s on by default in our US data center and off by default in our EU data center. If you’re in the EU data center, you’ll need to enable it manually if you want to collect geolocation data automatically. GDPR compliance and data residency Both our US and EU data centers are GDPR compliant. Your choice of region does not determine GDPR compliance—both regions meet all GDPR requirements and data protection standards. Customer.io is a certified member of the EU-US Data Privacy Framework (DPF). This certification means Customer.io is recognized as providing an adequate level of protection for personal data transfers from the EU to the US. As a result, data transfers between the EU and our US data center comply with GDPR requirements, eliminating the need for EU customer data to be stored exclusively within the EU. You might still choose our EU data center for data residency preferences, business requirements, or organizational policies that require data to remain within the EU/EEA region. Does my region affect how I use Customer.io? Your region does not change your experience with, or how you use, fly.customer.io. Whether your account is based in the US or EU data centers, you’ll access your account and workspaces using the same URL. When you use our APIs or libraries, however, you should specify your region. Specifying your region in the API By default, we assume that your account is in the US region. If your account is based in our EU data center, you should specify your region when you consume our APIs or libraries to route traffic to the appropriate region. You can determine your account’s region programmatically using the data center endpoint. This may simplify cases if you maintain an integration with Customer.io. When using the API with an EU-based account, append the API subdomain with -eu. For example, you’d use the Pipelines API at https://cdp-eu.customer.io. When using libraries, you’ll specify your region when you initialize the library. If you don’t provide a region, we assume that your account is in the US data center.  We redirect the track and transactional API calls where necessary, but not other APIs If you don’t specify that your account is in the EU region, we assume that your account is in the US. We redirect Track API requests from US endpoints to the EU so that they don’t fail, but this traffic passes through US servers and may cause data to be logged in the US. We do not redirect other APIs; if you use the wrong region for the App API, your request will fail (401). Automatic Geolocation Data Collection The automatic geolocation data collection setting is on by default in our US data center and off by default in our EU data center. This is a workspace-level setting, so you may need to enable or disable it in each workspace where you want to use (or not use) it. To enable or disable it: Go to Workspace Settings > Time Zone & Geolocation Settings. Enable or disable Automatic Geolocation Data Collection. Note that turning this setting on doesn’t backfill data. You need to identify people from our JavaScript client or mobile SDKs to capture or update geolocation data. How do I know what region my data is in? If you’re an Account Admin, your account region appears under Settings > Account Settings > Data and Privacy. Account and billing information is only available to administrators; users with other roles will not see the account region. You can also use the API to determine where your account is located. Test your credentials with the Account Regions endpoint. curl --request GET \ --url https://track-eu.customer.io/api/v1/accounts/region \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" Migrate your account to the EU data center Because Customer.io is DPF-certified, you don’t need to migrate to the EU data center for GDPR compliance.  We do not support data center migrations We don’t support data center migrations except in extenuating circumstances. Reach out to win@customer.io or your customer success manager for additional information. --- ## Enable experimental features URL: https://docs.customer.io/accounts-and-workspaces/beta-experimental-features/ The *Experimental Features* setting lets you try out features we're working on and presents you the opportunity to tell us how you feel about them. How it works We aim to release features early and often. Sometimes when we release new features, we’ll mark them as beta or experimental. In both cases, we think the features are stable and ready to use (though we can’t say that they’re bug-free), but we want your feedback! Beta features are early releases of a features we’re actively working on. In general, we think beta releases are stable but they may not solve all the problems we want them to, or—in the case of our SDKs—may need scaled testing. In general, beta features are available to everyone in your workspace. Experimental features are typically things that we hope improve our interface—and your experience with it. You have to enable experimental features for yourself. When you enable an experimental feature, you won’t affect anybody else in your workspace. Feature Enabled Range of effect beta by request or automatically account-level (all workspaces) experimental manually on the Experimental Features page per user (all workspaces) Experimental AI features We’re working on features that use artificial intelligence (AI) to help you build messages and take advantage of some of the more advanced features of Customer.io. But AI is tricky and we want to make sure that you can use AI-enabled features responsibly at your discretion. So, in most cases, we’ll expose AI-based features alongside our other experimental features. We do not use your customer data to train AI models. You’ll find experimental AI features in your account settings by clicking in the upper-right corner of Customer.io and going to Experimental features. If you’re an administrator and you want to disable access to AI features all together, you can disable Customer.io AI in your account’s Privacy, Data, & AI Settings. We currently offer the following AI features: Email Content Analysis: This feature uses AI to review your email content and receive an analysis report highlighting areas of opportunity to deliver a message quicker to the audience you’re trying to build relationships with. In-App Message Suggestions: This feature suggests in-app designs and content for you when you drag an in-app message into your campaign. You can even use prompts to generate more refined suggestions. Think of our AI suggestions like starter templates—when you use a suggestion, you’ll still edit it to add images, update text, links, and so on. But our suggestions can help you get started with a layout, design, and content of your messages. Segments: When you create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., sometimes it’s hard to determine the exact conditions that’ll return the group of people you’re interested in. This feature suggests segment conditions based on the descriptions of your segments. Beta features In the Customer.io UI, we generally release features as “beta” for two reasons: We’re actively soliciting your feedback to make sure that we built the best possible version of the feature to suit you. The beta feature is a completed part of a larger suite of features. We’re actively working on the suite and we want you to know that you can expect more changes. Beta releases are typically stable. We’ve tested things before we’ve made them available to you. We just want you to know that a feature is new, may see minor changes going forward, and is likely something we’re expanding on. And we want to hear from you about these features! We want to build things that support your needs. If they don’t—or you know how we could do better—you should let us know! Beta SDK releases Beta SDK releases are releases that we’ve tested internally, and expect to release to release as a stable feature. We release betas to grant you access to the latest features and give you time to handle changes in your app before a stable release. We recommend that you don’t use beta releases in your production apps unless specifically advised by Customer.io. Enable or disable an experimental feature When you enable an experimental feature, you enable it for yourself in every workspace that you have access to. You do not affect other users. In the upper right of the Customer.io interface, click and click Experimental features. Turn experimental features on or off. --- ## Workspaces in Customer.io URL: https://docs.customer.io/accounts-and-workspaces/workspaces/ Workspaces are a way of working with multiple products, sites or apps from a single Customer.io account. Everyone starts with one (its name is the same as your account name), and you can add or remove them as needed. Manage workspaces  To create, edit, or delete a workspace, you must be an Account Admin. Click your workspace in the upper left corner and then click Manage all workspaces to see a list of your workspaces. From here you can add, edit or delete workspaces (if you have more than one). Add a workspace By default, new workspaces use both email and id as identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace.. You can change identifiers after you create your workspace.  If you’re on our Essentials plan… You can create two workspaces. If you already have two workspaces, you’ll either need to upgrade to our Premium Plan or delete a workspace first. To get to your Workspaces page, click your workspace in the upper left corner and then click Manage all workspaces. Click Add Workspace. Give your new workspace a name and a custom color to help you differentiate between your workspaces. Set the default send behavior for the workspace: Send messages normally: All messages send as defined in your workflow. Test email delivery: Emails will send to a defined test address; other messaging types (Slack, webhooks, etc.) send as normal. Never send messages: Message delivery is disabled. (Optional) Disable open tracking for emails. Select the team members who can manage and access the new workspace. You can’t disable access to a workspace for an Account Admin; Account Admins can access all workspaces in an account. Click Save. When you’ve finished adding your workspace, you can switch between workspaces from the main navigation bar. Edit a workspace To edit an existing workspace, go to the Workspaces page and click ‘Edit’ in the Manage settings: You can change your workspace’s name, delivery settings or access permissions for team members. You can also change the color assigned to the workspace to help you differentiate between them. Delete a workspace Deleting a workspace permanently removes all data associated with the workspace—including campaigns, emails, people, etc. You may want to export your data before you delete a workspace. To remove a workspace, go to the Workspaces page and click Delete. You must type your workspace’s name (case-sensitive) to delete it.  Deleted workspaces are not recoverable. Deleting a workspace permanently deletes all campaigns, emails, customers, deliveries, metrics, and data contained within a workspace. Make sure you’re prepared to lose this data before you delete a workspace. General workspace settings After you create a workspace, you can change settings by going to Settings > Workspace Settings > General Workspace Settings. This is where you define the identifiers for people, which you need to add or update people in your workspace. You’ll decide how to identify people by email and/or id. You can always identify people by cio_id. This chart shows what it means to update email or id based on these settings: flowchart TD e["Update email #40;where id and email are identifiers#41;"] --> f{Is it set?} f -->|yes|h{"Is #quot;cio_id or id#quot; enabled?"} h -->|yes|j[You can set email with id] h -->|no|i[You must update email with cio_id] f -->|no|g[You can update email with id] a["Update id #40;where email and id are identifiers#41;"] --> b{Is it set?} b ---> |yes|k[You can update id with cio_id] b ---> |no|c[You can set id with email] n["Update id #40;where id is the only identifier#41;"] --> o{Is it set?} o --> |yes|p{"Is #quot;cio_id or id#quot; enabled?"} o --> |no|q[Person does not exist] p --> |yes|r[You can update id with cio_id] p --> |no|s[You cannot update id] Enable or disable email as an identifier New workspaces treat email as an identifierThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. by default, but you can disable it. Disabling email as an identifier only prevents you from using an email address to add or update people; you can still use it as an attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages.. The default configuration—in which email is an identifier—can be helpful for cases where you initially identify people who are interested in your product (leads) by email address and then assign them IDs later, when they become customers. When you enable email as an identifier, we’ll automatically merge people who have the same address.  Changing your workspace identifiers may delay processing of people and event data It can take up to a day to revalidate your data based on your new configuration settings. During this time, your workspace pauses processing people and event data and your Workspace performance dashboard will reflect this. Processing resumes once validation is complete; no data is dropped. To enable or disable email as an identifier: Go to Settings > Workspace Settings and click General Workspace Settings. Click update your workspace next to email. Carefully read about the behaviors that will change for your workspace. When you’re ready, click Change configuration. If you’re enabling email as an identifier, we’ll merge people that have the same email addresses. Behaviors for people with an email but not an ID When you enable email or ID as identifiers, you may notice slightly different behaviors from other workspace types. Reporting Webhooks: the customer_id key is null if you add a person without an ID (by email only). You should update your endpoints to use the new identifiers object. See our webhook documentation for more information. Segment Source events: After you migrate, message opens, clicks, etc. for people without an id are sent with the email address as an anonymousId. You can map these anonymous events to another destination or drop them. Allow updates to email using ID If your workspace uses both email and ID as identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace., this setting determines the requirements to change a person’s email after you set it. If you created your workspace after January 28, 2022, this setting is on by default. In general, if you see a lot of failed “Attribute Change” requests in your workspace, or a significant number of your identify calls fail, you might try turning this setting on. You can also try enabling the Multi-identifier profile merge. cio_id: you must identify a person by cio_id to change their email address. A person’s cio_id is set when you create them, and is immutable. It acts as a canonical identifier across changes to a person’s id or email. For example, imagine that a person already has an email address and you want to change it. The request on the left below identifies a person by their cio_id and would succeed. The one on the right would fail. Successful requestFailed request curl --request PUT \ --url https://track.customer.io/api/v1/customers/cio_1234 \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data '{"email":"new.email@example.com"}' curl --request PUT \ --url https://track.customer.io/api/v1/customers/id1234 \ --header "Authorization: Basic $(echo -n site_id:api_key | base64)" \ --header 'content-type: application/json' \ --data '{"email":"new.email@example.com"}' cio_id or id: you can change a person’s email address in any request that includes a person’s id. This makes it significantly easier to change your audience’s email address using our JavaScript snippet, the API, any of our reverse ETL integrations and so on.  This setting does not effect CSV imports You cannot change a person’s email using a CSV import unless you use the Update setting and identify people by their cio_id. Message sending Under “How do you want to send messages?” you can choose from: Send messages normally Send emails to a test address—note that this only applies to emails Never send messages When you first sign up for Customer.io, your account will send emails to a test address. This lets you send emails to a person in your account without verifying a domain so you can test your emails right away. You can see who will receive test emails in the top-left of your workspace. If you’ve integrated with other channels like in-app, those will send normally when “send emails to a test address” is selected.  How test mode affects sending While your workspace is in test mode, all emails are sent from Customer.io’s test domain and address (test@customeriotest.com) rather than your own verified domain. Because of this, you can’t select your own verified domains for sending, and we can’t track metrics like delivered, opened, and clicked back to your workspace. This keeps test activity from affecting your actual campaign metrics. To send from your own domain, verify your domain and switch to Send messages normally. By default, we’ll send test emails to the address that created the account. Account Admins and Workspace Admins can select another team member to receive test emails from the dropdown. The address for “send emails to a test address” is the same for all team members in the workspace; it’s not unique to each team member. After you verify your domain and are ready to send emails, click Send messages normally. Unless you are explicitly sending a test email, this setting ensures your messages send to actual recipients, as specified in the To field of your message. You can also choose to Never send messages of any type (email, in-app, etc.). This disables all message delivery and ensures no one can receive a message from any campaign, broadcast, or transactional message. You might choose this option for test workspaces. Disable open tracking Open tracking relies on a tracking pixel—a small image—in your messages; when a user opens an email containing the tracking pixel, the image loads from a remote location and tells us that a user opened the message. However, open tracking is not always reliable. In the interest of user privacy, Apple and the makers of other email clients offer options to prevent images like tracking pixels from loading when users open messages; this prevents us from tracking open events. In some cases, email clients and corporate firewalls immediately download images when a person receives an email, causing us to record an open even if a person didn’t actually open the message. To disable open tracking at the workspace level and prevent tracking pixels from being added to your emails entirely, you can go to Settings > Workspace Settings > General Workspace Settings. This setting overrides the message-level Track opens and link clicks in this message setting. Disabling open tracking this way can help you both respect your audience’s privacy and prevent you from focusing on sometimes-unreliable open rates, as opposed to clicks or conversions. When you disable open tracking, open metrics for emails may still appear in some charts or tables (in campaign metrics, etc), but will always show zero results. Whether you continue to track opens or not, we recommend setting conversion criteria for your messages and tracking link clicks as more reliable ways to determine the success of your messages and campaigns. Assign workspace access If you isolate workspaces by project, client, app, etc, you may want to limit who has access to each workspace. You assign access based on workspace, so your team members can have full access to one workspace and partial access to another. Only Account Admins can manage access for team members. They can change access when: creating or editing a workspace adding or editing team members Account Admins have access all workspaces in the account. You cannot disable an Account Admin’s access to a workspace. FAQs What data is shared across workspaces? No information is shared between workspaces. Workspaces are essentially separate instances of Customer.io. Each workspace has its own people, campaigns, metrics, and other data. How is billing calculated across workspaces? We bill based on people, objects, emails sent, and Data Pipelines API calls across all your workspaces. Check out How We Bill for more info. Is there any way of sharing data between workspaces? Workspaces are completely separate instances of Customer.io, each with their own people and associated data. This prevents sharing information and potentially messaging the wrong person. However, you can copy workflow actions across workspaces. What about testing? Are workspaces a way to do that? Although not designed specifically for testing, workspaces can be used as a sandbox to set up testing/staging environments. Each workspace is assigned its own set of API keys and are completely separate from your other workspaces. Once you’re ready to migrate a Campaign or message from test to production, you can copy entire workflow actions from one campaign to another across workspaces. How do I move a workspace to another account? To move a workspace from one account to another, you’ll need to contact Customer.io support. This process requires manual assistance from our team to ensure data integrity and proper migration. Click Need help? at the top of your workspace and then submit your request through Get help with an issue. --- ## Date and Time Data in Customer.io URL: https://docs.customer.io/accounts-and-workspaces/datetime-displays/ Throughout Customer.io, we try to give you as much insight as we can into what happens and when. We know that timing of messages is critical, so we want to make sure you know exactly when your messages are sent or drafted, for example, or when a particular user was created. Overview Throughout Customer.io, we try to give you as much insight as we can into what happens and when. We know that timing of messages is critical, so we want to make sure you know exactly when your messages are sent or drafted, for example, or when a particular user was created. We use your computer’s current time zone for all times, with a few exceptions: In exports, we use Coordinated Universal Time (UTC) timestamps. In segmentA segment is a group of people in your workspace. Use segments to trigger campaigns, track membership over time, or fine-tune your audience. There are two types of segments: data-driven and manual. Data-driven segments automatically update when people start or stop matching criteria. Manual segments are static. membership metrics and exports, we use Coordinated Universal Time (UTC). Our dashboard metrics, which are in Eastern Standard Time (EST). Details Where we can, we show the full time, date, and your time zone, like this: When we show you a shorter date (because we know that it’s overkill sometimes), you can hover over it to get more information, like this: Note: Daylight Savings Time If you notice that some times are displayed in EDT, PDT, or MDT (for example) but others are shown as happening in EST, PST, or MST respectively, don’t worry! This just means that one event happened while daylight savings time was being observed, and the other did not. It allows us to be just that little bit more accurate. Timestamps in the Activity Log The Activity Log has two types of dates: Timestamp and Processed At. Timestamp represents the date and time listed on an event. If you don’t set a timestamp, we use the date-time when we receive the event. Processed at is the date and time when we process an event. Anonymous events are not processed, and therefore do not have a “processed at” time. If there is a significant difference between the two, it could be for one of these reasons: An anonymous event was merged to a profile. Anonymous events are not processed until they’re associated with a person, so an anonymous event may be timestamped well before you identify a person and the event is associated with the person. You may have manually set a timestamp on an event. This typically happens when you backdate an event, or want to log the exact date-time that an event occurred and you don’t immediately send the event to Customer.io. Customer.io experienced a processing delay. FAQ How is time zone determined in Customer.io? The time zone we display is set to your computer’s current time. The only place where this is an exception is on our dashboard metrics, which are in EST: What time zone are exports in? Whenever you export your data from Customer.io, timestamp values are standardized to Coordinated Universal Time (UTC). You can convert these however you like; if you need more information, the EpochConverter site is a good resource. --- ## Case sensitivity and your data URL: https://docs.customer.io/accounts-and-workspaces/case-sensitivity/ When you send data to Customer.io, you should format it consistently so it's easier to reference in liquid to personalize messages and send data to other platforms. When is data case sensitive? AttributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. names are always case sensitive. You can save both first_name and First_Name to a profile; those fields will not merge together. This is also the case for reserved attributes like timezone and unsubscribed. You must lowercase these attributes to use time zone match or our global subscription functionality. Attribute values are case sensitive in 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}}. logic. For instance, if a person has a favorite attribute with the value Sports, the else statment below would render because of the different casing: {% if customer.favorite == "sports" %} Come to the game! {% else %} Try swing dancing! {% endif %} Liquid logic - this is true for strings and booleans: {% if customer.favorite == "sports" %} This renders for people with a value of `sports`. {% else %} This would render for people with any other value, including a different casing of sports. {% endif %} {% if customer.unsubscribed == true %} This renders for people with a value of `true`. {% else %} This would render for people with any other value, including a different casing of true. {% endif %} In-app message page rules When is data NOT case sensitive? AttributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. values are case insensitive in UI filters, segment conditions, and workflow logic conditions. For example, people with a language attribute value of Fr or fr will both receive the email variant for fr. EventSomething that a person in your workspace did. Events can trigger campaigns, add people to segments, etc, and you can use properties from events to personalize messages. names - in most cases, an event course_signup sent to your workspace is the same as Course_signup. When you’re sending data out of Customer.io, make sure you’re sending the format your destinations would expect. Conversion criteria are the exception to this rule. Event names used in conversion criteria are case sensitive, so course_signup and Course_signup would be treated as distinct events in this case. Trigger names for transactional messages --- ## Search your workspace URL: https://docs.customer.io/accounts-and-workspaces/workspace-search/ Our universal search feature can help you find campaigns, segments, templates, and other items in your workspace. Press ⌘K on Mac or CtrlK on Windows and Linux to open the search dialog from anywhere in your workspace.  Universal search is independent of the Customer.io AI toggle in your Privacy, Data, & AI settings. Disabling Customer.io AI doesn’t affect universal search. What you can search for The universal search feature cannot search for people or their data. It only searches workspace configuration—the names and content of things you’ve built in Customer.io. To find specific people, use the activity log, people page, or segments. Universal search helps you find the following items in your workspace: Campaigns by name Segments by name Templates by name or content Broadcasts (newsletters and API-triggered) by name Actions (workflow actions) by name Navigation shortcuts to jump to pages like settings or metrics How search works When you type a query, universal search uses AI-enhanced ranking to surface the most relevant results. You don’t need to type an exact match—the search tries to capture the intent behind your query and rank results accordingly based on the keywords you use. For example, searching for “welcome” might return your “Welcome series” campaign, a “New user welcome” template, and a “Welcome email” broadcast. The word “welcome” might not even appear in the names or descriptions of these items, but the search will try to rank results that represent the same concept. Privacy and data handling Universal search indexes workspace configuration data to make things easier to find—things like campaign names, segment names, attribute names, template content, and other metadata. We do not index or access your customers’ personal information. PII is not indexed. Search indexes workspace data (names, descriptions, types), not the values of your customers’ attributes or events. All search data stays within Google Cloud infrastructure. Search data is not used to train AI models. No data is shared with external AI providers. --- ## Session cookies and expiration URL: https://docs.customer.io/accounts-and-workspaces/login-session/ We manage your session and the sessions of your team members in the Customer.io user interface with a secure HTTP-only cookie. As an administrator, you can limit the duration of sessions as necessary to fit your own security policies. How it works We manage your session in Customer.io using a secure HTTP-only cookie. The cookie can only be read by the browser, protecting your session from unauthorized access and malicious cross-site scripting (XSS) attacks. By default, our session cookies last for 14 days. But, as an administrator, you can set the duration of sessions for everybody on your team to fit your security policy—down to 1 hour. If a session expires while a person is logged in, they’ll be prompted to re-authenticate, but they won’t lose their work. The login prompt appears wherever the user is in the product; it doesn’t force them back to the login screen!  The session duration setting applies to all team members You can’t set different session duration periods for different team members or roles. Set login session expiration If you’re an administrator, you can set session duration between 14 days and 1 hour. Anybody who is logged in when you change the setting will have to re-authenticate. They won’t lose their work; they’ll just need to re-enter their credentials to continue! Go to Settings > Account Settings and click Security. Under Login session expiration, set the session duration period. Your changes take effect immediately. --- ## Allowlist our IP addresses URL: https://docs.customer.io/accounts-and-workspaces/ip-addresses/ If you use a firewall or an allowlist, you may need to allow traffic from our IP addresses to support traffic from Customer.io based on the features you use. If you have a custom SMTP server, use our webhooks, or have an outgoing data warehouse integration, you’ll need to allow the appropriate addresses below. Make sure you allow the appropriate addresses for your region! Email (Custom SMTP) and Webhook IP Addresses US RegionEU 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 136.111.237.157 34.10.127.179 Reverse ETL IP Addresses (data-in) 34.29.50.4 34.22.168.136 35.222.130.209 34.78.194.61 34.122.196.49 104.155.37.221 Data Warehouse IP Addresses (data-out) 34.71.192.245 34.118.255.179 35.188.196.183 34.76.143.229 104.198.177.219 34.78.91.47 35.184.88.76 35.187.55.80 34.72.101.57 104.199.99.65 34.123.199.33 34.76.81.2 35.222.137.61 34.77.146.181 34.68.113.63 34.140.234.108 35.240.84.170 35.195.54.15 34.38.105.52 104.155.66.230 34.76.119.61 34.140.67.73 34.78.74.81 --- ## Customer.io, GDPR, and you! URL: https://docs.customer.io/accounts-and-workspaces/gdpr-faq/ [GDPR (General Data Protection Regulation)](https://gdpr-info.eu/) is a regulation to strengthen and unify data protection for EU citizens. In a nutshell, its goal is to protect individuals' data and grant them rights over access to and usage of that data.  Read the text in full! We’d encourage you to read the text in full and consult with your legal counsel for the most complete understanding of the GDPR. Customer.io helps you use customer data to help send personalized messaging. We understand the power of this capability, and with it the importance of helping you protect that data. We’ve stated our public commitment to the regulation and, as part of that, have either built new features for the product, or made them more accessible. Documentation Directory This is a collection of documentation for features of Customer.io which help you comply with GDPR article 24 (responsibility of the controller). End user data exports: How to export attributes for a group of users End user data deletion & suppression: Honor delete requests and the right to be forgotten Delete or suppress users to comply with GDPR requests You can remove a person (through our UI, our API, and other integrations) from your workspace to remove their profile data from Customer.io. But, to fully honor a GDPR “right to be forgotten” request, you may want to suppress a user. Suppressing a person in Customer.io: Deletes the person’s profile Redacts activity attributed to the person Prevents you from adding a person with the same identifiersThe attributes you use to add, modify, and target people. Each unique identifier value represents an individual person in your workspace. (generally their ID or email) to your workspace The last point is crucial: while you can remove profiles from your workspace, you can also add them back. Suppressing a user’s profile ensures that you can’t add a person with the same email or ID to your workspace. --- ## Security URL: https://docs.customer.io/accounts-and-workspaces/security/ The information below discusses the policies and procedures Customer.io has in place when dealing with Customer data.  See our full security policy You can read more about our security practices on our website and download our compliance documents. Login sessions are managed via secure HTTP cookie We manage your session in Customer.io with a secure HTTP cookie. The cookie can only be read by the browser, protecting your session from unauthorized access and malicious cross-site scripting (XSS) attacks. If you’re an admin you can set the expiration time for sessions between 1 hour and 14 days depending on your security policy. See Session cookies and expiration for more information. We use Bank-grade encryption Customer.io uses 128-bit SSL encryption for all authenticated sessions. We support TLS version 1.2 and above. This means that data sent to the Customer.io API as well as data retrieved through the Customer.io management interface is protected. You are limited to retrieving only your data Data stored in your Customer.io account is only available to you. Each request to retrieve data from Customer.io must be authenticated. Furthermore, requests are restricted to the currently logged-in account. Requests made to the Customer.io internal API require a logged in account and will not return successfully otherwise. We never store your credit card info When you purchase a paid Customer.io subscription, your credit card data is not transmitted through nor stored on our systems. Instead, we depend on Stripe, a company dedicated to this task. Stripe is certified to PCI Service Provider Level 1, the most stringent level of certification available. Stripe’s security information is available online. We require 2FA on all non-SSO accounts If you use Customer.io as your identity provider (IDP), we require two-factor authentication (2FA) for you to finish signing in. By default, we email you a magic link after you submit your username and password. You can also use an authenticator app such as 1Password or Google Authenticator. If you use Single Sign-on (SSO), we do not require 2FA. In this case, authentication is managed by your SSO provider. We log you out after 14 days If you use Customer.io as your identity provider, that is, you sign in using an email and password, your session will expire after 14 days. This includes accounts with two-factor authentication. This reduces the risk of hackers taking advantage of your account in a shared computing setting. The session will expire regardless of whether you’re active or not within the platform.  Re-authenticate before your session expires If you are logged into the platform, you’ll see a modal window two hours ahead of session expiration prompting you to re-authenticate and renew your session for another 14 days. If you don’t re-authenticate after two hours, you’ll be automatically logged out and lose all unsaved changes. Similarly, if you need to reset your password, you will have to follow the “Forgot password?” flow which includes logging out and so, losing unsaved changes. We recommend using a password vault to avoid this situation. Customer.io employees have restricted access Access to our servers is restricted to infrastructure engineers and maintenance staff. This access is granted through a unique key that can be revoked if necessary. Customer.io employees cannot access your account, workspaces, or the data you store in Customer.io—your people, messages, etc—unless you grant them access in Settings > Privacy, Data, & AI. To grant access, you must be an Account Admin or Member with the account-level permission, “Enable access for support teams.” You might grant us access to help you troubleshoot issues, etc. Enabling the Support Team Access or Customer Success Team Access (for Premium customers) settings grants authorized Customer.io personnel access to your account. You can disable this setting to revoke access at any time. Access Setting Max Time Grants Support Team Access 180 days Read-only access to members of the Technical Support team—and people operating in a Technical Support capacity—while providing support. Customer Success Team Access 180 days or indefinitely Read and write access for your Primary CSM. Other CSMs have read-only access Reporting a security concern If you see a hole in security or an area that we can improve, send us an email to win@customer.io. We’ll work with you to make sure we understand the issue and address it. We consider security correspondence and vulnerabilities our highest priorities and will work to address any issues that arise ASAP. --- ## Mobile and App Store Privacy URL: https://docs.customer.io/accounts-and-workspaces/mobile-privacy/ We're dedicated to safeguarding your users' privacy while empowering you with the flexibility to handle data as you need to. Apple and Google both require developers to provide information about their privacy practices in the App Store and Google Play Store. Here's what you need to know about our mobile SDK privacy practices. Take immediate action before May 1, 2024 Beginning May 1, 2024, Apple’s App store requires SDKs to include a privacy manifest describing the data they collect and use. This includes Customer.io’s SDKs! To ensure that your app meets Apple’s requirements, you must update the Customer.io SDK to a version that includes the privacy manifest. If you don’t, your app may be rejected from the App Store. Update to the following versions (or later): iOS: ≥ 3.1.0 or ≥ 2.13.0 React Native: ≥ 3.6.0 Flutter: requires iOS ≥ 2.13.0. Follow our update instructions to update the iOS native package in Flutter. Expo: requires React Native ≥ 3.6.0. Install the latest React Native SDK and run expo prebuild --clean.  You must add Expo privacy declarations manually Apple has a known issue parsing PrivacyInfo files for Expo. You’ll need to copy the reasons included in our iOS library’s node_modules/customerio-reactnative/ios/PrivacyInfo.xcprivacy file to your app’s PrivacyInfo.xcprivacy file or the configuration in the app.json. What data does Customer.io SDKs collect? Our SDKs collect user IDs and product metrics to enable app functionality, analytics, and message personalization. You can pass other data to our SDKs, but we don’t collect that information automatically. If you pass email addresses, phone numbers, or other personal information to our SDKs, you need to declare that data in your app’s privacy settings or in the respective Google Play and Apple App Store privacy declarations. Why doesn’t Customer.io declare email as a collected data type? Our SDKs don’t automatically collect your users’ email addresses. Your application collects email addresses and passes them to our SDKs. If you use email addresses, you you should declare that you collect email addresses in your apps’ privacy settings. Is the data Customer.io collects linked to users? It’s common practice to link data with users’ personal identities to enhance engagement through targeted communications. If you, like most of our customers, link data to users, you need to declare that you collect and link data (userId and product metrics) to users when you submit your app to Apple. Our SDKs’ privacy manifests don’t declare that data is linked to users because there are some use cases where you might not connect the data to a user. Apple’s App Store and privacy requirements Apple requires developers to provide information about their privacy practices in the App Store. This includes a privacy manifest that describes the data collected by your app and how it’s used. Our SDKs (the versions listed above or later) include a privacy manifest declaring the data directly collected by Customer.io. You can see the complete manifest in each individual module in our iOS SDK. But, in summary, Customer.io only directly collects a unique user and links product metrics to your users when you identify them. You can collect additional information, like email addresses, phone numbers, usernames, etc, and pass it to our SDKs. You’ll need to declare the kinds of data you collect in accordance with Apple’s standards in both your app’s own privacy manifest and when you answer Data privacy questions about your app in the App Store. Google Play Store and privacy requirements Google requires privacy disclosures for your app and updates. You’ll answer some questions about data safety in the Google Play Console’s App Privacy tab. Google has an article to help you complete your disclosures. When you use our SDKs, you’ll need to disclose how you collect and use data with Customer.io. As explained above, we only collect user IDs and product metrics. You’ll need to declare additional data you collect, like email addresses, phone numbers, and so on. --- ## Privacy URL: https://docs.customer.io/accounts-and-workspaces/privacy/ We all deserve to know what's happening with our data. This page helps you understand our privacy settings and some basics about our privacy policies. Account privacy settings You’ll find information about our privacy policy and some settings related to your privacy under Settings > Privacy, Data, & AI. Cookie settings The Performance and troubleshooting setting is enabled by default and lets us use cookies within fly.customer.io that help us track activity that we can use to troubleshoot issues in your account and workspaces. You can disable these cookies, but it may hamper our ability to troubleshoot problems you might have.  You must be an Account Admin to manage cookie settings. Customer.io account access By default, Customer.io employees cannot access your account, workspaces, or the data you store in Customer.io—your People, messages, etc. The Support access setting grants authorized support staff read-only access to your account, workspaces, and data to help troubleshoot your support requests. You can grant us access for up to 180 days at a time. You can also enable support team access when you submit a support ticket.  To manage support access, you must be an Account Admin or Member with the account-level permission “Enable access for support teams.” Customer.io AI By default, you and your team members can enable AI-based features at the user-level. When you enable an experimental AI feature, it only affects you; each team member in your Customer.io account must enable (or disable) AI features for themselves. If you’re not comfortable with members of your team using AI-based features, you can disable Customer.io AI in your account settings. When you disable Customer.io AI in your account settings, it turns off all AI features for everybody in your account and removes these settings so people can’t see or enable them. Before you disable access, note that we do not use your customer data to train AI models and we don’t use personal data (attributeA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. and event values) in any of our AI features. You can safely use our AI features without worrying about us (or anybody else) using your customer data to train AI models. Gemini safety settings AI features in Customer.io use Google’s Gemini models. You can configure safety thresholds for content created by our tools from your account’s Privacy page. For a full list of our features, check out our current functionality powered by AI. They include, auto-translations, our AI assistant, content analysis, and more. Go to Account Settings > Privacy, Data, & AI and locate Gemini Safety Settings. Note, these settings apply across all of the workspaces in your account. You can set filters for four categories: Harassment Hate Speech Sexually Explicit Dangerous Content For each category, you can choose a threshold ranging from Off (no filtering) to Block low and above (strictest filtering). Learn more about Gemini’s safety settings in Google’s documentation.  Test translations and our chat assistant after you save new settings. Blocking content can cause AI features to behave unexpectedly or prevent them from returning content. Make sure you test or preview content created by AI before sending it to your customers. Compliance prompt You can include a compliance prompt to help ensure generated content aligns with other regulatory and policy guidelines not specififed under Gemini Safety Settings. Go to Account Settings > Privacy, Data, & AI and locate Compliance Prompt to get started.  Test translations and our chat assistant after you save new settings. To see if your prompt is working as expected, make sure you test or preview content created by AI before sending it to your customers. Prevent your agent from editing live data By default, the agent can edit live campaigns, send and modify newsletters, trigger broadcasts, and edit segments that are in use. But editing live resources can affect your audience immediately—for example, changing a live campaign’s content or segment criteria could disrupt user journeysTypically, a person’s path through your campaign. If the campaign is triggered by a webhook, then a journey captures the webhook’s path, not a person’s.. You want to be careful when you edit live workflows. If you want to prevent the agent from making changes to live resources, turn off Allow agent to edit live data. When this setting is off, your agent can still create campaigns, message drafts, and so on; it just can’t modify anything that’s currently live or in use. You must be an account admin to manage this setting. Customer.io MCP On the same page, you can toggle Customer.io MCP to turn the Model Context Protocol (MCP) integration on or off. See our MCP documentation for more information. Premium accounts: Customer Success access If you’re a Premium customer, you receive help from a Customer Success Manager (CSM). Your CSM may require access to your account to help you—with campaigns, organizing data, and so on. When you enable Customer Success access, your dedicated CSM will have write-access to your account; all other members of the Customer Success team will have read-only access to assist as needed. If you have questions about this access, your CSM is happy to discuss this with you. You can grant access for a limited period of time and you can revoke access at any time.  To manage support access, you must be an Account Admin or Member with the account-level permission “Enable access for support teams.” Customer.io Privacy Policy summary This is a summary of Customer.io’s Privacy Policy, specifically the parts we’d want to understand if we were you. We track what you do on our website. We do that so we can answer questions like “How interested is this person in our product?” and “Should we show this person an ad to get them back to our site?” We track what you do in our software. We do that so we can answer questions like “What parts of our product experience need improvement?” and “How do we reproduce that pesky bug?” We send you emails to get you to buy our software and to help you use it. We do our best to keep these emails to a minimum. Any data we collect on you is stored securely. We lay out details on what “securely” means on our Security page. Some of the data we collect gets sent to other companies to make our software work. For example, we use Mailgun to send emails, which means your customers’ email addresses are transmitted to them. We have signed privacy agreements with Mailgun, and every company we send your data to. The point of those agreements is to ensure that these companies’ security standards are just as good as ours. You can use our software to track what your customers do on your site and within your software. By doing that, you may end up storing your customers’ Personally Identifiable Information (PII) like name and email address on Customer.io. That’s fine. That data is governed by your own privacy policies. We don’t dictate those outside of asking you not to store passwords or credit card numbers, and not to store PII on children who are less than 18 years old. Some of Customer.io’s employees have access to view the data in your account. They have that access so they can provide customer service, and so they can bill you for using our software. We make sure our employees take your privacy seriously. If a big government body requests your information for a legal reason, we’ll evaluate the ask. That means we may provide what they’re asking for, and we may not. This kind of thing doesn’t happen much, so we treat every case uniquely. We don’t change our Privacy Policy often, but we do change it. If that happens, we’ll give 30 days’ heads up via email so you know what’s coming. Got questions? Want to get into the details of our full privacy policy? Don’t want us to track, send, or otherwise process your data in any of the ways listed above? No problem. Send us an email at legal@customer.io. Alternatively, there are lots of tool you can use that will prevent you from being tracked on any site. Might be worth checking those out. --- ## Respecting your users' privacy URL: https://docs.customer.io/accounts-and-workspaces/user-privacy/ You want to maintain a healthy relationship with your customers, and that means both respecting their privacy and responsibly using the data you collect. Cookies (when using our JavaScript libraries) Our two client-side JavaScript libraries both use cookies that make it easier to recognize your audience and use them across calls to Customer.io. Journeys JavaScript SDK The Journeys Web SDK sets cookies to determine whether someone is anonymous or identified. These cookies do not contain any personally identifiable information (PII). _cioanonid: contains an anonymous ID that the tracking snippet sets automatically on a person who has not been identified yet. When you identify a person, we associate their anonymous activity (like page views) with the identified person. _cioid: contains the id used when you call _cio.identify with the tracking snippet. Once set, these cookies persist for up to 365 days or after a user clears their cache, for instance. Data Pipelines JavaScript Source Our Data Pipelines JavaScript client-side source can set up to five cookies depending on the methods you use and the settings you invoke when you initialize the client. These cookies do potentially store personally identifiable info (PII) if you pass it to us—but our JavaScript snippet contains ways to set and clear cookie values so that you can sanitize client-side information. See our JavaScript Source to learn more about the cookies we set and settings determining how these cookies work. Cookie Contains ajs_anonymous_id A user’s anonymous ID, set automatically when someone visits your site. This value is used to track anonymous activity and associate anonymous activity with an identified person. ajs_user_id The ID of the user, set when you identify a person. ajs_group_id The ID of a group, set when you associate a person with a group. ajs_user_traits Contains user attributesInformation that you know about a person, captured from identify events in Data Pipelines. Traits are analogous to attributes in Customer.io Journeys.—values associated with a person—that you set when you identify a person. ajs_group_traits Contains group traits—values associated with an objectAn object is a non-person entity that you can associate with one or more people—like a company, account, or online course.—that you set when you associate a person with a group. Personally Identifiable Information (PII) While Customer.io doesn’t automatically collect or store personally identifiable information (PII) on its own, you can pass this information to us—things like a person’s name, address, phone number, etc. This information may be reasonable for your use case and the messages you send your audience, but you should limit the data you store in Customer.io to only what you’ll need to send relevant messages. This both limits your data footprint and protects your audience against potential breaches. Limit who sees PII in Journeys Premium This feature is available for Premium plans. When you add viewers, authors, or custom roles to your account, you can limit their access to personally identifiable information (PII) with the Hide sensitive attributes setting. Account or workspace admins must mark these attributes as “sensitive” in the Data Index before they’re redacted from authors or viewers. Custom roles with data index permissions can also do this. Then admins can choose to redact this data from authors, viewers, or custom roles. Respect your users’ inboxes Make sure that you send your audience relevant messages that they want and expect. This isn’t simply about liability: it’s better for your business if you send messages that engages your audience. In some cases, quality is better than quantity! To this end, you can: Take advantage of message limits so that you don’t over-message your audience Make sure that your messages have clear calls to action Message users in the medium(s) they want to use Unsubscribes and the right to be forgotten When people unsubscribe from messages, they aren’t removed from your workspace. Rather, we set their attributes to indicate that they’re unsubscribed. We have a global unsubscribe attribute, and maintain a list of unsubscribed channels as a part of our Subscription Center feature. You can override these preferences to send important messages, but, in almost every case, you should respect your audience’s subscription preferences. If a person requests that they be forgotten, you can delete their profile from your workspace. This removes all of their data from your workspace, including their email address, and prevents you from sending them messages. In extreme cases, when a person indicates that they never want to be messaged again, you can suppress their identifiers. This prevents you from using their identifiers in your workspace again—so even if they make themselves known to you, they won’t be added to your workspace or be eligible to receive messages. --- ## Customer.io Security Qualifications URL: https://docs.customer.io/accounts-and-workspaces/security-certifications/ We're serious about data privacy and security—not just for our company, but for you and your users. This page lists the certifications we've earned to ensure your data is safe with us. The certifications we’ve earned are the result of independent, third-party audits that verify our compliance with (and commitment to) industry standards and best practices.  See our full security policy You can read more about our security practices on our website and download our compliance documents. ISO 27001 certification Customer.io is certified for ISO 27001 compliance as of September 24, 2024. Our certification expires September 24, 2027. See our certificate. ISO 27001 is an international standard for information security management systems (ISMS). It provides a framework of policies and procedures that includes all legal, physical, and technical controls involved in an organization’s information risk management processes. Conforming with ISO/IEC 27001 means we’ve put systems in place to manage risks related to data security and respect all of the international standard’s best practices and principles. SOC 2 Type 2 certification Customer.io successfully completed the SOC 2 Type 2 examination for Security and Availability. You’ll find our SOC 2 report here. HIPAA compliance Customer.io is HIPAA-ready (Health Insurance Portability and Accountability Act of 1996), meeting the privacy and security requirements of both the healthcare industry and your valued customers. Contact your Customer.io representative if you’d like more information about Customer.io’s HIPAA compliance or want to sign a Business Associate Agreement (BAA) with us. You can request a copy of our HIPAA report here. For best practices on sending HIPAA-compliant SMS and MMS messages, see our HIPAA compliance and privacy regulations guide. --- ## Troubleshooting login and browser issues URL: https://docs.customer.io/accounts-and-workspaces/login-trouble/ If you have trouble logging into Customer.io, we have a few tips to help you get back up and running. Help, Customer.io isn’t loading! If you see a perpetual loading spinner when you try to log in to Customer.io, you have trouble logging in, or you find the interface to be a little sluggish, you might have a problem with your web browser. Here’s the typical troubleshooting path for browser-related issues in Customer.io: Update your browser. Clear your browser cache. Try disabling your ad blocker or using Customer.io in incognito mode. Make sure that Customer.io isn’t blocked at the network level. If you still have trouble logging in, generate a HAR file so we can help debug the issue and contact us. Browser compatibility is the typical problem If you have trouble logging in, or you see another problem in Customer.io, try updating your browser to the latest version. We actively test our user interface with the latest two major versions of Google Chrome, Mozilla Firefox, Microsoft Edge, and Safari. Clear your browser cache We cache some data in your browser to improve performance. On rare occasions, the cache causes problems. If you have trouble with Customer.io, clearing your browser cache often fixes the problem. To clear cache for Customer.io—and only Customer.io—try performing a “hard refresh.” When you’re on https://fly.customer.io/login: All browsers on Windows or Linux: Press Ctrl + F5. Safari: hold Shift and click the reload icon in the toolbar. Chrome and Firefox on MacOS: Press Cmd + Shift + R. If that doesn’t work, you might try clearing all your browser cache. See instructions for Google Chrome, Mozilla Firefox, Safari, or Microsoft Edge. Clear cache for Customer.io To clear your cache for Customer.io, and only Customer.io: Chrome Chrome In Chrome, click More and go to Settings . Click Privacy and security and then Third-party cookies. Click See all site data and permissions. At the top right, search for fly.customer.io. To the right of the site, click Delete . Firefox Firefox Go to the Firefox menu in the upper-left of your browser and select Preferences or Settings depending on your operating system. Click Privacy & Security and go to the Cookies and Site Data section. Click Manage Data… and search for fly.customer.io. Click Remove All Shown. Click Save Changes. Edge Edge In Edge, click Settings and more > Settings > Cookies and site permissions. Under Cookies and data stored, select Manage and delete cookies and site data > See all cookies and site data and search for fly.customer.io. Select the down arrow to the right of the site and click Delete . Safari Safari In Safari, choose Safari > Settings, and click Privacy. Click Manage Website Data. Select fly.customer.io, and click Remove. Disable your ad blocker or use incognito mode Some ad blockers can interfere with Customer.io. If you have trouble logging in, try disabling your ad blocker for Customer.io or using incognito mode. If you can log in without your ad blocker, you might need to add Customer.io to your ad blocker’s allowlist. Make sure that Customer.io isn’t blocked at the network level In extreme cases, we’ve seen some virtual private networks (VPN) or company network firewalls interfere with Customer.io. If you have trouble logging in, try disabling your VPN. If you’re on a company network and you still have trouble, you might need to contact your IT department to un-block Customer.io. I can’t accept an invitation! Did you create a trial account under your email address or did you previously belong to a different Customer.io account? Your email address can only belong to a single account in Customer.io. So, before you can accept the invitation, we need to free up your email address. If you created a trial account under your email address, cancel your trial. Contact us to disconnect your email address from the trial account. If you need access to multiple accounts, you can log into each one with a different email address and switch between them without logging out. --- ## Create a HAR file for help troubleshooting URL: https://docs.customer.io/accounts-and-workspaces/create-a-har-file-for-help-troubleshooting/ Sometimes troubleshooting issues requires additional information about the network requests your browser generates when the issue occurs. When this is the case, our support team may ask you to record a HAR file (a log of network requests) while you are experiencing the issue so that we can dig deeper into the situation on our end and ultimately find a solution to whatever is going on.  CAUTION HAR files contain sensitive data and potentially personally identifiable information. It includes the content of the pages you accessed while recording the file as well as your cookies. This could include things like email addresses, phone numbers, usernames, passwords, credit card numbers, etc. Please handle the resulting file with the same caution you would any other sensitive information. We offer a tool so you can sanitize your Customer.io credentials - see the instructions below for more info. Instructions to generate a HAR file Chrome Safari Firefox Internet Explorer Edge Chrome Open the Chrome web browser and go to the page where you were experiencing the issue. If the issue is happening when the page loads, you may need to go to the previous page and then navigate to the troublesome page after you have started recording. From the Chrome menu bar select View > Developer > Developer Tools. Select the Network tab. Look for a round record button in the upper left corner of the Network tab. If it is red, you are already recording. If it is grey, click it one time to make it red and start recording. Check the Preserve log box. Click Clear to clear out any existing logs from the Network tab. While the network requests are being recorded, reproduce the issue that you were experiencing earlier. After you reproduce the issue, click the download icon in the top right of the Network tab. If you hover over it, it will show “Export HAR…” Then save the file to your computer. For older versions of Chrome, right-click anywhere on the table that shows your network requests, select “Save as HAR with Content” and then save the file to your computer. Upload your HAR file to our sanitizer tool. This will remove all Customer.io credentials from your file, but not third party credentials or cookies. If you want to remove these items, you will need to do so manually. Send your file to our support team by attaching it to your existing support ticket. Safari Before you begin, make sure you can see the Develop menu in Safari. If you don’t see the Develop menu in the menu bar, choose Safari > Settings, click Advanced, then check “Show features for web developers.” Open the Safari web browser, and go to the page where you were experiencing the issue. If the issue is happening when the page loads, you may need to go to the previous page and then navigate to the troublesome page after you have started recording. Open the Develop menu and click Show Web Inspector. Click the Network tab (see screencast below). The recording will start automatically based on the actions you take in the browser. While the network requests are being recorded, reproduce the issue that you were experiencing earlier. After you’ve reproduced the issue, click Export (see screencast below) then save the file to your computer. Upload your HAR file to our sanitizer tool. This will remove all Customer.io credentials from your file, but not third-party credentials or cookies. If you want to remove these items, you will need to do so manually. Send your file to our support team by attaching it to your existing support ticket. Here is a brief screencast of this process: Firefox Open the Firefox web browser and go to the page where you were experiencing the issue. If the issue is happening when the page loads, you may need to go to the previous page and then navigate to the troublesome page after you have started recording. Click the menu icon at the top-right of your browser window (see screencast below), then select Web Developer > Network. In the developer network tools panel, the Network tab should already be selected but if it is not, go ahead and click it (see screencast below). The recording will start automatically based on the actions you take in the browser. While the network requests are being recorded, reproduce the issue that you were experiencing earlier. After you have reproduced the issue, right-click anywhere under the File column and click on “Save all as Har” (see screencast below) and then save the file to your computer. Upload your HAR file to our sanitizer tool. This will remove all Customer.io credentials from your file, but not third party credentials or cookies. If you want to remove these items, you will need to do so manually. Send your file to our support team by attaching it to your existing support ticket. Here is a brief screencast of this process: Internet Explorer Internet Explorer is not a web browser we support. Please use one of the other browsers listed on this page to access Customer.io. Edge Open the Edge web browser and go to the page where you were experiencing the issue. If the issue is happening when the page loads, you may need to go to the previous page and then navigate to the troublesome page after you have started recording. Click the menu icon at the top-right of your browser window (see screencast below), then select More Tools > Developer Tools (or press F12 on your keyboard) The recording will start automatically based on the actions you take in the browser. While the network requests are being recorded, reproduce the issue that you were experiencing earlier. After you have reproduced the issue, click Save (see screencast below) to save the file to your computer. Upload your HAR file to our sanitizer tool. This will remove all Customer.io credentials from your file, but not third party credentials or cookies. If you want to remove these items, you will need to do so manually. Send your file to our support team by attaching it to your existing support ticket. Here is a brief screencast of this process: --- ## Use Customer.io with AI URL: https://docs.customer.io/ai/cio-with-llms/ AI is exciting and new—to you and us! Learn more about how we're enabling AI features across our platform.  Check out our roadmap to see what’s coming next We’re always working on new features. Take a peek at our roadmap to see what’s in store for the future. Our current functionality powered by AI Agentic tools that help you use Customer.io: 🎉New AI agent: The agent is your AI coworker in Customer.io. You’ll prompt it to help you do things in your workspace, like creating segments, drafting messages, and so on—so you can focus on important parts of your job and not the ins-and-outs of Customer.io. You’ll find clickable uses of AI sprinkled throughout the product that pass prompts to the agent. Customer.io CLI: Give AI coding assistants and automation scripts full access to your Customer.io workspaces through the command line. Customer.io MCP: Connect your Customer.io account to AI tools like Claude, ChatGPT, and Cursor through the Model Context Protocol. Both the CLI and MCP provide ways to use Customer.io through your AI tool of choice. Use the CLI when you’re working in a terminal or with an agent-driven coding tool like Claude Code; use MCP when you want a quick connection between a chat-based AI tool (Claude Desktop, ChatGPT, Cursor) and Customer.io without installing anything locally. AI features in different parts of Customer.io: LLM actions: Add AI-powered steps to your campaign workflows that send data through a Large Language Model (LLM) and store the results as journey attributes. Use them for personalized recommendations, sentiment analysis, translation, and more. LLM actions use AI credits. Email content analysis: Use AI to analyze your email for better ways to deliver your message to your audience. Recommended send time: Use AI to recommend the best time to send your messages based on the content of your message and your audience’s time zones. In-app survey analysis: We’ll analyze your in-app survey responses and distill the data into actionable insights. Generate descriptions for your data: Use AI to help you describe your data so our other AI features that rely on descriptions can make more accurate suggestions. Translate messages with AI: when adding a language variant to a message, you can use AI to translate the default content into the new language. Workspace search: Press ⌘K or CtrlK to search across your workspace. AI-enhanced ranking helps you find campaigns, segments, templates, and more. You can also download plain-text versions of our docs, including our API specifications, to create your own GPT or get help with Customer.io from LLMs outside our platform. Set up your business context Our AI tools use information you provide about your business when generating responses, content, and analyses. We automatically pull in the context we can find online to your Business context. Specifically, we analyze the email domain you set up in workspace settings. While we generate a basic profile for you, you can modify it so we have a better understanding of your business, audience, and brand. Each workspace has its own profile, so make sure the business context reflects each workspace’s goals. To update your business context, go to Workspace settings > Business context. You must be an admin or have the right permissions to update the profile. Under Basic info, you can modify your business info like name, website, and industry. You can update your business and audience descriptions to communicate more about your goals. If you can’t find your industry in the dropdown, click Other and populate the new field with your industry. Under Tone & Voice, you can modify how our AI tools communicate with you in your workspace. For instance, if you draw the slider to “Enthusiastic,” then ask the agent to draft a promotional email for you, it might deliver content like this: Subject: 🎉 Transform Your Business with Ami Academy! 🎉 Body: Hey there! Are you ready to take your business to the next level? At Ami Academy, we believe in making technology accessible and fun for everyone! Join us on this exciting journey to transform your business with our innovative solutions! Under Key links, you can change which links AI pulls from to understand your company’s policies. Under Platform availability, you can modify the links where people go to download your app or access your platform. Enable AI in your account If you don’t see an AI feature, check that Customer.io AI is enabled in Privacy, Data, & AI settings. If it’s not, turn it on or ask an Account Admin to enable it. To support Customer.io MCP, you need to turn on both the Customer.io AI and Customer.io MCP toggles. These are account-level settings. When you enable them, they apply to all team members across all workspaces in your account.  AI features respect your role and permissions Our AI features—including our agent and MCP server—authenticate based on your user and role. For example, if you don’t have the ability to create messages, our agent won’t draft/create new messages for you. Disable AI in your account Account Admins can disable AI features and the ability to integrate and use our MCP server through Privacy, Data, & AI settings. If you turn off the Customer.io MCP toggle, we don’t delete any integrations; rather your LLM can no longer use our tools and engage Customer.io workspaces. Team members only lose access to the MCP server, not other AI features in your workspaces. If you turn off the Customer.io AI toggle, all team members lose access to all AI features across workspaces, including the MCP server. Best practices: making sure AI works for you All of our AI tools take context around your business into account when generating responses, content, and analyses. We automatically pull in the context we can find online to your Business context. You can have a different business context for each workspace, if desired. When you create attributes, events, campaigns, segments, and so on, you give these assets names and descriptions. Our AI features rely on these names and descriptions to understand your data and provide suggestions—just like any member of your team would. That means that clearly labeling, describing, and organizing your data helps our AI features perform better. For example, imagine that you have an attribute called cname. This attribute might be relevant to you. But, if you don’t provide a description, the agent might not know what it means. Is it a company name, customer name, or does it refer to the canonical name in a domain name system? That’s something you need to tell the agent. You can even use AI to describe your data! In your Data Index, select an attribute or event and click in the description field to generate a description.  We don’t store or train AI on your customers’ personal information We only use the names and descriptions of data (key names, not values) with AI. For example, if you provide a first_name attribute for a person named “Alex,” our AI-based tools will know that you have a first_name attribute, but will not see Alex’s name. Follow uniform naming conventions for your data When you create attributes and events, we’ll use the names you provide to understand your data and provide AI-based suggestions. To make sure that we understand your data—and produce suggestions that match your standard formatting rules—we recommend that you follow uniform naming conventions. For example, you should follow a consistent naming scheme like snake_case or camelCase for your attributes and event data. That way, when we propose suggestions, they’ll match your preferred format. Use tags to organize campaigns, segments, and more Tags are a feature in Customer.io that help you organize similar assets like campaigns and segments. Tags not only help your team understand things in Customer.io that share common characteristics or use cases, they help AI as well! Tags also have descriptions. Provide descriptions for your tags to help your team and our AI features understand your data! --- ## Ask the agent URL: https://docs.customer.io/ai/agent/get-started/ The agent is your personal assistant or coworker in Customer.io. You can prompt it for answers or ask it to help you do things in Customer.io. Click Agent in the sidebar or click Ask Agent in the bottom right to open a fresh conversation. You can ask it to do just about anything that you can do in Customer.io. Give it a prompt like: “Create a segment with people that opened an email in the last 90 days.” “What’s the click-to-open rate for my birthday campaign?” “Of my onboarding and welcome campaigns, which had the lowest deliverability this month?” “How do I get started with Design Studio?” “In Design Studio, create a welcome email. Greet the customer with their first name and fall back to ‘valued customer.’”  To use the agent, your workspace must have Customer.io AI enabled. You can also adjust how AI features behave in your compliance prompt settings. What can the agent do? Our agent can do just about everything you can do in Customer.io. It can create segments, campaigns, newsletters, Design Studio emails, and more. Beyond working in Customer.io, it can search documentation and help you troubleshoot issues in your workspace. The agent operates with your permissions. It can only access data and perform actions that your role allows. Before taking high-impact or destructive actions, it’ll ask you for confirmation. Pay attention to confirmation prompts and allow or deny actions depending on your comfort level. The agent also remembers context from your previous conversations and learns your preferences over time, so it gets more helpful the more you use it. We’re actively working on expanding the agent’s capabilities. Make sure you submit feedback to help it (and us) improve over time! Restrict the agent from editing live data By default, the agent can edit live campaigns, newsletters, segments in use, and other active resources. But changes to live campaigns and other assets can potentially disrupt active user journeys if you’re not careful. If this is something you’re worried about, and you’re an account admin, you can turn off Allow agent to edit live data in Privacy, Data, & AI settings. The agent can still create drafts and work with unused resources when this setting is off. What the agent can’t do The agent works within Customer.io. It doesn’t have access to external services or data sources. Here’s what it can’t do: Connect to your external databases. The agent can’t query your SQL databases, data warehouses, or other external data stores. It can help you configure integrations but it can’t access external data sources directly. Access external websites or third-party APIs. The agent can search Customer.io documentation for you, but it can’t browse the web, visit URLs, or reach out to third-party services on your behalf. It can help you set up Customer.io integrations and connections with those services. Send live messages. The agent can help you set up campaigns and broadcasts but they go through your normal approval and sending flows. It can’t send messages on its own. Data the agent can access The agent uses data about you, your business context, and the current page in Customer.io to help it better understand your needs and help you do things in Customer.io. It also has access to data like people, events, and objects, and metrics for campaigns and broadcasts. Information the agent uses includes, but is not limited to: Your name, ID, and timezone The current page URL The name and ID of your account Attributes for people, events, and objects Metrics for campaigns and broadcasts The agent is limited by your permissions. It never has access to more information than you do. Write effective prompts The more context you give the agent, the better it can help you. Here are some tips for getting the most out of your conversations: Be specific. Include the names of campaigns, segments, attributes, or other resources. “Check the deliverability of my Onboarding Welcome campaign” is better than “check my campaign.” Providing exactly what you want the agent to do helps it give you the best possible answer. Explain your goal. Tell the agent what you’re trying to accomplish, not just the immediate task. For example, “I want to re-engage people who haven’t opened an email in 60 days” gives the agent enough context to suggest a complete approach. Share what you know. If you’ve already tried something or have specific requirements, tell the agent. This helps it skip suggestions you’ve already ruled out. Break complex requests into steps. If you need help with a multi-step process, start with the first step and build from there. The agent remembers context within a conversation, so you can iterate. Here are some examples of vague prompts versus specific, actionable ones: Don’t say Say this instead “Check my campaigns” “What’s the click-to-open rate for my Onboarding Welcome campaign this month?” “Create a segment” “Create a segment of people who signed up in the last 30 days but haven’t completed onboarding.” “Help with deliverability” “My bounce rate spiked last week. Can you check which campaigns had the highest bounce rates and suggest what might have changed?” “Write an email” “Draft a re-engagement email for people who haven’t opened a message in 60 days. Use a casual tone and include a 20% discount offer.” Start a conversation To start a conversation with the agent: Click Agent in the sidebar or click Ask Agent in the bottom right to open a fresh conversation. Prompt the agent: choose from the suggestions, ask a question, or give a command. After you start a conversation, you can continue the chat by choosing further suggestions or typing another question. You can also learn more by following the links in the References bubble at the bottom of each response. We automatically save your conversation history. You can revisit and continue previous conversations within 30 days of starting them. After 30 days, we delete the conversation. Upload files Upload files to give the agent additional context for your conversation. The agent supports images, documents, and data files—not just screenshots. You can upload images up to 5 MB in size, and documents up to 10 MB. Supported file types: Images: JPEG, PNG, GIF, WebP (5 MB max) Documents: PDF, plain text (.txt), Markdown (.md) Data: CSV Open the Agent panel. Click under the text input area, and select your file. Ask the agent a question or give instructions based on the file. For example, you could ask it to analyze the data, follow guidelines from a document, or extract text from an image. Click enter or select . Here are some examples of how you could use file uploads: Upload a brand style guide (PDF or Markdown) and ask the agent to follow it when drafting email content. Upload a CSV of customer data and ask the agent to analyze trends or suggest segments. Upload a screenshot of a campaign from another platform and ask the agent to recreate it in Customer.io. Upload an image and ask the agent to generate alt text for your emails. Upload charts from a third-party analytics tool and ask the agent for recommendations based on the data. Memory and context The agent remembers context from your previous conversations and learns your preferences over time. The more you use it, the better it gets at helping you. You can tell the agent to remember things for future conversations. For example: “Always use sentence case in my email subject lines.” “Our brand voice is friendly and informal—avoid corporate jargon.” “When I ask about deliverability, include bounce rate and spam complaint rate.” The agent applies these preferences automatically in later conversations, so you don’t have to repeat yourself. You can also upload files—like a style guide, brand guidelines, or product information—and ask the agent to remember the content. For example, upload a PDF of your brand guidelines and say “Remember these guidelines for when you help me write emails.” The agent incorporates that context going forward. Manage conversation history If you click the Agent icon in the sidebar, you’ll see a list of your conversations. You can click on a conversation to continue it or click the three dots to the side of the conversation to manage it. If you open the agent using Ask Agent, click in the agent panel, then choose History to manage your chats. We automatically save your conversation history. You can revisit and continue previous conversations within 30 days of starting them. After 30 days, we delete the conversation permanently. Your chat history is available across all workspaces you have access to; you can view conversations started in Workspace A when you’re in Workspace B. You can delete your conversations to remove clutter or rename them to more easily find them when viewing the list. Delete conversations To delete a conversation select it, click and click Delete. Rename conversations To rename a conversation select it, click and click Rename. Edit your business context Click Context to edit your business context. The agent uses your business context to better understand your needs and generate responses. For example, you can modify the tone of agent responses and content analyses or change the business info shared with AI. Submit feedback If you run into issues with the agent, let us know! At the bottom of any conversation, click Give feedback, describe the issue then submit it so our team can review. --- ## How the agent works URL: https://docs.customer.io/ai/agent/how-it-works/ The agent does a few things behind the scenes. This page can help you understand what you see when the agent reads skills or writes code in its sandbox. When you ask the agent to do something, it works through a series of steps behind the scenes. Along the way, you might see it doing things that look technical—reading files, writing code, running commands. This is all part of the agent’s process. Where you work in a user interface, clicking and typing, the agent works by reading and writing code. Skills If you see the agent reading a skill, it’s just loading instructions for how to work with a specific part of Customer.io. Skills are built-in reference guides that cover things like campaigns, segments, Design Studio, and so on. Think of skills as the agent’s version of reading documentation before using the product. Code and the sandbox If you see the agent writing or running code, that’s normal too. The agent operates in a sandboxed environment—an isolated workspace where it can think, plan, and execute tasks. Writing code is how it interacts with Customer.io, building email markup, processing data, and chaining steps together. The sandbox is isolated to your workspace. The agent can’t access anything outside of it, and the code it writes is ephemeral—it exists only for the duration of the task. --- ## Routines URL: https://docs.customer.io/ai/agent/routines/ You can set up recurring tasks that the agent runs automatically on a schedule—like deliverability reports, segment audits, and broadcast recaps. How it works Routines are recurring tasks that the agent runs automatically on a schedule. You tell the agent what to check and how often, and it delivers a summary by email each time it runs. You can use routines to automate the kinds of checks you’d otherwise do manually—monitoring deliverability, auditing segments, comparing broadcast performance, or tracking goal conversions. Routines run in safe mode—the agent can read your workspace data and analyze it, but it can’t make changes to your campaigns, segments, or other resources. When a routine runs, it creates a fresh agent session—no conversation history from previous runs. It executes your prompt using the same tools available in normal agent conversations. That way, if a routine executes while you’re working on something, it won’t affect your current work. Whenever a routine runs, you’ll see it as an entry in your agent’s conversation history. What routines can do Routines can: Query campaign, segment, and subscriber data Analyze performance metrics and trends Compare results across campaigns or time periods Generate reports and summaries Send email notifications with findings Routines can’t: Modify campaigns, segments, newsletters, or workspace settings Execute code or scripts Delete or overwrite files Before you begin Routines are available to workspaces with Customer.io AI enabled. You need Editor or Admin permissions to create routines. Create a routine You can create routines at any time by chatting with the agent. Just ask it to set up a routine describing what you want to track, how often you want updates, and what you’d like in each summary. But you can also click Routines in the agent sidebar to see suggestions and manage your routines.  Not sure what to monitor? Ask the agent! Describe your goals or pain points—like “I want to improve my email deliverability” or “I’m worried about subscriber churn”—and it can suggest routines tailored to your workspace. Go to Agent and click Routines in the sidebar. Pick a suggestion or click Create a custom routine to start a conversation. The agent asks what you want to monitor and how frequently. Be specific about what you want to track and why. Something like “check my campaigns” is too vague to be useful! When you’re done, the agent creates the routine and it starts running on schedule. Routine suggestions When you visit the Routines page, you’ll see suggestion cards to help you get started: Deliverability watchdog—Monitor bounce rates, spam complaints, and domain reputation trends. Broadcast recap—Compare how recent broadcasts performed and surface actionable takeaways. Segment hygiene check—Audit segments for unused, empty, or redundant conditions. Goal conversion report—Track conversion rates and week-over-week trends across all goals. Subscriber churn early warning—Spot at-risk subscribers before they disengage or unsubscribe. Audience growth tracker—Track how key segments are growing or shrinking over time. Clicking a suggestion opens an agent conversation pre-filled with a detailed prompt. You can modify it before the agent creates the routine. Manage routines Go to Agent > Routines to see all your routines. Each routine shows its name, schedule, status, and when it last ran. Pause and resume Toggle a routine on or off from the list. Pausing a routine stops it from running on schedule. Resuming it re-enables the schedule. Edit a routine Expand a routine to see its details. Click Edit to change the name or prompt. The agent updates the routine when you save. To change a routine’s schedule, ask the agent in a chat conversation. Delete a routine Expand a routine and click Delete. This permanently removes the routine and its schedule. Trigger a run manually Ask the agent to run a specific routine immediately, outside its normal schedule. You might do this if you want to test a new routine or get an on-demand report. Results and delivery When a routine runs, the agent sends results to the email address associated with your Customer.io account. Each run creates a separate agent session, so it doesn’t interrupt your current work or affect your active conversations. You can also review the full conversation in your agent conversation history. Auto-disable on failure If a routine fails 3 times in a row, it automatically pauses to prevent repeated failures. The Routines page shows an error banner with details about the last failure. To fix it: Review the error message on the Routines page. Edit the prompt to address the issue. Toggle the routine back on—this resets the failure counter. You can configure the failure threshold between 1 and 5 consecutive failures when creating the routine. Plan limits Routine limits are per user, per workspace. Each user gets their own limits in each workspace they belong to. For example, on an essentials plan, if you have two users and three workspaces, each user can have 1 active routine per workspace for a total of 6 active routines. Essentials Premium Enterprise Active routines (per user, per workspace) 1 5 5 Minimum interval Weekly Daily Daily Total routines (per user, per workspace) 10 10 10 Active routines are routines that are currently enabled and running on schedule. Paused routines don’t count against this limit. For example, on an Essentials plan, you can have 1 active routine per workspace—but a different user in the same workspace gets their own limit of 1. Total routines is the maximum number of routines you can create in a workspace, regardless of whether they’re active or paused. You can create up to 10 routines but only enable as many as your plan allows at a time. Minimum interval is the shortest time between runs. Essentials plans can schedule weekly runs; Premium and Enterprise plans can schedule daily runs. --- ## Segment builder URL: https://docs.customer.io/ai/agent/ai-segment-builder/ When you go to create a segment, you can click **Describe segment** and the agent will help you build a segment from a plain-text description. How it works Segment conditions typically require you to break your audience down into if this, then that style statements—but the logic can become complex when you have narrow or especially dense conditions. To help you build these conditions, you can use our agent. Describe the segment or ask for suggestions based on your target audience. You can ask the agent to build a segment at any time. Or, when you go to create a segmentA group of people who match a series of conditions. People enter and exit the segment automatically when they match or stop matching conditions., click Describe segment to start a conversation with the agent.  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. Getting better suggestions Like many AI-related features, success typically requires: Well-defined data: make sure your data has clear names and descriptions to help us better set up your segment criteria. A clear prompt: the better you can describe your ideal audience, the better the suggested segment will be. Check the results before you use an AI-generated segment When you create a segment, check the results to make sure they match your expectations. Is the count of people in the segment reasonable to you? You might even spot check a user or two to make sure they’re who you expect to see in the segment. If the segment doesn’t match your expectations, you can tell the agent what’s wrong and have it revise the segment; or you can edit the segment conditions manually. As suggestions become more accurate, you may want to edit conditions manually. AI often gets you most of the way to your objective, but you’re ultimately in control of your segments and the people you want to send messages to. --- ## Troubleshooting and feedback URL: https://docs.customer.io/ai/agent/troubleshooting/ AI features can assist you as you work with Customer.io, but it's important that you verify AI-generated responses and outputs before you implement them. This page covers some of our recommendations to help you use AI features responsibly, and how to provide feedback to help us improve. Verify AI-generated results AI suggestions and generated content are designed to help you, but they’re not a substitute for your expertise. AI can make mistakes, potentially leading to unintended consequences. You should not rely on AI-powered features as a singular source of truth, especially when it comes to features that can impact your audience—like segments that trigger campaigns or updating messages in live campaigns. When you work with AI-powered search results, you should review the cited sources from our documentation to verify the accuracy of the information. Checking original sources can help you verify the answers provided by AI. You can also use the thumbs up and thumbs down buttons to tell us if an AI-generated response was helpful or not. Your feedback helps us improve our AI features. Improve AI-generated results by accurately describing your data When you create attributes, events, campaigns, segments, and so on, you give these assets names and descriptions. Our AI features rely on these names and descriptions to understand your data and provide suggestions—just like any member of your team would. That means that clearly labeling, describing, and organizing your data helps our AI features perform better. For example, imagine that you have an attribute called cname. This attribute might be relevant to you. But, if you don’t provide a description, the agent might not know what it means. Is it a company name, customer name, or does it refer to the canonical name in a domain name system? That’s something you need to tell the agent. You can even use AI to describe your data! In your Data Index, select an attribute or event and click in the description field to generate a description. Here are some best practices to help you get the most out of our AI features: Use descriptive names: Give attributes, events, and campaigns clear, meaningful names Add descriptions: Provide descriptions for your data attributes to help AI understand their purpose Follow consistent naming conventions: Use standard formats like snake_case or camelCase consistently Use tags effectively: Tag similar campaigns and segments with descriptive labels Provide feedback Use the thumbs up and thumbs down buttons to tell us if an AI-generated response was helpful or not. You can also indicate that you want to submit feedback to tell us more. We’re actively working on improving our AI features and your feedback helps us improve! --- ## Get started URL: https://docs.customer.io/ai/cli/get-started/ The Customer.io CLI gives AI agents and automation tools programmatic access to your workspaces. Install our skill, point an AI assistant at it, and let it drive. How it works The Customer.io CLI is a command-line tool that gives AI agents and scripts full access to your Customer.io workspaces. It covers the Journeys UI API and CDP Data Pipelines API through a single cio api command—no per-endpoint code, no SDK boilerplate. The CLI is an LLM-first tool. Every command returns structured JSON, every endpoint is self-describing via cio schema, and the interface is designed for predictability over interactivity. You can give it to an AI coding assistant—Claude Code, Codex, or similar—and write natural language prompts to do things in Customer.io.  Make sure you trust your AI tools The Customer.io CLI works with your AI provider of choice. While our LLM subprocessors don’t store this data, the AI providers you use might. Make sure that you use AI tools that are approved by your organization to ensure the safety of your—and your customers’—data. Get started with an AI coding assistant The fastest path is to install our skill—a set of instructions that teaches your AI assistant how to drive the CLI. The skill handles installing the CLI itself, signing in (or signing up), and using the right commands for the task at hand. Add the skill to your project: npx skills add customerio/cli Open your AI coding assistant in the project (Claude Code, Cursor, Windsurf, Codex, etc.). Prompt it for any Customer.io task—for example: “Set up Customer.io for my React Native app” “List the active campaigns in my workspace” “Create a new Customer.io account” (the skill walks through signup end-to-end) “Find people who haven’t opened an email in 30 days and create a win-back segment” The skill detects whether the CLI is installed, walks you through installation and authentication, and then runs the work. It also includes sub-skills for full account onboarding and SDK integration—so the same entry point covers signup, domain setup, sending your first email, and wiring SDKs into your code.  No skill support? Use cio prime instead If your AI tool doesn’t support skills, follow the manual install below, then run cio prime and paste the output into your agent’s context. That gives it the full CLI reference in a single block. Manual install You can also install and use the CLI directly in your terminal. Use this path if you want to script the CLI in CI/CD, run it without an AI assistant, or your tool doesn’t support skills. Install Install with npm: npm install -g @customerio/cli Or with Go: go install github.com/customerio/cli@latest After installation, the cio command is available in your terminal. Authenticate The CLI uses service account tokens (sa_live_...) for authentication. If you don’t have a token yet, create one in your Customer.io account: Go to Account Settings > API Credentials and click the Service Accounts tab. Click Create Service Account, give it a name, and click Create. Click Create Token: Enter a name for the token Choose an expiration Optionally, check Read-only to permanently restrict the token to GET requests. Click Create Copy the sa_live_ token. You’ll need it for the next step and it’s only shown once. Log in to the CLI with your token. The CLI handles the OAuth token exchange automatically—you just paste your token. # Interactive login — paste your sa_live_ token when prompted cio auth login # Non-interactive (for CI/CD pipelines and scripts) echo "$CIO_TOKEN" | cio auth login --with-token # Verify your authentication cio auth status  Set CIO_TOKEN from a secret store, not from your shell config. See Store tokens securely for recommendations by use case. Token resolution order The CLI checks for credentials in this order: --token flag (highest priority) CIO_TOKEN environment variable ~/.cio/config.json file (set by cio auth login) What you can ask your agent to do Once your agent has the skill (or the cio prime reference), you can prompt it with natural language: List all active campaigns in my workspace Create a segment of users who signed up in the last 7 days Show me the delivery metrics for my onboarding campaign Set up a new CDP source for my React Native app Find people who haven’t opened an email in 30 days and create a win-back segment Your agent will translate these into the appropriate cio schema lookups and cio api calls. The CLI handles pagination, error recovery, and multi-step workflows automatically. --- ## Service accounts URL: https://docs.customer.io/ai/cli/service-accounts/ You don't authenticate with the CLI directly. Rather, you create a service account, which provides long-lived tokens for the Customer.io CLI so your agents can work with Customer.io. Service accounts let you authenticate with the Customer.io CLI and other programmatic tools without using your personal credentials. They provide long-lived tokens with fine-grained access controls—ideal for AI coding assistants, CI/CD pipelines, and automation scripts. You can setup multiple service accounts and multiple tokens per account. You might have different service accounts for different projects or departments in your account; and different tokens within each account to represent the different tools or users. Create a service account Go to Account Settings > API Credentials and click the Service Accounts tab. Click Create Service Account. Enter a name and optional description. (Optional) check Read-only to permanently restrict the service account to GET requests. Click Create. After creating the service account, you’re prompted to create your first token. Create a token A service account can have multiple tokens. Use separate tokens for different integrations so you can rotate or revoke them independently. In the Service Accounts tab, expand the service account. Click Create Token. Enter a name for the token. Set Expiration: No expiration, 30, 60, 90 days, or 1 year. (Optional) check Read-only to permanently restrict the token to GET requests. Click Create. Save the token value starting with sa_live_. You can’t retrieve it later. You should store it in a secure location or use it with cio auth login so you don’t lose it. Revoke a token Revoking a token immediately invalidates it and any active sessions. Other tokens on the same service account aren’t affected. Expand the service account in the Service Accounts tab. Find the token and click Revoke. Delete a service account Deleting a service account revokes all of its tokens. This can’t be undone. Store tokens securely Service account tokens are long-lived and can grant full write access to your account. Treat them like passwords, and pick a storage method that matches how you’ll use the token. Use case Recommendation Why Local interactive development Run cio auth login to store your token in ~/.cio/config.json Built in; scoped to your user account Local scripts or non-interactive runs A project-scoped .env file (git ignored), loaded with a tool like direnv or dotenv Scoped to one project; rotate independently per project CI/CD pipelines Your CI provider’s secrets store (for example, GitHub Actions secrets), exposed as CIO_TOKEN Encrypted at rest, masked in logs, auditable Shared machines Your OS keychain (macOS Keychain, secret-tool on Linux) Encrypted, per-user, no plaintext on disk A few things to avoid: Don’t put sa_live_ tokens in shell config files like ~/.zshrc or ~/.bashrc. These files are often synced to dotfile repos or backed up to cloud storage, where the token can leak. Don’t commit .env files. If you use one, add it to your .gitignore and ship a .env.example with placeholder values for teammates. Use one token per integration. Separate tokens let you rotate or revoke access for one tool without disrupting the others. Read-only access You can restrict access at two levels: Read-only tokens: Check Read-only when creating a token. This permanently restricts every session from that token to GET requests. Read-only sessions: When exchanging a token directly (not through the CLI), add --read-only to the request. --- ## Command reference URL: https://docs.customer.io/ai/cli/reference/ While the CLI is optimized for AI agents, you can also use it directly in your terminal. Understanding how the CLI works can also help you structure prompts (or skills) to do the right things in Customer.io. In general, LLMs will use the CLI in the following order: Find skills to learn more about how to do things in Customer.io Find the right schema for the endpoint you want to call Use api to make the API call Skills The CLI has a set of skills that can be used to learn more about how to do things in Customer.io. You can list the available skills with cio skills list. # List all skills cio skills list # Read a specific skill cio skills read <skill_name> Discover endpoints Use cio schema to explore what’s available without leaving the terminal: # List all resources cio schema # List endpoints for a resource cio schema campaigns # Get full details for a specific endpoint cio schema campaigns.list Make API calls Use cio api <path> for any endpoint. You’ll resolve path placeholders like {environment_id} from --params: # List campaigns in a workspace cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' # Get a specific campaign cio api /v1/environments/{environment_id}/campaigns/{campaign_id} \ --params '{"environment_id": "123", "campaign_id": "456"}' The CLI defaults to GET requests. Pass --json to send a POST, or override with -X: # Create a campaign (POST inferred from --json) cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' \ --json '{"campaign": {"name": "Welcome Flow", "type": "none"}}' # Delete a campaign cio api /v1/environments/{environment_id}/campaigns/{campaign_id} \ --params '{"environment_id": "123", "campaign_id": "456"}' -X DELETE Filter and format output Use --jq to filter responses with jq expressions: # Only active campaigns cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' \ --jq '.campaigns[] | select(.state == "active") | {id, name}' Paginate results # Manual pagination cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' --page 2 --limit 50 # Auto-paginate (outputs NDJSON — one JSON object per line) cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' --page-all Dry run: make sure requests are valid Use --dry-run to validate a request without executing it: cio api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id": "123"}' \ --json '{"campaign": {"name": "Welcome Flow", "type": "none"}}' --dry-run CLI reference Flag Environment variable Description --token CIO_TOKEN Service account token override -X, --method HTTP method override --json JSON request body (or @filename) --params Path and query parameters as JSON --jq jq expression filter --dry-run Preview request without executing --page Page number --limit Page size --page-all Auto-paginate, output NDJSON --timeout CIO_TIMEOUT Request timeout (default: 30s) Exit codes Code Meaning 0 Success 1 General error 2 Validation or input error 3 Authentication error 4 Authorization error 5 API error (4xx/5xx) --- ## Get Started URL: https://docs.customer.io/ai/mcp/get-started/ Customer.io MCP connects your Customer.io account to AI tools that support the Model Context Protocol—like Claude, ChatGPT, Cursor, and more.  Recently updated We’ve updated our MCP server with write operations and a new tool architecture. Your existing sessions and natural language prompts continue to work. If you have hardcoded tool references in custom GPTs, Claude Projects, or scripts, see our migration guide for details. How it works Customer.io MCP connects your Customer.io account to AI tools that support the Model Context Protocol. With the MCP server, you can do just about anything you can do in Customer.io via your AI tool of choice—read workspace data, create and manage campaigns, send newsletters, work with segments, and more—all through natural language prompts. The way you set up your MCP client depends on the tool you use. See our instructions for ChatGPT, Claude, and Cursor for details.  Make sure you trust your AI tools Customer.io MCP works with your AI provider of choice and includes tools that return information about people in your workspace. While our LLM subprocessors don’t store this data, the AI providers you use might. Make sure that you use AI tools that are approved by your organization to ensure the safety of your—and your customers’—data.  Using Claude Code or another terminal-based agent? If your AI tool runs in a terminal, our Customer.io CLI is usually a better fit than MCP. The CLI gives your agent direct command-line access to the full API surface—no MCP setup required. What can Customer.io MCP do? Customer.io MCP gives your AI tools access to your workspace through a set of tools. You don’t need to tell your AI tool which tools to use—just describe what you want, and it picks the right ones. The current version of Customer.io MCP gives your AI tool access to the full Journeys UI API and CDP Data Pipelines API. Your AI tool can discover endpoints, read data, and perform write operations—all through natural language prompts. The tools available to your AI tool are: Tool What it does cio_prime Loads the AI-ready reference for the Customer.io API. Your AI tool should call this at the start of a task. cio_schema Discovers API endpoints—list resources, find endpoints, and inspect parameters before making calls. cio_api Makes authenticated API requests. Read-only by default; writes are opt-in. Supports filtering, pagination, and dry-run previews. cio_skills_list Lists available agent skills—task-specific instructions for multi-step operations like creating campaigns. cio_skills_read Reads the full content of a specific skill so the AI tool can follow step-by-step workflows. cio_auth_status Shows the current authentication state, including which workspaces the token can access. Writing data to Customer.io. By default, cio_api only makes GET requests. Your AI tool has to explicitly opt in to a write on each call, so read requests (* Strict body validation. Requests with unknown fields in the body return a 422 error rather than silently dropping them. This catches typos and stale field names before they cause confusing failures. Structured JSON errors. Tool errors come back as JSON, not plain text. If you’ve built workflows that parse the older text-format errors, you’ll need to update them. The MCP includes skills to help your AI tool understand Customer.io. Some of the tools above (cio_skills_list, cio_skills_read) give your AI access to a Customer.io-maintained library of task-specific instructions. Your AI tool fetches the right skill on its own when it recognizes a task; you don’t need to invoke them by name. So when you give your AI tool a task, it’ll read skills to understand how to accomplish the task in Customer.io, look up schemas to perform the task, and then do the work. You don’t need to tell your AI tool which skills to use—it’ll figure it out on its own when it recognizes a task. But you can ask your AI to do things like: “List all active campaigns in my workspace.” “Create a segment of users who signed up in the last 7 days.” “Show me the delivery metrics for my onboarding campaign.” “Draft a welcome email in Design Studio.” “Set up a new CDP source for my React Native app.” Enable Customer.io MCP for your account You must be an account admin to enable Customer.io MCP for your account. After you enable it, you and other users can set up clients—see ChatGPT, Claude, or Cursor and other IDEs. Go to Settings > Privacy, Data, & AI. Turn on Customer.io MCP. If it isn’t already enabled, you’ll also need to turn on Customer.io AI. You must enable both Customer.io AI and Customer.io MCP for team members to use Customer.io MCP. Now you’re ready to set up your MCP client—see ChatGPT, Claude, or Cursor and other IDEs for help.  Enabling Customer.io AI also enables a set of AI features in your account across all team members These features include the ability to translate messages with AI, analyze email content, build segments from written descriptions, and more. Learn more about our AI features. Who can do what Account admins can enable or disable Customer.io MCP for the account. Other users can’t change this setting. Any user in an account where MCP is enabled can connect their own MCP client. Each user authenticates with their own Customer.io login, and connections respect that user’s role and permissions—your AI tool can only do things you can do. Each user manages their own sessions. Account admins can’t view or revoke MCP sessions for other users. To revoke a session, the user who created it has to do it from their own personal settings. What happens when you disable MCP Turning off the Customer.io MCP toggle stops MCP requests immediately. Customer.io checks the toggle on every API call, so existing sessions can no longer use any MCP tools. Re-enabling the toggle restores access for those same sessions—we don’t delete the underlying OAuth tokens, so users don’t need to reconnect. If you want to revoke a specific connection permanently, the user who created it has to revoke their session in personal settings. Find your workspace When you use Customer.io MCP, you’ll need to tell your AI tool which workspace to work in. You can select a workspace by name or by ID. Manage your MCP sessions To see active sessions or revoke a connection, go to Settings > Personal Settings and click View sessions under Connected clients. Revoking a session ends it immediately and forces the client to re-authenticate. Install and troubleshoot SDKs Customer.io MCP includes skills that can help you install and troubleshoot any of our SDK-based integrations, including mobile SDKs or any of our web-based integrations like our JavaScript client. You can ask your AI tool questions like: Can you help me integrate with the Customer.io SDK for iOS? My users aren’t receiving push notifications on Android. Can you help me troubleshoot? Your AI tool will help you update your code and return detailed steps to help you find and troubleshoot issues. 403 errors after a successful connection If your MCP tool can connect and authenticate but every tool call fails with a 403, your account admin probably turned off the Customer.io MCP toggle. Customer.io checks the toggle on every request, so a freshly authorized client still gets rejected if the toggle is off. Ask your admin to re-enable MCP in Privacy, Data, & AI settings. Troubleshooting OAuth client not found error If you see an “OAuth client not found” error, it means the connection between your MCP tool and Customer.io expired or was reset during the authorization process. This typically happens when your AI tool (like Claude or Cursor) restarts, disconnects, or refreshes its connection while you’re completing the authorization flow. If you complete authorization in a browser tab from a previous connection attempt, Customer.io can’t find the original session and returns this error. To fix the error, try the following things: Close any open “Customer.io authorization” browser tabs. Fully remove the Customer.io connection. Restart your AI tool. Re-add the Customer.io MCP connection from scratch. Complete the authorization flow only in the newly opened browser tab. --- ## ChatGPT setup URL: https://docs.customer.io/ai/mcp/chatgpt/ You can integrate ChatGPT with Customer.io in your personal settings so you can engage your workspace through ChatGPT conversations. ChatGPT prerequisites You must have a ChatGPT Pro, Plus, or Enterprise plan and enable Developer Mode in your ChatGPT account before you can add Customer.io as a connector. Enable Developer Mode You must be an admin in your ChatGPT account to enable Developer Mode. Go to ChatGPT Settings > Connectors. Click Advanced settings > Developer mode. You may have to scroll down to find Advanced settings. Turn on Developer mode. Add the MCP connector to ChatGPT You’ll need our MCP server URL: US region: https://mcp.customer.io/mcp EU region: https://mcp-eu.customer.io/mcp If your application requires you to set a type or transport method, set it to http. We don’t support the server-sent events (sse) transport type. Go to Connectors on the ChatGPT web app. You can’t complete this process on the mobile app. If your admin limited the connectors you can use, click the custom connector that represents Customer.io. Then click Manage followed by Connect. Otherwise, click Create then fill in the details: Name: Customer.io Description: Access Customer.io workspace data and tools MCP Server URL: the US or EU URL above Authentication: OAuth Select each Customer.io workspace you want to give ChatGPT access to then click Allow access. After you connect, pick the workspaces you want to use with the MCP server. Use the Customer.io connector in conversations To test it out, open a ChatGPT conversation: Click then choose Developer Mode. Click the Customer.io connector to make it a source.  Direct ChatGPT to use the Customer.io connector For best results, clearly ask ChatGPT to use the Customer.io connector (and not built-in tools) in your conversation, and specify any steps you want it to follow. Find your workspace When you use Customer.io MCP, you’ll need to tell your AI tool which workspace to work in. You can select a workspace by name or by ID. Manage your MCP sessions To see active sessions or revoke a connection, go to Settings > Personal Settings and click View sessions under Connected clients. Revoking a session ends it immediately and forces the client to re-authenticate. Troubleshooting If you run into the error “This MCP server doesn’t implement our specification,” then the connector is missing a required tool or Developer Mode hasn’t been enabled on your ChatGPT account. Check with your ChatGPT administrator to update the connector. For more on troubleshooting, check out OpenAI’s info on connectors. --- ## Claude setup URL: https://docs.customer.io/ai/mcp/claude/ You can integrate Claude with Customer.io in your personal settings so you can engage your workspace through Claude conversations.  MCP support isn’t available for Claude’s free plan You need a paid Claude plan to use MCP connectors like Customer.io’s. Use the built-in connector Claude Desktop has a Customer.io connector that you can enable to use Customer.io. In Claude Desktop, go to Settings > Connectors. Find and enable the Customer.io connector from the list. If you don’t see it, your organization may need to approve the connector. Contact your IT admin to enable it. Click Connect and authenticate with Customer.io. After you connect, pick the workspaces you want to use with the MCP server. Set up a custom MCP connection If you can’t find the connector in Claude Desktop, you can add Customer.io as a custom MCP server. You’ll need our MCP server URL: US region: https://mcp.customer.io/mcp EU region: https://mcp-eu.customer.io/mcp If your application requires you to set a type or transport method, set it to http. We don’t support the server-sent events (sse) transport type. In Claude Desktop, go to Settings > Connectors and click Add custom connector. Add the MCP server URL above. Click Connect. After you connect, pick the workspaces you want to use with the MCP server. Find your workspace When you use Customer.io MCP, you’ll need to tell your AI tool which workspace to work in. You can select a workspace by name or by ID. Manage your MCP sessions To see active sessions or revoke a connection, go to Settings > Personal Settings and click View sessions under Connected clients. Revoking a session ends it immediately and forces the client to re-authenticate. --- ## Cursor and other IDEs URL: https://docs.customer.io/ai/mcp/ide/ Connect your IDE to Customer.io MCP. We have a one-click setup for Cursor, plus instructions for any IDE that supports MCP (Windsurf, VS Code, and more). Cursor (one-click setup) Cursor has a one-click installer in your Customer.io personal settings. Go to Settings > Personal Settings. Click the Cursor tab and click Add to Cursor. In Cursor, click Needs Login to authenticate with Customer.io. After you connect, pick the workspaces you want to use with the MCP server. Windsurf, VS Code, and other MCP-capable IDEs For any IDE that supports MCP, you can add Customer.io as a custom MCP server by editing your tool’s MCP configuration. You’ll need our MCP server URL: US region: https://mcp.customer.io/mcp EU region: https://mcp-eu.customer.io/mcp If your application requires you to set a type or transport method, set it to http. We don’t support the server-sent events (sse) transport type. Add an entry like this to your MCP config (the example uses the US URL—swap for the EU URL if your account is in the EU): { "mcpServers": { "CustomerIO": { "type": "http", "url": "https://mcp.customer.io/mcp" } } } After you save the config, your IDE will prompt you to authenticate with Customer.io. After you connect, pick the workspaces you want to use with the MCP server. Find your workspace When you use Customer.io MCP, you’ll need to tell your AI tool which workspace to work in. You can select a workspace by name or by ID. Manage your MCP sessions To see active sessions or revoke a connection, go to Settings > Personal Settings and click View sessions under Connected clients. Revoking a session ends it immediately and forces the client to re-authenticate. --- ## Update your MCP client URL: https://docs.customer.io/ai/mcp/mcp-migration/ When we add, rename, or change MCP tools, you might need to update hardcoded tool references or reconnect your client. Update hardcoded tool references If you’ve hardcoded references to specific v1 tool names—like create_segment, get_profile, or list_segments—in skills, workflows, etc, you’ll need to update them. In most cases, we recommend that you use natural language prompts like “list my campaigns”. Check your workflows, scripts, and prompts for hardcoded tool names: Custom GPT instructions that name specific tools (for example, “use the create_segment tool”) Claude Projects with tool references in the system prompt Saved prompts or workflows that call out specific tools by name Scripts that call the MCP server directly with a specific tool shape Reconnect your client If your client struggles to pick up the new tools, you may need to reconnect. Remove the Customer.io connection from your client (Claude Desktop, ChatGPT, Cursor, or wherever you’ve configured it). Re-add the connection following our setup instructions for ChatGPT, Claude, or Cursor and other IDEs. Verify the new tools are available—ask your AI tool to list the available Customer.io tools, or try a prompt that depends on the new tool surface. Your server URL and authentication don’t change. You just need a fresh connection so your client picks up the latest tool list. May 2026 changes: expanded capabilities, tools replaced with cio_* We replaced the v1 tool architecture (purpose-built tools like create_segment and get_campaign) with a smaller set of generic, schema-aware cio_* tools that cover the full Journeys UI API and CDP Data Pipelines API surface. Your existing sessions and natural language prompts continue to work—you don’t need to do anything if you interact with the MCP server through normal conversation. The current version supports write operations the v1 set didn’t: Create, edit, and launch campaigns—build and manage automated workflows from your AI tool. Send newsletters—draft and send one-time messages to your audience. Manage segments—create, update, and delete segments. Work with transactional messages—manage your transactional email and push content. Access customer profiles—look up and manage people in your workspace.  Need more time? If this change affects your workflows and you need time to update them, contact us. We can give you a 30-day window to continue using the previous toolset while you migrate. --- ## Email content analysis URL: https://docs.customer.io/ai/content-analysis/ Email inboxes are noisy these days, and providers have taken steps to filter messages. Our AI-powered content analysis feature helps you understand your email content and improve deliverability so that your messages aren't inadvertently filtered out.  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. How it works When you work on an email in your campaign, broadcast, or newsletter, you can click Analyze Email Content to review your email and highlight ways you can improve deliverability so your message gets to the audience you want to build relationships with.  Wait until you’ve finished writing your email For the best results, wait to analyze your email until you finish writing it. This ensures that we provide the most relevant suggestions for your message. Analyzing an email In the top-right corner of the email composer, click Analyze Email Content. Analysis report Content analysis checks your message for common deliverability issues, like: Trustworthiness: Does your message look credible? Could a user interpret it as spam? Clarity: Is your message clear and easy to understand? Does it have a clear call to action? Brand: Is your message consistent with your brand’s voice and tone? Issues with 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}}.: Are there issues with liquid that could prevent your message from being sent? For example, if you specify an attribute like {{ customer.first_name }}, but the attribute is not set, your message won’t send. You can fix this kind of issue by adding a fallback value for the attribute! --- ## In-app message suggestions URL: https://docs.customer.io/ai/in-app-suggestions/ Building in-app messages can sometimes be a challenge—while we have a block-based editor, you also need to think about positioning, design, the pages or screens you want to show, and more. With AI, we'll suggest some messages to get you started and help you build messages faster.  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. How it works When you drag an in-app message into your campaign and click it, you’ll see a section that says Add Content. Just below that, you’ll see Suggested with AI. Here we’ll provide some starter messages based on your campaign and message objectives. It takes a moment to generate suggestions, and you can always click View more to see more treatments. How to turn on this feature You must enable two settings in order to use this feature: If you’re an Account Admin, enable Customer.io AI in Account Settings. Otherwise, reach out to an Account Admin for help. Enable In-App Message Suggestions in Experimental Features. Experimental Features are personal settings; this only controls whether AI-powered in-app survey analysis is available to you, not your teammates. Getting better suggestions We rely on things like your campaign’s name, description, and publicly available information about your company to generate suggestions. To get the best possible suggestions, you should: Give your campaign a meaningful name and a description that explains who it speaks to and why. Clearly describing the purpose of the campaign helps us generate better suggestions. Provide a goal for the campaign. Knowing the outcome you hope to achieve—in Customer.io’s terms (performing an event, joining a segment, or leaving a segment) helps us generate better suggestions. Build your campaign logic before you add content. If your message is a part of a larger campaign with conditions and branching logic, that logic plays into the suggestions we generate. If you’ve built your campaign logic already, we’ll use that logic to generate better suggestions! Suggestions are just starters You can—and should—edit the suggestions to make them your own. While we want to generate better suggestions over time, they’re meant to compliment your own creativity, expertise, and knowledge of your customers. --- ## In-app survey analysis URL: https://docs.customer.io/ai/in-app-survey-analysis/ In-app surveys help you collect feedback from your users, but it can be tough to analyze the results and determine your next steps. Our **Analyze Survey** feature helps provide you with actionable insights from your surveys.  Not seeing this AI feature? Make sure “Customer.io AI” is enabled in Privacy, Data, & AI settings. Reach out to an Account Admin if you can’t edit the toggle. How it works When you create an in-app survey, you’ll add buttons with Tracked Name parameters. When someone interacts with the button, you’ll see the results in Tracked Responses on the Metrics tab of your campaign or API-triggered broadcast. The Analyze Survey feature observes the responses to your survey (or any set of in-app messages with multiple buttons) and provides you with insights based on responses to your messages. How to turn on this feature You must enable two settings in order to use this feature: If you’re an Account Admin, enable Customer.io AI in Account Settings. Otherwise, reach out to an Account Admin for help. Enable Survey Analysis in Experimental Features. Experimental Features are personal settings; this only controls whether AI-powered in-app survey analysis is available to you, not your teammates. Analyze a survey You can run a survey on any live campaign or API-triggered broadcast that contains in-app messages. You’ll get the best results if you’ve given your survey options or other buttons descriptive Tracked Name values and gathered a significant number of responses. Go to your campaign or broadcast’s Metrics tab. Under Tracked Responses, click Analyze Survey. Data quality assessment As a part of survey analysis, we’ll also assess the quality of the available response data. This is where we’ll tell you whether or not there’s enough data to make a truly informed decision. While we’ll provide analysis in any case, we’ll let you know if we aren’t confident given the available data. If your survey hasn’t collected enough responses for us to be confident in our analysis, you might want to wait for more people to respond and re-run the analysis. Getting the best possible analysis We rely on the contents of your message, the names and descriptions of your campaign, and the Tracked Name values for your in-app buttons as a part of our analysis. When you build your in-app messages or surveys, make sure you give names and descriptions to your campaign and messages. You’ll also want to set descriptive Tracked Name values for your in-app buttons or survey options to help us understand the purpose of your survey and what each response means! --- ## Use our docs with AI URL: https://docs.customer.io/ai/use-docs-with-ai/ You can use our documentation and API specifications with your AI tool of choice, making it easy to search and get answers from our docs right from your editor or other AI tools. How it works Large language models (LLMs) are great at reading markdown because, unlike HTML, there’s a lot less noise for them to parse. To make our documentation more accessible to AI, we’ve provided markdown-accessible versions of most of our documentation so you can reference it with your own AI tools. You can access plain-text markdown versions of our docs in a few ways: (Recommended) Our llms.txt file: This is an emerging standard and acts as a sort of LLM-friendly sitemap. It provides links to our markdown files so your LLM can access markdown versions of our pages without having to download them. Download individual markdown pages: you can either use the download link in the sidebar or just append .md to the end of most URLs to get the markdown version of the page. You can add individual pages to your own knowledge base or use them as a reference for your LLM. Download our API specifications: our APIs are documented using the OpenAPI 3.0 standard, which LLMs typically understand. You can use them to build integrations with Customer.io. Use our llm sitemap You can access or download our llms.txt file to help your LLM access, and provide answers based on, markdown versions of our documentation. The llms.txt file is a flattened version of the sitemap for docs.customer.io based on an emerging standard for LLM-friendly documentation. While it’s still an early standard, it can be a handy way to get answers from our docs without having to open a new browser tab or download individual pages. Access our llms.txt file Learn more about the proposed llms.txt standard Download plain text documentation You can download or copy our documentation as plain text markdown files for use with your LLM of choice—you can even create your own GPTs and tools! You can either use the Copy and Download buttons in the right sidebar of our documentation or add .md to the end of (almost) any URL. For example, you can check out the markdown for this page at https://docs.customer.io/get-started/with-llms.md. While you’re also welcome to use our HTML-rendered pages, plain text is generally easier for LLMs to handle than HTML because: Plain text contains fewer formatting tokens. Plain text shows content that may not be rendered in HTML, like content hidden in unselected tabs. LLMs can parse and understand markdown hierarchy. Download OpenAPI specifications You can download our OpenAPI specifications as JSON for help building integrations with Customer.io. Because these specifications are written in OpenAPI, they’re easy for LLMs to parse and understand. Pipelines API: Get data into Customer.io using an API similar to Segment and other CDPs. Most of our SDKs rely on this API. Track API: The classic way to bring data into Customer.io. Many partners rely on this API. App API: This is the API you would use if you wanted to build an “app” around Customer.io. From here, you can trigger broadcasts, send transactional messages, and get or update information about people, messages, etc. Webhooks API: Describes our endpoint to create a reporting webhook and all the events we’ll send to your webhook endpoint. We also offer Postman collections that can help you try things out. You’ll find the postman collection for any of our APIs on the individual API pages.