> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brew.new/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate From Customer.io to Brew

> Move profiles, Liquid templates, event-driven segments, and API calls from Customer.io into Brew, with a cutover order that avoids double-sends.

Customer.io works like a full customer data platform, not just an email tool. It has profiles, attributes, events, and objects across two separate APIs. Its messages use Liquid, a real templating language rather than simple merge tags.

That's why this guide has more steps than the Mailchimp or Loops one. Read the Concepts table first, it saves time.

This guide assumes your Brew brand and sending domain are set up. If not, [verify your domain](/get-started/verify-your-sending-domain) first. DNS changes can take up to 72 hours.

Customer.io also uses [Entri](https://www.entri.com/) for automatic domain setup, the same DNS-connection partner Brew uses. That part of the process should look familiar.

<Note>
  Looking to go the other way, designing in Brew and sending from Customer.io? See the [Customer.io integration](/integrations/export/customerio) instead.
</Note>

Rule for the whole migration: **only one platform sends live email to a given contact at a time.**

## Concepts

| Customer.io                | Brew                                                             |
| -------------------------- | ---------------------------------------------------------------- |
| Profile                    | Contact                                                          |
| Attribute                  | Custom property                                                  |
| Data-driven segment        | [Audience](/audience/create-audiences), with a limit, see Step 4 |
| Manual segment             | Custom property plus an Audience filter, see Step 4              |
| Campaign or Automation     | [Automation](/create-emails/automations)                         |
| Broadcast (one-time)       | [Email](/create-emails/emails)                                   |
| API-triggered broadcast    | Automation with a custom trigger                                 |
| Transactional message      | An automation fired by a trigger event                           |
| Liquid tag with a fallback | Brew merge tag with a fallback, simple cases only                |
| Object or relationship     | No equivalent, handle manually                                   |

## 1. Export Profiles From Customer.io

<Warning>
  Every marketing recipient must have opted in to hear from you. Importing or syncing contact data into Brew is not permission to email it. See [What You Can Send](/get-started/what-you-can-send).
</Warning>

Go to the Profiles page and click Export to CSV. Choose all attributes, displayed attributes, or specific attributes. Filter to a segment first if you only want a subset.

Customer.io gives you one CSV, not the status-split export Mailchimp uses.

The suppression list isn't self-serve. Email Customer.io's support team to request it. They return a SHA-256 hashed CSV with a compare tool rather than a plain list.

Request this early, it adds lead time the rest of the migration doesn't have.

Use the compare tool to identify which of your exported profiles are suppressed. Mark those rows `Subscribed: false` before importing, so Brew never sends to them. Brew's importer also never re-subscribes a contact who previously unsubscribed.

## 2. Map Your CSV

| Column                    | Required                                         |
| ------------------------- | ------------------------------------------------ |
| `id`, `email`, or `phone` | Yes, whichever your workspace uses as identifier |
| Anything else             | Becomes a custom property automatically          |

Notes:

* Customer.io reserves five names that won't map 1:1 to Brew's own reserved names: `id`, `email`, `phone`, `created_at`, `unsubscribed`. Rename collisions before mapping
* Brew's reserved names are `created_at`, `updated_at`, `email`, `first_name`, `last_name`, `source`, `subscribed`

**Personalization.** This step needs real judgment, not just a syntax swap.

Simple attribute substitution translates cleanly, into a Brew merge tag with a built-in fallback:

```
{{ customer.first_name | default: "there" }}   →   {{ firstName | there }}
```

Everything past that needs a manual rebuild rather than a renamed tag:

* `{% if %}...{% else %}...{% endif %}` conditional blocks
* Journey attributes (`{{ journey.attribute }}`), temporary values scoped to a single workflow run, which have no Brew equivalent
* Object and relationship loops (`{% for class in objects.online_classes %}`), tied to Customer.io's Objects data model, which Brew doesn't have
* Snippets (`{{ snippets.footer }}`), reusable blocks, which you rebuild as standing instructions on your brand instead

Event and trigger properties do have an equivalent. `{{ event.property }}` and `{{ trigger.data.property }}` become `{{ property }}`, referencing the payload schema you define on the trigger event. See [merge tags](/create-emails/automations#personalization).

If a template used only simple substitution, migration is mechanical. If it used any of the rest, budget real time to rebuild the logic.

## 3. Import Into Brew

Audience → Add contacts → CSV Upload. Map columns, tick [validation](/audience/validate-contacts) if the list hasn't been checked recently, review, then import. See [Add Contacts](/audience/add-contacts).

## 4. Rebuild Segmentation

Data-driven segments are the one real structural difference from Mailchimp or Loops. Customer.io segments can filter directly on event history.

Their own example is a segment for profiles with no login in 30 days. It's built straight off a `page_viewed` event with no derived field required.

Brew Audiences filter on three things: contact properties, subscription status, and email engagement (opens, clicks, and sends). They can't filter on other event history directly. To rebuild a segment based on event history:

1. Write the derived signal onto the contact as a custom property, using an automation or the API on whatever cadence fits.
2. Build your Brew Audience on that property, not on the raw event.

Before cutover, spot-check that a recreated Audience's membership count is in the right ballpark against the original segment. A wrong translation can undercount or overcount who gets an email, and that isn't always visible in the UI.

**Manual segments don't have a direct equivalent.** Brew has one subscription state per contact, subscribed or unsubscribed. There are no opt-in categories and no subscriber-facing preference center. Marketing emails and automations respect that flag, and transactional sends bypass it.

Rebuild each manual segment as a custom property plus an Audience filtered on it. If people opted into it themselves, note that Brew can't reproduce that self-service control. Decide how to honor the original consent before your first send.

## 5. Rebuild Templates

| You have                                            | Use in Brew                                                             |
| --------------------------------------------------- | ----------------------------------------------------------------------- |
| A Customer.io email with simple Liquid substitution | Screenshot or export, then prompt Brew to rebuild it against your brand |
| A Customer.io email with real Liquid logic          | Rebuild by hand. The logic has no direct import path                    |
| A Figma source                                      | Connect with Figma                                                      |

Open **Emails** in the sidebar, then use the **Import** tools on the right side of the canvas. See [Importing existing designs](/create-emails/emails#importing-existing-designs).

Set your [brand identity](/brand/brand-identity) once: logo, colors, fonts, and voice pulled from your site. Every rebuilt template inherits it automatically, which beats hand-porting markup with Liquid still embedded in it.

## 6. Rebuild Automations

| Customer.io                                | Brew node                                                                               |
| ------------------------------------------ | --------------------------------------------------------------------------------------- |
| Data-driven segment entry or event trigger | Integration event, or custom HTTP trigger if the source isn't a native Brew integration |
| True/false branch, multi-split branch      | Filter or Split                                                                         |
| Time window, wait until, randomized delay  | Wait                                                                                    |
| Broadcast (one-time send)                  | Email, sent to a Brew Audience                                                          |
| API-triggered broadcast                    | Automation with a custom HTTP trigger, or a manual-audience run                         |
| Transactional message                      | An automation fired by a trigger event, see Step 7                                      |

Test-fire before publishing, and leave every rebuilt automation unpublished until cutover day. [Build an Automation](/create-emails/build-an-automation) covers the trigger setup and the test step.

## 7. Swap the API Calls in Your Code

Customer.io splits its API in two. The Track API uses a Site ID and Track key to identify people and record events. The App API uses a bearer key for transactional sends and API-triggered broadcasts.

Brew uses one bearer key for everything, with no split credentials to manage.

**Identify and track.** These calls power every automation, not just transactional ones.

Before, with Customer.io's Track API:

```js theme={null}
await cio.identify(userId, {
  email: user.email,
  first_name: user.firstName,
});
await cio.track(userId, {
  name: "trial_started",
  data: { plan: "pro" },
});
```

After, with Brew:

```js theme={null}
await fetch('https://brew.new/api/v1/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BREW_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ email: user.email, firstName: user.firstName }),
});

await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_TRIAL_STARTED_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `trial-started-${subscription.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, plan: "pro" },
    }),
  },
);
```

The pattern stays the same whatever kind of email the automation sends. What changes is which automation receives the event, not this code.

**Direct sends.** Customer.io has two direct-send methods, transactional sends and API-triggered broadcasts. Both are a single App API call to different endpoints. In Brew both become the same trigger-plus-payload pattern.

Before, with Customer.io's App API:

```js theme={null}
// Transactional
await cioApp.sendEmail({
  transactional_message_id: "welcome",
  identifiers: { email: user.email },
  message_data: { first_name: user.firstName, login_url: loginUrl },
});

// API-triggered broadcast
await cioApp.triggerBroadcast(broadcastId, {
  data: { first_name: user.firstName },
  recipients: { segment: { id: segmentId } },
});
```

After, with Brew, both become a fire call against whichever automation you built for that trigger:

```js theme={null}
await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_WELCOME_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `welcome-${user.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, firstName: user.firstName, loginUrl },
    }),
  },
);
```

Derive the `Idempotency-Key` from something stable about the action, like the user id above. A key built from a timestamp changes on every retry, which defeats the point and lets a retry send twice. See [Idempotency](/api-reference/api/idempotency).

One exception: a broadcast aimed at a whole saved segment isn't a per-recipient event. Use a [manual-audience automation run](/create-emails/automations#manual-audience), `POST /v1/automations/{automationId}/run`, or send a one-off Email to an Audience instead. The fire endpoint is for per-recipient triggering.

Full request shapes are in the [API reference](/api-reference/api/api-introduction). This section shows the shape of the migration, not a copy-paste replacement for your code.

## 8. Cut Over

1. Warm up your Brew sending domain before any real volume. Send to your most engaged contacts first and ramp over several days. [Gradual send](/create-emails/send-options#gradual-send) does the ramping for you.
2. Rebuild contacts, segments, templates, and automations while Customer.io keeps sending normally.
3. Move one message type at a time. A reasonable order is broadcasts first, then automations, then transactional.
4. Give single-recipient, account-critical messages extra care, whichever category they're in. A password reset or a receipt qualifies.
5. Watch each moved type in [analytics](/analytics/reading-analytics) before you move the next.
6. Confirm recreated segments' membership counts before their automations go live. See Step 4.
7. Point signup forms, event tracking, and inbound integrations at Brew once everything upstream is verified.
8. Keep Customer.io active but unused for a couple of weeks as a fallback, then downgrade or cancel.

Never leave the same message type live in both places at once.

## Quick Reference

| Task                   | Where in Brew                                      |
| ---------------------- | -------------------------------------------------- |
| Import contacts        | Audience → Add contacts → CSV Upload               |
| Build a segment        | Audience → Create Audience                         |
| Import a template      | Emails → canvas → Import                           |
| Build a flow           | Emails → Automations tab                           |
| Create a trigger event | Trigger events, or `POST /v1/automations/triggers` |
| API keys               | Settings → API                                     |

## Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to explore it further with ChatGPT or Claude.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
