Migrating to Super Speedy Emails (from MailPoet, MailerLite, or WooCommerce)
This guide walks you through moving your existing subscribers and customers into Super Speedy Emails (SSE), then verifying everything is correct before you send a single email. The migration is run via WP-CLI and is designed to be re-runnable — you can dry-run as many times as you like before committing changes.
Three things get migrated, each independently:
- WooCommerce customers → the
sse_product_ownersownership cache. So SSE knows which subscribers own which products, even for orders placed long before the plugin was installed. - MailPoet subscribers + lists + unsubscribes → SSE subscribers + lists + suppressions.
- MailerLite subscribers + groups + unsubscribes → SSE subscribers + lists + suppressions, via a CSV export.
You can do any subset, and you can do them in any order. They don’t interfere with each other.
Table of Contents
- Why dry-run first?
- Step 1 — Back up your database
- Step 2 — Import existing WooCommerce customers
- Step 3 — Migrate MailPoet
- Step 4 — Migrate MailerLite
- Step 5 — Sanity-check report
- Step 6 — Manually verify the unsubscribers are respected
- Step 7 — Stop sending from the old provider
- Re-running migrations
- What does NOT get migrated
- Troubleshooting
- In a hurry — the absolute minimum
Why dry-run first?
Every importer ships with a --dry-run mode that writes nothing — it only counts what would happen. Treat dry-run as the rehearsal: read the output, sanity-check the totals, then add --commit to apply.
⚠️ The default mode is
--dry-run. You must pass--commitexplicitly to write changes.
Step 1 — Back up your database
Standard advice but worth saying. Before any commit run:
wp db export ~/sse-pre-migration-$(date +%Y%m%d).sql
If a migration produces a result you didn’t expect, restore from this dump and re-do the dry-run with different inputs.
Step 2 — Import existing WooCommerce customers
This populates SSE’s product-ownership cache from every paid order in your store. New orders are picked up automatically by a hook, but existing orders need a one-off backfill.
# Dry-run — see how many orders would be processed:
wp sse migrate wc-customers
# Commit — actually populate the cache. Can take a minute or two on a big store.
wp sse migrate wc-customers --commit
# Or just the last year's orders, if you don't want to backfill years of history:
wp sse migrate wc-customers --days=365 --commit
What this does, plainly:
- Walks every WooCommerce order with status
completedorprocessing. - For each order, ensures there is an SSE subscriber row matching the billing email (creates one if not).
- For each line item, writes a row into
sse_product_ownersso segments like “owners of Scalability Pro” find this customer instantly. - Bundle expansion is automatic: a Super Speedy Pack buyer gets
direct=1for the Pack anddirect=0rows for each component. So “owners of Scalability Pro” includes Pack buyers, but “direct buyers of Scalability Pro” does not.
Important: Subscribers created from this backfill have source = wc_purchase and marketing_opt_in = 0. They are not on any marketing list. They can receive product_support, onboarding, and transactional emails (per the legal-basis matrix), but they will never receive newsletter or marketing blasts unless they explicitly sign up for a list.
This is the “no silent marketing opt-in” rule and it’s deliberate.
Step 3 — Migrate MailPoet
If MailPoet is installed on this WordPress install, the importer reads directly from MailPoet’s database tables. No exports required.
# Dry-run:
wp sse migrate mailpoet
# Commit:
wp sse migrate mailpoet --commit
What the dry-run output tells you
You’ll see something like:
MailPoet migration — DRY RUN
Lists:
matched existing 0
new lists 4 (would do)
Subscribers:
new 2843 (would do)
updated 0 (would do)
Breakdown by status:
active 2204
pending 12
unsubscribed 589
bounced 38
Suppression rows: 627 (would do)
List memberships: 2204 (would do)
Success: dry-run complete (no writes)
Reading this:
- 2204 active subscribers will be imported as marketing-opted-in members of your lists.
- 12 unconfirmed will be imported as
pending(no list memberships, no welcome resend — pinging stale unconfirmed addresses is a deliverability footgun). - 589 unsubscribed + 38 bounced = 627 suppression rows. These people will not receive marketing email from SSE. (The unsubscribed get a
marketingscope suppression; the bounced get aglobalscope suppression.) Their subscriber rows are still created — they just can’t be marketing-mailed. - 2204 list memberships — every active subscriber’s MailPoet segment membership is preserved.
If those numbers look right, run with --commit.
MailPoet status mapping
| MailPoet status | SSE outcome |
|---|---|
subscribed | Active SSE subscriber, marketing-opted-in, list memberships set |
unconfirmed | SSE subscriber as pending, marketing-opt-in off, no list memberships |
unsubscribed | SSE subscriber created, marketing-opt-in off, marketing-scope suppression added |
bounced | SSE subscriber created, marketing-opt-in off, global-scope suppression added |
inactive (MailPoet’s stale-engagement bucket) | Treated as unsubscribed — marketing-scope suppression |
| Soft-deleted (deleted_at set) | Skipped entirely |
Segments → SSE lists
Each MailPoet segment of type default becomes an SSE list with slug mailpoet-<id>-<segment-name> and category list_broadcast. Auto-segments (e.g. “WooCommerce customers” of type wc_users) are filtered out — those aren’t real opt-in lists.
If a list with the matching slug already exists, the importer reuses it instead of creating a duplicate.
Step 4 — Migrate MailerLite
MailerLite is an external service, so the importer reads from a CSV export rather than a database table.
Export from MailerLite
- Log into MailerLite.
- Go to Subscribers.
- Click Export (top-right). Choose All subscribers or a specific group/status. The “All subscribers” export is fine — the importer respects each row’s status field.
- Pick CSV format. MailerLite emails you a download link or starts the download.
- Save the file somewhere on the WordPress server (e.g.
/tmp/mailerlite.csv).
The importer accepts these column names (case-insensitive):
email(required)first_name/first name/fname/namelast_name/last name/lnamestatus(active/unsubscribed/bounced/unconfirmed/junk)groups/group/lists/list— comma- or semicolon-separated list names
The standard MailerLite export includes these.
Run the import
# Dry-run:
wp sse migrate mailerlite --file=/tmp/mailerlite.csv
# Commit:
wp sse migrate mailerlite --file=/tmp/mailerlite.csv --commit
MailerLite status mapping
| MailerLite status | SSE outcome |
|---|---|
active | Active SSE subscriber, marketing-opted-in, group memberships → list memberships |
unconfirmed | SSE pending, no list memberships |
unsubscribed | Marketing-scope suppression |
bounced | Global-scope suppression |
junk (MailerLite’s complaint bucket) | Global-scope suppression, reason=complaint |
Groups → SSE lists
Each unique group name in the CSV becomes (or reuses) an SSE list with slug mailerlite-<sanitised-name> and category list_broadcast.
Step 5 — Sanity-check report
After running any combination of the above with --commit, run the sanity report:
wp sse migrate report
You’ll see:
== Subscribers ==
total 4127
marketing-opted-in 2531
pending (no consent) 12
from MailPoet 2843
from MailerLite 811
from WC purchase 473
== Suppressions ==
total 1184
marketing-scope 1104
global-scope (hard) 80
imported (migrated) 1104
== Lists & WC ownership ==
lists 8
product owners (subs) 473
ownership rows total 812
WC subs with no list 473 (product-only audience — expected, can still receive product_support/onboarding/transactional)
This is your final pre-launch checklist. Things to look at:
- Total vs. marketing-opted-in. Big gap = lots of suppressed-but-imported subscribers. That’s correct if you had many unsubscribes — bad if it doesn’t match what your old provider reported.
- Suppression total. This should roughly match the count of “unsubscribed + bounced” people across all your old providers. If this number is suspiciously low, your unsubscribes were not fully imported — STOP, investigate, do not send.
- WC subs with no list. These are customers backfilled from WooCommerce who never signed up to a marketing list. They will not receive your newsletter. They can be reached for product support, onboarding, and transactional. This is the legally cleanest default.
Step 6 — Manually verify the unsubscribers are respected
Sanity-checking that unsubscribes were honoured is the single most important check before sending.
Verification A — Pick a known unsubscriber
If you can think of a specific person who unsubscribed in MailPoet/MailerLite, look them up:
# Check the subscriber row exists:
wp db query "SELECT id, email, status, marketing_opt_in, source FROM wp_sse_subscribers WHERE email = 'someone@example.com'"
# Check they have a suppression:
wp db query "SELECT email, scope, reason, source FROM wp_sse_suppressions WHERE email = 'someone@example.com'"
Expected: subscriber row present, marketing_opt_in = 0. Suppression row present, scope = marketing (for an unsubscribe) or scope = global (for a hard bounce or complaint).
Verification B — Spot-check a handful in the admin
- Go to SSE Emails → Suppressions in the WP admin.
- You should see the marketing-scope rows from MailPoet/MailerLite listed with reason =
migrated. - Search for a few specific email addresses you know unsubscribed in your old provider — they should appear here.
Verification C — Dry-run a campaign and check the count
Compose a broadcast campaign in the admin against a list you migrated. Before sending, the audience-resolver shows the recipient count. That count should exclude suppressed addresses automatically — the gatekeeper handles this without you needing to remember.
If the count is suspiciously high (close to the total subscriber count), the suppression mask probably isn’t being applied. Stop and check.
💡 You can always run a campaign in test mode first. Set
wp option update sse_test_mode log_only— no real emails go out, but log rows are written so you can audit who would have been sent to.
Step 7 — Stop sending from the old provider
Once everything looks right:
- Disable sending in MailPoet (don’t uninstall yet — keep the data around as backup).
- Stop scheduling in MailerLite (or downgrade to a free plan if you’re paying).
- Send your first SSE campaign to a small list as a final smoke test.
- After a couple of weeks of confidence, uninstall MailPoet.
Re-running migrations
Every importer is idempotent. Re-running on the same source produces no duplicates:
- Subscribers are matched by email; existing rows are updated, not duplicated.
- Lists are matched by slug; existing lists are reused.
- Suppressions are uniquely keyed on
(email, scope); re-adding a row is a no-op.
This means it is safe to re-run a migration if:
- You realised mid-migration that something was wrong and want to start fresh after fixing it.
- You exported MailerLite again with more data and want to re-import.
- You’re not sure whether a previous run completed; just run again with
--commitand check the sanity report.
What does NOT get migrated
- Sending history. SSE doesn’t import “this person was sent this email in 2023”. The audit trail starts fresh.
- Open/click tracking history. Same reason.
- Templates from MailPoet/MailerLite. SSE is plain-text-first; you’ll re-author any newsletter templates in SSE’s composer.
- Sender domains / authentication. Set up your sending domain (MailGun) directly. SSE doesn’t touch DNS.
Troubleshooting
MailPoet tables not found in this install Either MailPoet is no longer installed, or it lives in a different table prefix. If your install has a non-standard prefix, set:
wp option update sse_migrate_mailpoet_prefix wp_yourprefix_
and re-run.
CSV file not readable Path is wrong, or the WP user can’t read it. Use an absolute path; check ls -la shows readable permissions.
Dry-run says 0 subscribers For MailPoet: the tables exist but are empty. For MailerLite: the CSV header didn’t match any of the expected column aliases. Open the CSV in a text editor, check the first row, and rename headers if needed (e.g. change Email Address to email).
“Updated” counts are non-zero on the first dry-run That means subscriber rows with those email addresses already exist in SSE. This is expected if you’ve also run the WooCommerce backfill — a WC customer who’s also on a MailPoet list will have one row created by the WC backfill, then “updated” by the MailPoet import to set marketing_opt_in=1 and source. This is the correct behaviour.
In a hurry — the absolute minimum
# Back up:
wp db export ~/sse-pre-migration.sql
# Backfill WC customers (no consent risk; just populates the ownership cache):
wp sse migrate wc-customers --commit
# Migrate MailPoet (dry-run, sanity-check the numbers, then commit):
wp sse migrate mailpoet
wp sse migrate mailpoet --commit
# Migrate MailerLite from a CSV export:
wp sse migrate mailerlite --file=/tmp/mailerlite.csv
wp sse migrate mailerlite --file=/tmp/mailerlite.csv --commit
# Final check:
wp sse migrate report
If the report numbers match expectations, you’re done — disable sending in your old provider and start using SSE.