Skip to main content
Plan and run a migration of your Customer.io automations, transactional messages, and subscription preferences to Courier.
Customer.io runs lifecycle marketing and transactional messages from one account. Both move to Courier: automations (formerly campaigns) become , transactional messages become you send through the , and broadcasts become Courier .

Map Customer.io concepts to Courier

Automations and journeys

A Customer.io automation starts when a person matches a trigger, then runs its workflow of messages, delays, and branches. A Courier does the same with , , , , and nodes. Automation triggers become journey triggers: an API invoke, an inbound webhook, a Segment event, or a user joining an audience. Build journeys . Your growth team edits content and timing on the canvas while your engineers connect the product events behind it. One behavior to plan around: a journey run reads the recipient’s profile once, when it starts. If a later step depends on something that changes during the run, like whether a trial activated, fetch it with a right before the step that needs it. The example below shows how.

Transactional messages

Customer.io transactional messages are templates your app sends with an API call. In Courier that’s a and a call to the . One template can hold email, push, SMS, chat, and Inbox content, and Courier picks channels from your routing and the user’s preferences.

Segments and audiences

Courier group users by profile attributes and keep membership current as profiles change. A data-driven segment built on profile fields, like plan = trial, becomes an audience. A manual segment becomes a list. Segments built on event history, account relationships, or rolling time windows are usually easier to compute in your app or warehouse. Your app then invokes the journey for the right people, or keeps a up to date.

People, objects, and profiles

Courier hold each user’s email, phone, push tokens, and any custom fields you personalize with. Keep the same user ID you use in Customer.io so both systems point at the same person. Customer.io objects like companies or accounts usually map to , which carry their own branding, defaults, and preferences.

Subscription preferences

Customer.io stores each person’s topic and channel choices in the cio_subscription_preferences attribute, and a global unsubscribe in unsubscribed (docs). Courier let users opt in or out per topic and pick channels per topic, and Courier enforces them at send time. Map each Customer.io topic to a Courier subscription topic, then link each template to its topic. Move what each person actually receives, not the raw attributes: Users manage their choices on Courier’s or an in your app.

Liquid and Handlebars

Customer.io templates use Liquid. Courier uses , with {{variable}} syntax and helpers for conditionals, loops, and formatting. Simple variables carry over with a rename: {{customer.first_name}} becomes {{data.first_name}} when you pass it in the send, or a {{profile.*}} variable when it’s stored on the profile. Rewrite Liquid filters and tags as Handlebars helpers.

In-app messages and Inbox

Courier is an in-app notification center with drop-in components for , , , and . It runs on the same pipeline as email, push, and SMS, so one template and one preference setting cover all of them.

Why Courier

  • Billed per send, not per profile. Courier counts each template sent to a recipient. A large database you message lightly doesn’t drive your bill. See .
  • Product and lifecycle on one platform. Transactional sends, journeys, broadcasts, Inbox, and preferences share one pipeline, one set of users, and one log.
  • Everything in the UI is on the API. Your growth team builds in the journey canvas and Design Studio. Engineers and AI agents work on the same journeys and templates through the API, CLI, and MCP server.
  • Preferences you can embed. A hosted page or a drop-in component, with per-topic channel choices and tenant defaults.
  • Bring your own providers, with failover. Connect SendGrid, Postmark, Amazon SES, Twilio, Firebase FCM, APNs, and more. Configure a backup per channel and Courier when the primary is down.
  • Full delivery logs. trace every message from API request to provider response, with the rendered content.

Example: migrate a trial-activation automation

A Customer.io automation that sends a welcome email, waits two days, and follows up only if the trial hasn’t activated becomes one Courier journey. Rebuild it in your Test environment, check it, then switch the trigger.

Build the journey

Create a journey with an API trigger and two data fields: trial_id and first_name.
Shorten the while you test. Each delivers one channel with its own journey template. To add an Inbox reminder, add a second Send node. asks your app for the trial’s current status right before the follow-up, so a customer who activated during the delay doesn’t get it:
Pass {{recipient}} in a header so your endpoint can check that the trial belongs to that user. Return the status as JSON:
Set the merge strategy to Overwrite so the fresh value replaces anything already in data. The then reads it. If the request fails, the run stops before the follow-up. See . In the journey’s settings, set the cancelation token to trial-{{data.trial_id}}. It replaces the automation’s exit condition. Publish each journey template, then publish the journey. Publishing the journey doesn’t publish its templates.

Start a run

Where your app sent the event that triggered the Customer.io automation, instead:
A retry with the same Idempotency-Key returns the first run instead of starting a second. Courier keeps the key for 25 hours by default.

Cancel when the trial activates

When activation happens in your app, cancel by token:
Cancel only touches active runs, so it’s safe to retry and safe to call after the run has finished. Messages already delivered stay delivered.

Test, then switch

Check these in your Test environment: Then send the event to Courier instead of Customer.io, so each event has one sender. In Customer.io, stop the automation and choose Finish their journey, so people already in it get their remaining messages and no one new enters.

Migrate step by step

1

Create your Courier workspace

and create a workspace. Test and Production have separate API keys, so you can build and test without touching live traffic.
2

Connect your providers

Customer.io sends email through its own infrastructure unless you set up a custom SMTP server. If you have one, connect that same provider to Courier. In Courier, go to Integrations and connect the providers you want for each channel: an email provider like SendGrid, Postmark, or Amazon SES, plus SMS and push providers. Add a backup provider per channel for .Set up your sending domain’s SPF, DKIM, and DMARC records with your email provider before your first production send. needs no external provider.
3

Move your people

Export people and the attributes your messages use from Customer.io (export docs). Create a Courier profile for each with the same user ID, through the or a .
Keep both systems in sync until cutover. Your app should write profile changes to Courier from the moment you export.
4

Move preferences

  1. Create a for each Customer.io topic, with a matching default state
  2. Import each person’s choices through the
  3. Link each template to its topic
Import preferences before any optional message sends from Courier. If someone unsubscribes in Customer.io after your export, carry that change over too.
5

Recreate templates

For each transactional message, create a in Design Studio, add a channel for each place it sends, and convert Liquid to . Publish it.Content inside a journey lives in that journey’s Send nodes, so build it there.
6

Rebuild automations as journeys

Rebuild each automation as a , following the example above. Start with one automation that has a clear trigger and a clear goal, check it end to end, then move the rest.Rebuild one-time sends as to a list or audience. An API-triggered broadcast becomes a journey you invoke for each recipient, or a broadcast to a list.
7

Update your send calls

Replace Customer.io transactional calls (POST /v1/send/email and the other /v1/send/* endpoints) with Courier’s :
Replace the events that started automations with .
8

Set up event streaming

If you stream delivery events from Customer.io’s reporting webhooks, set up to send message events (sent, delivered, opened, clicked) to your warehouse or analytics tools.
9

Test and cut over

  1. Send test messages in your Test environment and check them in
  2. Check that preferences, routing, and rendered content match your Customer.io setup
  3. Move one message type or automation at a time to your Production API key, so each event has one sender
  4. Stop the matching Customer.io automation with Finish their journey

Migrate with an AI agent

Most of this migration is mechanical: read an automation, rebuild it as a journey, rewrite the call site. A coding agent handles that well. Set up three things first:
  • teaches the agent Courier’s real APIs and patterns:
  • gives the agent access to your workspace: create templates, build journeys, upsert users, and read message logs. See for setup in Claude Code, Cursor, Codex, and VS Code.
  • Docs for agents. Point it at llms-full.txt for API detail beyond this page.
Then start it with a prompt like this one. Fill in the bracketed parts.
Check the Liquid conversion in step 4 closely. Each {{customer.*}} value becomes either a profile field you sync or a value you pass in data. Decide which before you build the templates.

Map the API

FAQ

Can I import Customer.io automations directly?

No. Rebuild each automation as a journey. Most automations map step for step, and the example above shows the pattern. An agent with the Courier MCP server can do most of the rebuilding.

Do I need my own email provider?

Yes. Courier sends through providers you connect, like SendGrid, Postmark, or Amazon SES. You keep your provider relationship and sender reputation, and you can add a backup provider for failover.

What should I migrate first?

Start with transactional messages. They’re single sends with a clear trigger, so they’re quick to rebuild and check. Then move one lifecycle automation end to end before moving the rest.