Saaspocalypse
The migration runbook

Migrating off Klaviyo — phase by phase.

Klaviyo migrations turn on three specifics: the Profiles + Events APIs use cursor pagination with a 10 req/sec burst limit, the Flows engine has no public 'export' so the trigger logic has to be rebuilt from the UI inventory, and your Shopify webhooks need to re-point in lockstep with the cutover or you'll double-charge customers' inboxes for a week.

6-10 weeks end-to-endFixed priceParallel run before cutoverYou own the code

The phases, tuned to Klaviyo

Each phase below is the Klaviyo-specific version of our standard playbook. Same shape every engagement, different specifics per SaaS.

  1. Audience + Flow audit

    1 week

    We inventory every active Flow, every Segment, every signup Form, and every SMS short code. Most stores have 20+ Flows that compounded over years — about half are no longer firing or are sending to inactive segments.

    Screenshot every active Flow as a single-page reference (the API can't export the trigger logic)
    List every Segment with its filter rules and current size
    Inventory active Forms (popups, embedded) and their UTM rules
    Audit SMS senders (short codes vs long codes, carrier compliance state, opt-in records)
    Map Shopify webhooks currently firing into Klaviyo (orders, refunds, abandoned checkouts, fulfillment)
    You walk away with

    A Flow Inventory + Segment Catalogue + the Shopify webhook switchover plan.

  2. Schema + suppression model

    1 week

    Profiles get a single Postgres table, deduped on email and phone with a unique index. Suppression and unsubscribe state flip booleans instead of inflating a bill. We preserve the Klaviyo `profile_id` for backwards compatibility with anything pointing at it.

    Design `profiles` table with dedupe on (lower(email)) and (E.164 phone)
    Design `events` (or `profile_events`) table for the activity stream
    Design `suppressions` with reason codes (hard bounce, unsubscribe, complaint, manual)
    Plan the SMS sender flow direct to Twilio (no Klaviyo markup) + 10DLC / short-code compliance
    Decide attribution windows up front so reporting agrees with Shopify + GA4 from day one
    You walk away with

    Schema migrations + a documented attribution model + a Twilio sender plan.

  3. Extract via Klaviyo APIs

    1-2 weeks

    Profiles, events, lists, segments and metrics come out via the v2024-XX REST APIs. Throttling matters here — burst at 10 req/sec, sustained ~150 req/min, and segment-level extraction is a separate dance.

    Authenticate via a Private API Key (server-side only)
    Paginate profiles with the `page[cursor]` parameter (the JSON:API style)
    Pull every event with a `filter=greater-than(datetime,…)` window — chunk by date or you'll time out
    Extract list and segment memberships separately (segments aren't materialised; you re-derive them)
    Throttle to stay under the burst + steady-state limits; the cursor lets you resume
    You walk away with

    A re-runnable extract script + a reconciliation report against Klaviyo's own dashboard.

  4. Build + weekly demos

    3-5 weeks

    Three Flows ship in week one (welcome / abandoned cart / post-purchase). Templates become React Email components in your repo. SMS goes direct to Twilio. By end of phase your team is sending the weekly newsletter from the new product.

    Build the Flows engine on Inngest — every step a typed function with retries
    Templates as React Email / MJML, version-controlled, previewed in CI against Litmus
    Email sends via Amazon SES or Resend (fractions of a cent per send, no contact-tier multiplier)
    SMS via Twilio direct — no markup, you handle 10DLC / short-code registration once and own it
    Reporting in Metabase against the same Postgres — drill-downs agree with the chart
    You walk away with

    Three live Flows + weekly campaigns running from the new product on a staging domain.

  5. Parallel run + Shopify cutover

    1-2 weeks

    Shopify continues to fire webhooks into Klaviyo for a fortnight while the new product mirrors them. Daily reconciliation compares opens, clicks, conversions. Then we switch the webhook endpoints on a Tuesday morning.

    Dual-write Shopify webhooks via a Cloudflare worker (Klaviyo + new endpoint)
    Daily reconciliation: sends, opens, clicks, attributed revenue
    Move SMS sender identity (short code or 10DLC long code) — the carrier paperwork is the long pole, start week 1
    Suppress all but one Klaviyo Flow during the final week so we're not double-sending
    Cut Shopify webhook endpoints to the new product, leave Klaviyo on as read-only
    You walk away with

    A reconciliation dashboard showing seven days of zero drift + a signed sign-off from the head of marketing.

  6. Cutover + Klaviyo read-only

    1 day + 90-day fallback

    Klaviyo Flows get paused (not deleted — kept for compliance), Shopify webhooks point cleanly at the new endpoints, the SMS sender identity moves over, auto-renewal cancelled.

    Pause every Klaviyo Flow (don't delete — Klaviyo holds the unsubscribe records you may need)
    Confirm SMS sender identity has fully migrated to Twilio
    Verify Shopify webhook delivery to the new endpoints with synthetic events
    Lock Klaviyo to read-only / viewer role
    Cancel Klaviyo auto-renewal in writing
    You walk away with

    Cutover Runbook + a confirmed SMS sender migration + a quiet inbox where the renewal email used to land.

The Klaviyo APIs we'd actually hit

Concrete endpoints we use to extract your data during the parallel-run phase. We keep the extract scripts — they're useful at every renewal.

Get profiles
GET https://a.klaviyo.com/api/profiles/
Paginate every profile via the `page[cursor]` parameter, with the API revision header pinned
Get events
GET https://a.klaviyo.com/api/events/
Pull the activity stream — chunk by date window or you'll time out
Get metrics
GET https://a.klaviyo.com/api/metrics/
Inventory every event type (Placed Order, Viewed Product, etc.) so the new schema is exhaustive
Get lists
GET https://a.klaviyo.com/api/lists/
Migrate static list memberships (subscribers, VIPs, etc.)
Get segments
GET https://a.klaviyo.com/api/segments/
Read segment filter logic — segments are derived, not stored, so we re-derive them in SQL
Get list profiles
GET https://a.klaviyo.com/api/lists/{list_id}/profiles/
Pull the current membership of each list (needed for the initial backfill)
Get campaigns
GET https://a.klaviyo.com/api/campaigns/
Inventory campaign history for reporting parity
Get flows
GET https://a.klaviyo.com/api/flows/
List Flow metadata (the trigger logic is opaque — rebuild from the screenshots inventory)

The Klaviyo-specific gotchas

Things we've learnt the hard way (or that the documentation understates). We'll surface these on the kickoff call so nobody is surprised in week 6.

What stays where it is

Migrations work because they're scoped honestly. Here's what we don't move — by design.

  • ·Klaviyo's CDP product — most stores don't actually use it; we sync direct from Shopify / your warehouse
  • ·Reviews — handled by a dedicated tool (Stamped, Junip, Yotpo) if you need it, not bundled
  • ·Predictive Analytics models — replaced with explicit SQL definitions you can read
  • ·Mobile push and in-app messages — scoped separately if mobile is a real channel
  • ·Klaviyo's native A/B testing UI — replaced with feature flags on the send template
  • ·Klaviyo Forms templates — your popups become first-party React components on your own domain

Get a Klaviyo-shaped migration plan.

Tell us your seat count, contract end date, and the integrations you can't lose. We'll come back within a business day with a fixed-price scope and a timeline tuned to your renewal.

Scope a Klaviyo migration

One business day. NDA + DPA on request.