Looking to go the other way, designing in Brew and sending from Customer.io? That Customer.io integration is coming soon.
Concepts
1. Export Profiles, Consent, and Suppression
Go to People and open the Export to CSV menu in the top right. 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 with everyone in it, so the status split happens at import. The suppression list is a separate export. Go to Workspace Settings → Workspace Suppressions, export the CSV, then download it from the Exports page. The CSV is hashed, not a plain list. Its columns arecio_id, id_sha256 and email_sha256, with id and email hashed as lowercase SHA-256. Hash the email values in your profile export the same way to match rows.
This list covers people deleted and suppressed for GDPR. Unsubscribes come through the unsubscribed column of your profile export.
Bounces and spam complaints sit in Workspace Settings → Email → Suppression List. That list has no export button, so pull it with Customer.io’s ESP Suppression API if you need it.
Customer.io has already deleted the people on the Workspace Suppressions list, so they aren’t in your profile export. Keep that hashed list so your backend never re-adds them. Hash an address the same way before you create a contact or fire a trigger for it. Brew’s importer also never re-subscribes a contact who previously unsubscribed.
Keep any opt-in date and source columns in the export. Bring Your Consent Records covers carrying them over.
2. Map Your CSV
Notes:
- Customer.io reserves a set of fixed-name attributes that won’t map 1:1 to Brew’s own reserved names, including
id,cio_id,email,phone,created_atandunsubscribed. Rename collisions before mapping. - Per-topic preferences live in a separate attribute,
cio_subscription_preferences. Export it too if you use subscription topics. Someone who opted out of one topic imports as subscribed to all marketing, unless you mark themSubscribed: falseor import them into the matching domain’s unsubscribe list. - Don’t import
unsubscribedas it is. Brew reads opt-outs only from aSubscribedcolumn, so anunsubscribedcolumn becomes a plain custom property and those people import as subscribed. created_atis skipped, because Brew manages its own creation date. Rename it, for example tocioCreatedAt, to keep the date.- Dates: day-first dates such as
03/04/2026(3 April) import correctly when you pick day first at the mapping step, andYYYY-MM-DDneeds no choice. See date formats.
Created At, see Add Contacts. Custom properties can’t use the names of Brew’s built-in contact fields, listed in Custom Properties.
3. Import into Brew
Audience → Add contacts → CSV Upload. Map columns, review, then import. Tick the validation checkbox first if the list hasn’t been checked recently. It costs credits, which is worth it before warming up a new domain. Validate Contacts has the cost and Add Contacts the full import behavior.Bring Your Consent Records
A contact’sSubscribed status says Brew may email them. A consent record says how and when they agreed. Carry both over.
- Export the evidence. Include any opt-in date or subscription-preference attribute your Customer.io profiles carry. Keep those columns as custom properties.
- In the app, tick the consent checkbox at the review step. Brew records consent once for the whole file, dated the day you import.
- To keep each person’s original opt-in date, import over the API instead, with a
consentrecord per contact. See Add Contacts for the consent fields.
4. Rebuild Segmentation
Data-driven segments are the one structural difference to plan for. Customer.io segments can filter directly on event history, including page events. In Brew, write that signal onto the contact as a custom property, then build the Audience on it. Lists and Event-Based Segments has the details. 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. Rebuild each one as a custom property plus an Audience filtered on it. If people joined a segment themselves, decide how to honor that choice before your first send.5. Rebuild Templates and Merge Tags
Open Emails in the sidebar, then use the Import tools on the right side of the canvas. See Importing existing designs.
Set your 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.
Translate Merge Tags
Brew renders standard Liquid on every workspace, so most templates need only renamed fields:{{ customer.first_name | default: "there" }}becomes{{ customer.firstName | default: "there" }}. Brew field names are camelCase, and a snake_case name renders empty.{% if %}...{% else %}...{% endif %}conditionals,{% for %}loops, and filters render at send time.{{ trigger.order.total }}reads the nested payload you send with the fire, no flattening required.- In an event-triggered campaign,
{{ event.plan }}becomes{{ trigger.plan }}. Brew has noevent.namespace. - Customer.io-only tags and filters, such as
add_day,format_number,unsubscribe_urlandview_in_browser_url, need rewriting. Brew reads an unknown filter name as fallback text.
{{ firstName | there }} is a merge tag with a fallback, not a filter chain.
Still needing a rebuild, because the data model behind them has no Brew equivalent:
- Journey attributes (
{{ journey.attribute }}), temporary values scoped to a single workflow run - Object and relationship loops over Customer.io’s Objects (
{% for class in objects.online_classes %}). Loops over payload data you send yourself work fine - Snippets (
{{ snippets.footer }}), reusable blocks, which you rebuild as standing instructions on your brand instead
6. Rebuild Automations
Brew has no exit conditions or re-entry settings like Customer.io’s. A contact update doesn’t remove anyone, and a repeat event starts a second run. To exit people who no longer match, add a Filter after each Wait, as Conditions After a Wait shows.
Test-fire before publishing, and leave every rebuilt automation unpublished until cutover day. 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 API key over HTTP basic auth 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 keep contacts current and power every automation you rebuild. Before, with Customer.io’s Track API:identify call covers every app user, so the contact is created with subscribed: false. For people who opted in to marketing, send subscribed: true with a consent record instead.
subscribed: true applies only to a new contact. Brew refuses it for anyone who opted out, and Unsubscribes says how to retry.
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, and both move to the same fire endpoint.
Transactional messages become triggers with one-step automations. In Brew, a transactional email is an automation. Create a trigger once, declaring the payload fields, and wire it to a one-step automation. Its Send Email node sends from a domain with a transactional sending purpose.
That domain purpose is what makes the send transactional: no unsubscribe link required, and delivery reaches unsubscribed contacts. Publish it once, then your code fires it by trigger id. transactional_message_id maps to the triggerEventId you fire, and message_data becomes payload, nested JSON and all.
Before, with Customer.io’s App API:
email field is the contact key, so there’s no separate to. Templates read the payload as {{ trigger.order.total }} and contact fields as {{ customer.firstName }}. Those are the same namespaces your Customer.io templates already use.
GET /v1/automations/triggers/{triggerEventId} returns the trigger’s payloadSchema, the exact contract every fire is validated against. Diff it against what your code sends, then call GET /v1/automations/triggers/{triggerEventId}/readiness to preflight the wiring without firing. QA first with a test run of the automation, which applies the same payload rules as a live fire.
API-triggered broadcasts use the trigger fire endpoint instead:
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.
One exception: a broadcast aimed at a whole saved segment isn’t a per-recipient event. Use a manual-audience automation run, 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. This section shows the shape of the migration, not a copy-paste replacement for your code.
8. Cut Over
Move in this order:- Rebuild while Customer.io keeps sending. Import contacts and rebuild segments, templates, and automations. None of it touches live sending. Keep every Brew automation unpublished.
- Check your segments. Confirm each recreated Audience’s membership count against the original. See Step 4.
- Refresh opt-outs. Right before your first Brew send, export Customer.io’s new unsubscribes and bounces and import them into Brew as
Subscribed: false. That stops marketing only. Transactional sends still reach these contacts, so don’t fire transactional triggers for addresses that hard-bounced or complained. Repeat before each type you move, until Customer.io stops sending. Anyone who opted out during the rebuild stays opted out. - Warm up with the first type you move. Pause that type on Customer.io first, so nobody gets it from both. Send to your most engaged contacts first and ramp over several days. Gradual send does the ramping for you. No history means no reputation, even on a verified domain.
- Move one type at a time. Broadcasts first, then automations. For each automation, in order: pause it on Customer.io, publish the Brew version, then point its signup forms, event tracking, or inbound integrations at Brew. Watch each in analytics before you move the next.
- Move transactional messages last. Before you move any, add a separate transactional-purpose subdomain (see Transactional Email). A fire for a new address creates a subscribed contact, so only send marketing to people who opted in. They are the highest-stakes sends. Cut them over one at a time, and test-run each automation with a real payload before you point production code at its trigger.
- Keep Customer.io as a fallback for a couple of weeks, then close it down once every message type has a clean week in Brew.
From an Agent or Code
Steps 3 to 5 also run from an AI assistant connected over MCP. Ask in plain English, one step at a time:“Import subscribed.csv, then unsubscribed.csv, as separate batches, and record the consent source as import.”
“Create an audience of subscribed contacts whose plan is pro, and tell me how many it holds.”
“Import this HTML as an editable email called Newsletter.”Check each count against the old platform before you move on. From code:
- Contacts.
POST /v1/contacts/import-csv, orbrew-cli contacts import-csv --file contacts.csv, once per file. Add Contacts covers limits and theconsentrecord. - Segments.
POST /v1/audienceswith anameandfilters, orbrew-cli audiences create. - Templates.
POST /v1/emails/importwith theformatandcontent. Import is free and uses no AI.
Quick Reference
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:- Self-Service Tools
- Talk to Our Team
Search Documentation
Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.
Ask ChatGPT or Claude
Click here to open the Copy page menu at the top of this page, then send the page to ChatGPT, Claude, or your editor.