The Migration Nobody Warned You About
You are moving a B2B operation onto Shopify Plus. The catalog port goes fine. Customer accounts import cleanly. Themes look good. Then you start trying to bring over the invoice history, and the entire timeline you committed to collapses.
The pain comes from a specific gap: Shopify Plus has a B2B account schema, but the import tooling around it assumes you are starting fresh. Customers who have 18 months of invoice history, partial payments on open invoices, credit notes against past orders, and net-30/60/90 terms with running balances do not fit into the “create a new customer and let them place orders” model. You have to construct the history yourself, in the right schema, with the right relationships, and reconcile it against finance.
This is the single most common cause of B2B-on-Plus launch delays we see. It also lights up at the worst possible time: 2 to 3 weeks before go-live, when the finance team starts asking how the open AR is going to show up in the new system.
This post is the tactical playbook we run in audits and migrations. It assumes you have the legacy data in some form (ERP, QuickBooks, Sage, a custom system, an Excel export, anything) and you need to land it inside the Shopify B2B account schema before launch.
What “old invoices” actually means
Before mapping schemas, define the categories. In our audits, every “import the invoices” project has at least four of these data types:
Open Invoices
Invoices issued but not yet paid. The merchant has revenue recognized, AR on the books, and an expectation that the B2B customer will pay against this invoice in their new Shopify B2B account. The customer expects to see the invoice when they log in, expects the payment terms they had on the legacy system, and expects to be able to pay it through whatever flow the new portal supports.
This is the highest-stakes category. Missing one means missing revenue. Duplicating one means double-billing the customer and looking incompetent.
Paid Invoice History
Closed invoices going back some period (typically 12 to 36 months). Customers want to log in and see their history. Reordering convenience often comes from copying past invoices. Finance wants the history for audit and dispute resolution.
Lower urgency than open invoices, but if it disappears, customers will notice within a week of go-live.
Draft Orders and Pending Quotes
Quotes that have been issued but not converted to invoices. Some B2B sales motions live almost entirely in this state: a sales rep generates a quote, customer reviews, eventually places the order. If the quotes do not migrate, the sales rep’s pipeline disappears.
Credit Notes and Refunds
Adjustments against past invoices. Credit notes that have been issued but not applied. Pending refunds. These get forgotten and surface 30 days post-launch when finance closes the month.
Partial Payments and Remaining Balances
A subset of open invoices have been partially paid. The customer paid $5,000 against a $12,000 invoice. The remaining $7,000 still needs to be tracked. Plus’s native model does not handle this cleanly without custom work.
The customers who pay attention to invoice history are also the customers with the largest AR balances. The smallest accounts will not notice if history is broken. The biggest will email you the day they log in.— Every B2B Plus migration we have audited, 2024-2026
The Plus B2B Schema vs. Your Legacy System
Shopify Plus’s B2B account schema centers on three objects: Company, Company Location, and Customer. A Company has one or more Locations. Each Location has one or more associated Customers (the human buyers). Orders attach to a Location.
This is not how most legacy ERPs model B2B. Common mismatches:
| Legacy pattern | Maps to Plus | Migration cost |
|---|---|---|
| One account per business, one buyer per account | Company → Location → 1 Customer | Low |
| One account per business, multiple buyers, no location tracking | Company → Location → many Customers | Medium |
| One account per buying location of a business (multi-location chain) | Company → many Locations → many Customers | Medium-High |
| One account per individual buyer with no parent business concept | Company per customer (synthetic), or skip B2B | High |
| Account with a billing-vs-shipping split | Company with separate billing/shipping addresses | Medium |
| Account with custom catalog (price list) per account | Company → catalog assignment | Medium |
| Account with custom payment terms (net 30 for some, net 60 for others) | Company → payment terms | Medium |
| Account with credit limit / hold logic | No native mapping (build via metafield + checkout block) | High |
The schema mapping is the first thing we lock in any audit. Get the mapping right and the rest of the migration is mechanical. Get it wrong and the import scripts will keep almost working but never quite.
A specific trap: legacy systems often treat “a buyer at Company X” and “a buyer at Company Y who is the same human” as one customer. Plus treats them as separate Customers attached to separate Locations. If your data deduplicates buyers across companies, you have to undo that deduplication before import or you will end up with one Customer pointing at two Locations of two different Companies, which is not a valid Plus state.
The 5-phase Migration Playbook
This is the sequence we run. Each phase has a clear acceptance criterion. Skip a phase, and you find out about it 2 weeks before launch.

Phase 1: Extract and Inventory
Pull every data type from the legacy system into a portable format (CSV, JSON, SQL dump). For each invoice, capture: invoice number, customer reference, line items with SKU + quantity + price, issue date, due date, status (open/paid/credit), amount paid to date, remaining balance.
Inventory totals: how many Companies, how many Locations, how many Customers, how many open invoices, total open AR balance, oldest unpaid invoice, breakdown by payment-term type.
Acceptance: a single spreadsheet (or set of spreadsheets) where every row is mapped to a target schema field. If a field has no obvious target, flag it.
Phase 2: Schema Mapping and Synthetic-Account Decisions
Walk through the mapping table above. Decide where each legacy entity lands. The hard decisions:
- How are you handling Companies whose legacy data does not include a clear company name (e.g. customers tagged as “ACME Buying Group” with no actual company entity)?
- How are you handling Locations for businesses with one address but multiple buyers?
- How are you splitting price lists if a customer was previously on a custom-priced contract?
- How are you preserving payment terms (net-30/60/90) per Company?
Acceptance: a written mapping document that anyone on the team can read and execute against. We have used Notion, Coda, and Google Docs interchangeably; the format is less important than the completeness.
Phase 3: Staged Import to a Development Store
Import the data into a Plus development store first. Do not skip this. The first import will surface schema mismatches you did not catch in mapping, validation errors from the Plus admin, and edge cases (a Company with 50 Locations, a Customer attached to no Location, a price list that references SKUs you have not imported yet).
Order of import: 1. Companies 2. Locations under each Company 3. Customers (attached to Locations) 4. Price lists / catalogs (per Company) 5. Payment terms templates 6. Open invoices as draft orders (more on this in Phase 4) 7. Paid invoice history (as Order records with appropriate dates) 8. Credit notes (as Order edits or as metafields, depending on schema choice)
Use the Shopify Admin API (POST /admin/api/2025-04/draft_orders.json, POST /admin/api/2025-04/companies.json, etc.) over the bulk import UI for anything past 50 records. The UI is fine for spot-checks; the API is the only realistic option at scale.
Acceptance: the dev store has every Company, Location, Customer, and invoice landed. AR totals from the legacy system match within $5 of the dev store’s open AR.
Phase 4: Open-Invoice Handling Specifically
This is where most migrations stall. There is no native “open invoice” object in Plus. The standard pattern is to import each open invoice as a draft order with the customer’s reference number in the order name, the original issue date, the original line items, and the remaining balance as the order total.
Trade-offs of this approach:
- Pro: draft orders work natively in the Plus admin, customers can complete payment via a draft-order-checkout link, and finance can tag/filter by the original invoice number.
- Con: draft orders are not the same as invoices in every reporting view. Some Plus B2B portal templates do not show draft orders in the customer’s “Open invoices” list by default. Verify what your theme does before assuming this works.
For stores with very specific invoice formats (e.g. legal requirement to show original line items at original prices regardless of catalog changes), the draft-order approach can fail. Alternatives: custom metafield-based invoice tracking with a portal extension, or a sibling tool (an invoicing app) that integrates with Plus customers.
Plan one full sprint for open-invoice handling alone. Every audit we have run treats this as one task and discovers mid-sprint that it is actually three tasks: import the data, render it correctly in the portal, and confirm payment flows.
Phase 5: Reconciliation Period
Once the dev store imports cleanly, run the same imports against the production store within a controlled window (usually a weekend, ideally with the legacy system in read-only mode). Then immediately reconcile.
Reconciliation checks:
- Sum of open AR in legacy = sum of open invoice values in Plus draft orders + balance metafields.
- For 10 randomly sampled Companies: payment terms match, price list matches, last 5 invoices visible in the portal.
- For 5 randomly sampled Customers: log in to the portal as the customer, confirm the order history is correct.
- Finance signs off in writing: “open AR as of the cutover matches.”
Plan 5 to 10 business days of reconciliation. Real bugs always emerge in this window: a Company missed its price list assignment, a payment term defaulted wrong, a metafield was renamed in the API. Catching them in week 1 post-launch is normal. Catching them in month 3 is a finance audit nightmare.
The Most Common Breakage We Keep Seeing: The B2B ERP-Sync Loop
After enough migrations, the patterns repeat. The four most common failures:
Customers See the Wrong Prices on First Login
The customer logs in, navigates to a product they have been buying for 3 years, and sees a different price than expected. The Company’s price list was either not attached, attached to the wrong catalog, or the catalog itself has incorrect prices because the import script used the wrong field as the source.
The fix is a price-list-by-Company audit before launch. The cause is usually that “price” in the legacy system is a calculated field, not a stored value, and the import script grabbed the wrong base.
Open Invoices Show But Cannot Be Paid
The draft orders imported correctly, but the merchant did not configure the customer-facing payment flow for draft orders. The customer sees “you have an open invoice for $7,500” with no button to pay it. Customer emails support. Support cannot figure out how to take the payment because their playbook assumes a regular order.
The fix is to enable draft-order checkout link generation, train the support team, and ideally render the link directly in the portal under the open invoice list.
Payment Terms Are Wrong on New Orders
Companies migrate with their net-30/60/90 terms attached, but the terms are applied to the Company itself, not to a Customer or to specific catalogs. New orders placed by the customer might bypass the terms (e.g. fall back to standard checkout) if the catalog assignment is misconfigured.
The fix is end-to-end testing: log in as a test customer at a tagged Company, place a real test order, confirm the payment terms appear at checkout. Repeat for every Company tier.
Historical Orders Show in Random Order
Past orders import with their original created_at dates, but Shopify’s default sort in the customer portal is created_at DESC. If the import script set created_at to the import date (or NOW()), every customer sees all 18 months of history bunched in the same week. Customers complain that they “can’t find their last order.”
The fix is to backfill the original order dates, either via the Admin API’s created_at parameter on order creation (allowed via specific endpoints) or via a follow-up PUT to override.
ERP-Specific Gotchas (NetSuite, SAP, Microsoft Dynamics, QuickBooks)
The migration mechanics above are the same regardless of which legacy system you are coming from. The data-mapping pain, however, is specific to each ERP. Here are the patterns we have hit on the four systems we have migrated from most often.

NetSuite
The most common ERP for $20M+ B2B Shopify migrations, and the most painful to extract from.
- Customer hierarchy mismatch. NetSuite supports parent-child customer relationships natively (a parent account with child sub-customers for different locations or departments). Plus’s Company-Location-Customer model is more rigid. We usually flatten NetSuite’s hierarchy into Companies + Locations, and any sub-customer detail that does not fit the schema lands in Company metafields.
- Custom segments and class lookups. NetSuite’s price levels and customer segments often depend on classification fields that have no Plus equivalent. Plan a one-time mapping table that the import script consults.
- The “memorized transactions” trap. NetSuite has recurring invoices set up as memorized transactions. If you migrate the open invoice but not the underlying schedule, the next billing cycle does not generate an invoice. Audit every recurring B2B agreement separately.
SAP (Business One and S/4HANA)
- The IDoc export overhead. Most SAP-to-Plus migrations route through IDoc exports for orders and invoices. The IDoc format is complete but verbose; expect to spend a sprint on the parser before you can even start the schema mapping.
- Tax jurisdiction expansion. SAP stores tax at the jurisdiction code level (which is finer-grained than Plus expects). Decide upfront whether you collapse to country-level tax or maintain the jurisdiction granularity via tax-engine integration (Avalara, Vertex).
- Credit memo workflow gap. SAP’s credit memo lifecycle (issue, partial apply, fully apply, void) does not map cleanly into Plus’s order-edit + refund model. We have ended up modeling credit memos as a sibling metafield on the customer with their own state machine; the Plus order layer reflects the final applied amount, not the credit memo itself.
Microsoft Dynamics (365 Business Central and Dynamics GP)
- The IDoc export overhead. Most SAP-to-Plus migrations route through IDoc exports for orders and invoices. The IDoc format is complete but verbose; expect to spend a sprint on the parser before you can even start the schema mapping.
- Tax jurisdiction expansion. SAP stores tax at the jurisdiction code level (which is finer-grained than Plus expects). Decide upfront whether you collapse to country-level tax or maintain the jurisdiction granularity via tax-engine integration (Avalara, Vertex).
- Credit memo workflow gap. SAP’s credit memo lifecycle (issue, partial apply, fully apply, void) does not map cleanly into Plus’s order-edit + refund model. We have ended up modeling credit memos as a sibling metafield on the customer with their own state machine; the Plus order layer reflects the final applied amount, not the credit memo itself.
QuickBooks (Online and Desktop)
- The most common starting point, the loosest schema. Many sub-$20M B2B businesses are running QuickBooks for invoicing alongside their old commerce platform. The data quality is often the lowest of the four systems we see: missing customer references, ad-hoc product SKUs, free-text payment terms, manual journal entries that adjusted invoices outside the normal flow.
- The “two systems of truth” problem. Stores migrating to Plus B2B from QuickBooks usually have BOTH a commerce platform (old) and QuickBooks (financial ledger). The two have drifted. Reconciling them before the import is a separate project, often a 1 to 2-week pre-migration sprint that the team did not budget for.
- Class and location tagging. QuickBooks Class lists are the closest analogue to NetSuite’s segments. Same advice: build a mapping table, document any losses.
Universal advice across all four ERPs: the import script you write is throwaway code. It will run once, in a controlled window, and never again. Resist the temptation to make it elegant. Make it complete, well-logged, and re-runnable against subsets of data. Optimize for “we can re-run this for just the failed rows” over “this is well-architected long-lived software.”
What to Do This Week If You Are Mid-Migration
If you are reading this with a B2B Plus launch on the calendar, three immediate steps:
- Pull totals from the legacy system. Count of companies, count of open invoices, sum of open AR, breakdown of payment terms. Make sure these numbers are documented somewhere finance can sign off on.
- Stand up a dev store and do the first staged import with a sample of 5 to 10 Companies and 20 to 30 invoices. Find the schema bugs cheap, not expensive.
- Block a 5-business-day reconciliation window in the launch plan. If it is not there, the launch will slip into it anyway, but unplanned.
If you want a structured second opinion on the migration, the Shopify Teardown includes a B2B account audit specifically scoped to open AR migration, schema mapping, and a recommended phase plan. 30 calendar days, fixed price, $75,000/year recoverable revenue guarantee.
Frequently Asked Questions
Can I Use The Shopify CSV Import To Bulk-Load Invoices?
The standard product/customer CSV import does not support B2B-specific schema. For Companies, Locations, and draft orders at scale, you need the Admin API. The CSV path works only for the simplest cases.
What Is the Plus Rate Limit When Importing 1,000+ Records?
Plus is roughly 80 requests per second for the Admin API. For bulk imports, batch in groups of 100, throttle to stay under 60 req/sec for safety. A 5,000-row import takes 1 to 2 hours of clock time.
Can I Import a Credit Note That Is Older Than the Corresponding Order?
The order must exist first. If you are importing both historical orders and credit notes against them, sequence the imports: orders first (in chronological order), then credit notes referencing the order IDs.
Will the Customer’s Email Notifications Fire When We Import Their Open Invoices?
By default, draft order creation does not email the customer unless you explicitly trigger it. Import quietly. Then send a single consolidated “your new portal is live” email once.
How Long Does a Typical B2B-on-Plus Migration Take End-to-End?
For a store with 50 to 200 Companies and 500 to 2,000 historical invoices, plan 6 to 10 weeks from access to launch. Less than that is usually a sign someone is skipping the reconciliation phase.
What Happens to Legacy Invoice PDFs?
They do not migrate to Plus natively. Most merchants keep them in object storage (S3, Drive, the legacy system) and reference them from a metafield on the Plus order. We have also seen teams attach them to draft orders as notes or to Customer metafields. There is no universally clean answer.
Is there an app that does all this for us?
We have not found one that handles the full open-invoice + payment-terms + price-list + history case. Apps cover slices (a “data importer” app, an “invoicing” app), but the end-to-end remains custom work. Plan for an engineering project, not an app install.



