Introduction
This documentation describes the process for migrating a large site from Substack to WordPress and WooCommerce for one of our partners. The Substack site we migrated had around 113K subscribers, free and paid, at the time of migration. Special care was taken to preserve users’ payment details and subscription levels so that the migration would be seamless from the subscriber’s perspective.
The partner sends out a weekly newsletter that contains links back to their WordPress site, so the goal of the migration was to consolidate the newsletter with the site into a single platform, as well as take advantage of the deeper customization options available within WooCommerce.
Planning the migration
Before bringing the Substack subscribers into the new site, it was very important to have a place for them to go once migrated.
Recreate subscriber tiers in WooCommerce
The first step was to re-create the subscriber tiers from Substack within WooCommerce to replicate the partner’s offerings:
- Free: Access to the free weekly newsletter, along with free articles on the WordPress site. Premium content on the site is hidden behind a paywall.
- Paid Monthly or Yearly Subscribers: The primary paid tier, with options to pay either monthly or yearly. These users would get access to the free weekly newsletter and premium content on the site.
- Founding Members: The highest paid subscriber level (need to look up benefits)
We used WooCommerce Subscriptions to create the subscription tiers, and WooCommerce Memberships to link each subscription tier to a membership access level, which would determine whether or not a user could see premium content.
In addition to the digital memberships, users still have the option to purchase a subscription to the partner’s print publication as well.
Set up member-only content in the WordPress site
WooCommerce Memberships powers the pay gate used across the site. The extension provides a member-only content block that the partner can place within posts to designate premium content. We designed and developed a custom pattern to make the pay gate’s appearance match the aesthetic of the site.
Set up mailing lists and automation
Our next step was to create the mailing lists to which the subscribers would be added to receive the weekly newsletter. These lists would be where the Substack subscribers would be automatically added to once imported, and which lists new users who purchase subscriptions directly from Substack would be added to in the future.
- Newsletter Mailing List: This is the free mailing list where all subscribers, regardless of their subscription type, would be added. This is the “master” list for sending out the weekly newsletter.
- Paid Users: This list contains just the users who have purchased any subscription. The focus of this list is to allow our partner to send emails just to paid subscribers.
- Founding Members: This list is only for Founding Members, with the purpose of having a way to reach just those users at this membership level.
We used MailPoet to set up these lists and created several workflows within the AutomateWoo extension to automatically add subscribers to the respective lists upon purchasing a subscription and remove them from a list upon subscription deactivation. These automations also ran during the migration process to ensure subscribers ended up on the right lists. The automations we set up were:
- Add Free Subscriber to Mailing List:
- Trigger: user signs up for the free newsletter via a sign-up form on the site.
- Action: user is added to the free mailing list upon confirming their subscription
- Remove Free Subscriber from Mailing List:
- Trigger: user unsubscribes from the free mailing list
- Action: the user is removed from the free mailing list
- Add Paid Subscriber to Mailing Lists:
- Trigger: user purchases a premium subscription
- Action: the user is added to the free and paid mailing lists upon confirming their subscription
- Remove Paid Subscriber from Mailing Lists:
- Trigger: user unsubscribes from the free or paid mailing list or their subscription is no longer active (for whatever reason)
- Action: the user is removed from the free and paid lists
Mailing List Cleanup
Before migrating a large mailing list, it’s important to review and clean the list to ensure strong email deliverability and compliance. In our case, the partner assisted with reviewing the subscriber list in advance on Substack to help remove outdated or invalid addresses and reduce the risk of deliverability issues post-migration. We also had conversations around how to handle double opt-in, since users had already explicitly subscribed via Substack and expected to continue receiving emails without interruption. These types of hygiene and consent considerations are essential in any migration to avoid spam traps, maintain sender reputation, and ensure that subscribers are properly onboarded into the new platform.
Performing the migration
Substack and Stripe overview
Prior to the migration, we also needed to understand how subscriptions are set up and managed on Substack. Substack uses Stripe Subscriptions to manage paid memberships. Our task was to:
- Export product data from Stripe.
- Map Stripe products to WooCommerce products on a staging site (which will eventually become your production site).
- Import subscriptions into WooCommerce Subscriptions.
- Assign the imported users the correct Membership level in WooCommerce Memberships based on what type of subscription they have.
- Cancel original Stripe subscriptions to avoid double-billing.
To facilitate the creation of the product mapping CSV, follow these steps:
- Log in to Stripe, and then go to Stripe Product Catalog → Export prices to generate an export, selecting the items that are relevant for your export.
- Next, generate an export of products and their prices from your WooCommerce staging site, also as a CSV file.
- Load the exported Stripe CSV into a spreadsheet, and add the Product Name and Product ID columns. Then add the exported WooCommerce product, mapping Stripe Plan IDs to WooCommerce Product IDs.
- Export this spreadsheet as a CSV.
We used:
- WooCommerce Subscriptions Importer and Exporter plugin
- An internal, custom-built command-line tool used to streamline the subscription migration process. It generates properly formatted CSV files from exported data (e.g., from Stripe or Piano), which are compatible with the WooCommerce Subscriptions Importer and Exporter plugin linked above. It also includes commands to cancel legacy Stripe subscriptions post-migration.
We recommend reviewing WooCommerce’s documentation on Migrating Subscribers to WooCommerce Subscriptions for guidance and to explore options for what would work best for your migration needs, including the possibility of hiring a WooExpert if your migration requires custom scripting.
The Migration Process
The migration of users from Substack to WordPress took place in two main phases. One benefit of migrating smaller batches of users in your initial migration is that it makes it easier to catch and address any oversights or unforeseen issues before running your larger batches.
Phase 1: Founding Members (228 users)
We received a list of Founding Member emails from Substack and:
- Downloaded the Stripe Customers and Subscriptions exports.
- Used spreadsheets + VLOOKUP to filter just the Founding Members’ data.
- Mapped the corresponding Stripe products to WooCommerce products in a product mapping CSV.
- Ran our custom command-line tool to generate the import file. Here’s an example of the command from the tool:
wp newspack import_csv stripe \
--input-file=/tmp/stripe-subscriptions.csv \
--stripe-customers-file=/tmp/stripe-customers.csv \
--product-mapping-file=/tmp/stripe-product-mapping.csv
- Imported the file via the WooCommerce Subscriptions Importer.
- Used the following command from our custom tool to cancel the original Stripe subscriptions:
wp newspack stripe cancel-subscriptions-from-csv \
--subscriptions-ids-csv=/tmp/subscription-ids.csv
One benefit of migrating a smaller number of users in your initial migration is that it makes it easier to catch and address any oversights before running your larger migration batches.
Total time: ~45 minutes.
Phase 2: Paid Users (8,256 users)
We repeated the above process for all remaining paid users:
- Exported Stripe customer + subscription data.
- Ran the same CLI workflow to generate the import file.
- Validated the import in batches of 1, 5, 10, 50, 100, then all users to ensure integrity.
- Imported subscriptions and ran cancellation commands in Stripe.
- Ensured that users were assigned the correct membership levels in WooCommerce Memberships.
Total time: ~10 hours (including validation and post-processing). This phase used the membership import setting correctly, so no follow-up script was needed.
Migration cleanup and edge cases
During our post-migration checks, we identified and addressed several edge cases:
- New Subscribers Mid-Migration: A few new signups occurred during the import window. We ran a follow-up import to add them.
- Cancelled Subscriptions: Some users had cancelled via Substack but not Stripe. Stripe lacked cancellation metadata, so these users were charged again. We cross-referenced Substack’s export (Cancel Date column) and created a custom script to mark those subscriptions as pending cancellation.
- Comped Users: Users who were comped a free subscription by the partner on Substack (“comped”) didn’t appear in Stripe and were missed in our main import. We added them using a custom script.
- Gifted Subscriptions: Gifted subscriptions didn’t show up in Stripe’s active subscriptions but could be found via invoice records. We:
- Searched Stripe for gift invoices using an appropriate search term (“partner name gift”)
- Manually compiled a list of active gift recipients.
- Wrote a custom import script to add these users to the WordPress site
Mailing List + Delivery Optimization
In addition, MailPoet offers a number of ways to optimize sending performance. Some suggestions include:
- Utilizing MailPoet’s sending service for the best sending performance.
- Documentation on improving sending speed when using a third-party sending service
Lessons Learned
Substack Imports
- We used a robust set of tools in our migration process (our internal command-line tool alongside the WooCommerce Subscription Importer and Exporter extension. However, given the size of the migration — 1000+ subscriptions — the import process can take several hours to complete, so it is best to plan accordingly.
- The Cancel Date column in Substack’s export is not reflected in Stripe Subscriptions. As a result, these canceled subscriptions should be manually marked with a wc-pending-cancellation status in the main import CSV prior to import.
- Gift subscriptions from Substack are not represented in Stripe Subscriptions either. These must be manually identified—either by reviewing the Substack export or searching Stripe invoices for “subscription gift”—and then imported manually or with a custom script.
- Likewise, comped users on Substack (those with free subscriptions) are not present in Stripe data. These users also need to be imported separately, using either manual methods or a custom import script.
WooCommerce and Memberships
- To ensure users gain instant access to digital products, those products must be marked as both “virtual” and “downloadable.” We encountered an issue where over 200 paid users could not access their purchased content because the digital product was marked only as “virtual,” causing orders to remain in processing status and requiring manual admin intervention. Marking it as “downloadable” ensures the order is auto-completed after purchase.
- It’s also important to tailor transactional emails based on the type of product. For example, digital products should not include language about shipping or delivery.
- Lastly, make sure to remove the Member-Only content block from any post that doesn’t contain restricted content. Even an empty block will incorrectly activate the paygate, confusing users by suggesting the content is unavailable.
MailPoet
- When importing users into MailPoet, their status defaults to “Unconfirmed” unless explicitly set to “Subscribed.” While the default behavior is appropriate when you want users to manually opt in, in this case, all users had already opted in via Substack and were expecting to receive the weekly newsletter. To ensure deliverability, we re-imported these users using the MailPoet subscriber import tool and set their status to “Subscribed” via the CSV.
- Mail Tester proved to be a valuable tool for identifying email formatting or deliverability issues before the emails were sent.
