Guide

How to Migrate from Customer.io (Step-by-Step)

June 24, 20269 min readThe Trigger Engage team

Whether it's the per-profile bill creeping up every quarter or a growing wish to own your own customer data, plenty of teams decide to migrate from Customer.io. The good news: a clean migration is very doable if you plan it. The bad news is that the sloppy version double-sends messages to real people and loses years of engagement history. This guide walks the whole move step by step so you avoid both.

A migration flow from Customer.io: export, map events, rebuild, shadow mode, then cut over
A staged migration — with a shadow period — avoids double-sends and lost data.

Why teams migrate

Customer.io is a capable, well-built platform, and for many teams it's the right call to stay. But three reasons come up again and again when people decide to leave. The first is cost at scale: pricing is tied to the number of profiles you store, so as your list grows — including dormant and duplicate records — the bill climbs whether or not those people ever open an email. The second is data ownership: your profiles, events, and message history live in someone else's system, and getting a full, portable copy takes deliberate effort. The third is self-hosting: some teams, especially in regulated or privacy-sensitive markets, want the whole engine running on infrastructure they control.

If you're still deciding where to go rather than how to get there, start with our roundup of Customer.io alternatives and come back once you've shortlisted a destination. One option worth a look is Trigger Engage, an open-source (MIT) engine you can self-host or embed directly in a Laravel app — there's no per-profile fee, so a large, partly-dormant list costs you nothing extra.

Before you start: audit what you have

Migration is mostly a data and inventory problem, not a coding problem. Before touching anything, write down what actually exists in your current account. Inventory your campaigns and journeys, your segments, your email and message templates, every tracked event name, and every integration that reads from or writes to Customer.io (your app, your CDP, your webhooks, your analytics). Then mark what's genuinely in use. Most accounts carry a long tail of abandoned drafts, one-off broadcasts, and segments nobody references. You do not need to port those. Migrating only what's live is the single biggest thing that keeps the project from ballooning.

Tip: Rank your journeys by how many people actually flow through them each week. Rebuild the top handful first, verify them in shadow mode, and treat the long tail as optional — much of it can be quietly retired rather than migrated.

The migration, step by step

With the audit done, the move itself breaks into five stages. Do them in order — the early stages make the later ones safe.

  1. Export your data. Pull your people and their attributes, your segment definitions, your templates, and your event history out of Customer.io. At a high level, Customer.io provides account data exports plus an API you can page through for people and their events; the exact screens change over time, so follow their current export documentation rather than any fixed click-path. Save everything as flat files (CSV or JSON) in your own storage first — that archive is your safety net regardless of which tool you land on.
  2. Map events and identifiers. This is the step that makes or breaks a clean cutover. Keep the same person identifiers (the ID you already use to `identify` a user) and the same event names on the new side. If a signup fires account_created today, it should fire account_created tomorrow. Because both platforms follow the identify/track pattern, your call sites port with only the client swapped. A tiny before/after makes it concrete:
    // Before — Customer.io
    Customerio::identify($user->id, [
        'email' => $user->email,
        'plan'  => $user->plan,
    ]);
    Customerio::track($user->id, 'account_created', [
        'source' => 'web',
    ]);
    
    // After — Trigger Engage (same IDs, same event names)
    TriggerEngage::identify($user->id, [
        'email' => $user->email,
        'plan'  => $user->plan,
    ]);
    TriggerEngage::event($user->id, 'account_created', [
        'source' => 'web',
    ]);
    Keeping names and IDs stable means your journeys and segments port cleanly instead of needing a rewrite. For the Laravel specifics of wiring these calls, see firing events from Laravel.
  3. Rebuild segments and journeys. Recreate your audiences in the new tool's segment builder and your automations in its journey builder. Because you kept the same attributes and event names, the rules translate almost one to one — "users on the free plan who haven't logged in for 14 days" is the same definition on either side. Our guides to behavioral segmentation and lifecycle email sequences cover how to structure these so they're maintainable rather than a tangle of one-off rules.
  4. Run in shadow mode. Before you send a single real message from the new system, run both in parallel. Send your events to Customer.io and the new tool, but keep Customer.io authoritative and disable outbound sending on the new side. Now you can compare: are the same people entering the same journeys? Do the templates render identically? Do the segment counts match? Trigger Engage supports exactly this shadow-mode cutover, so you can validate everything end to end without any user receiving a duplicate.
  5. Cut over. Once the counts and renders line up, flip the SDK so the new system becomes authoritative and enable its outbound sending. Import your existing suppression and opt-out list first — this is non-negotiable — and keep the old account's suppression list on hand as a reference. Watch deliverability closely on the first few real sends: a new sending domain or IP warms up gradually, so start with lower-volume, high-engagement journeys before turning on big broadcasts.

Common pitfalls

Most migrations that go wrong go wrong in a small number of predictable ways. Double-sends happen when both systems send at once — the shadow-mode discipline above prevents it. Broken suppression is the scariest: if opt-outs don't carry over, you can email people who asked never to hear from you. Lost pre-signup history bites teams that tracked anonymous visitors, since a naive import only brings identified profiles. And hard-coded template IDs quietly break journeys when the new system assigns its own IDs.

RiskHow to avoid it
Double-sends to real usersKeep the old system authoritative and disable outbound on the new side until cutover.
Broken suppression / opt-outsExport and import the suppression list before the first send; verify a known opted-out address is suppressed.
Lost pre-signup historyChoose a tool with anonymous→identified merge so early events attach to the profile once the person signs up.
Hard-coded template IDsReference templates by name or key, not by numeric ID, and re-point them after rebuild.

How long it takes

Timelines vary with account complexity, not company size. A small setup — a handful of journeys, a few segments, one app firing events — is realistically a few days of work end to end. A large account with dozens of interlocking journeys, many integrations, and a big historical dataset is more like two to four weeks, and most of that is the shadow-mode period where you're watching and comparing rather than building. Budget for the observation window; rushing the cutover is where the expensive mistakes live.

Frame the destination correctly and the whole project gets simpler: you're not just swapping one vendor for another, you're moving to a marketing-automation platform you actually control — one where segments, journeys, and data all live on infrastructure you own. Trigger Engage runs standalone or embeds in Laravel via composer require trigger-engage/server; the docs cover installation, the SDK, and the shadow-mode workflow in detail.

Frequently asked questions

Can I export my data from Customer.io?
Yes. Customer.io offers account-level data exports plus an API you can page through to pull people, their attributes, and their event history. Export everything to your own storage as CSV or JSON before you start rebuilding — that archive is portable to any destination and is your fallback if anything goes wrong mid-migration.
Will I lose message history when I migrate?
Not if you export it first. Historical sends live in the old system, so archive that reporting data before you close the account. Ongoing behavioral history stays intact when you keep the same person IDs and event names, and a tool with anonymous→identified merge preserves pre-signup activity that a naive import would drop.
How long does a Customer.io migration take?
A small account is a few days; a large one with many journeys is typically two to four weeks including a shadow-mode period. The build itself is fast — the time goes into auditing, running both systems in parallel, and comparing counts and renders before you flip outbound sending on.
Is there a free alternative to Customer.io?
Yes. Trigger Engage is open source under the MIT licence, self-hostable, and has no per-profile fee, so a large or partly-dormant list doesn't inflate your bill. Its identify/event SDK mirrors the identify/track pattern, which is what makes porting your existing call sites a small change rather than a rewrite.