Migration guide

Import wholesale customers to Shopify

Replatforming with four hundred trade accounts behind you is not a registration problem — it is a moving problem. This guide covers how B2B Onboard imports an existing wholesale base into Shopify as real B2B companies, contacts and locations: the exact CSV columns, the dry-run preview, the registry check that runs on every tax ID, and the things a migration deliberately leaves behind.

It works on every paid Shopify plan — B2B company creation is not a Plus-only capability. Jump to plan availability for the one Plus-only caveat, stated honestly.

Why wholesale accounts are the hard part of a B2B migration

Products and retail customers have a well-worn path onto Shopify. Wholesale accounts do not. Shopify has no native CSV import for companies — the built-in customer importer creates customers, not B2B companies, company locations, or company contacts. So the entity your trade business actually runs on is the one entity you cannot upload.

Shopify’s own help centre points at three routes, and they are the three routes most merchants end up choosing between:

All three move rows. None of them checks a tax ID against an official registry, and none of them decides how tax should be treated for the company that comes out the other end. That work lands on you afterwards, account by account, at exactly the moment you are least able to spend a week on it. B2B Onboard imports the same list and does that checking as part of the run — and records what it found, so the decision you make later has something behind it.

How accounts come across

Three steps, in Companies → Import companies. Nothing is created until you say so.

1

Upload

Download the CSV template, fill in your companies, contacts and locations — up to 5,000 rows and 20 MB per file — and upload it. The template’s column order is the order in the table below.

2

Preview

A dry run reports total rows, how many are valid, and the distinct companies, contacts and locations it found — plus a row-by-row list of what needs attention: missing required fields, malformed email addresses, unrecognised countries, phone numbers Shopify will reject, duplicate contact emails inside the same file. Nothing is created at this step, so you fix and re-upload until the list is clean.

3

Run, then download the results

Real B2B companies, contacts and locations are created for every valid row — the same write path as approving an application. When it finishes, download the results file: one line per row with its outcome, any error, the registry check and the reference behind it. Rows that failed come back in a second file in their original columns, so you fix them in place and upload the same file again.

Uploaded the same file twice by mistake? A file that has already been imported is recognised before anything is created — you can open the earlier run’s report instead, or deliberately choose to run it again. Imported records stay visible in the applications list under the Imported filter, badged on every row, and stay out of your dashboard metrics and weekly digest so a migration never flatters your approval rate. See the applications guide for how the Imported badge and filter behave.

Every column the template accepts

Thirty columns, in this order. Four are required — a row missing any of them is reported as invalid in the preview and is not created.

Column Required Notes
company_name Required The company as you want it to appear in Shopify. Rows repeating a company — the same external_id, the same tax_id, or the same name spelled the same way — are grouped into one company with several contacts, and the preview counts them as one company.
contact_first_name Required Given name of the person who will order for this company.
contact_last_name Required Family name of the same person.
contact_email Required Must be a well-formed address. The same address twice inside one file is flagged in the preview before anything is created.
contact_phone Optional International format (for example +4520123456) travels best. Numbers Shopify will not accept are reported in the preview rather than failing mid-run.
address1 Optional Street and number for the company location.
address2 Optional Suite, floor, department or care-of line.
city Optional Town or city of the location.
province Optional State, province or region. Accepts the code or the full name. Countries that Shopify requires a province for (the US, Canada, Australia and others) are flagged in the preview when it is missing or unrecognised.
country Optional ISO 3166-1 alpha-2 code (DK, DE, GB) or the full country name. Everyday aliases such as “USA” and “UK” resolve. Anything that does not resolve is reported rather than guessed.
zip Optional Postal or ZIP code for the location.
tax_id Optional VAT number, ABN, NZBN or organisation number. Needs country filled in on the same row, because the country decides which registry the number is checked against. It is also the second key rows are grouped by (after external_id): spacing, hyphens, dots and capitalisation are ignored, and within one file the same number must always name the same company — if it names two, the preview reports it instead of merging them.
external_id Optional The customer’s account number on the platform you are migrating from. It is stored as the company’s external reference in Shopify, and it becomes the first key the importer matches on — ahead of tax number, email domain and company name. Re-upload a file carrying the same external_id and the matching company is updated or skipped (your choice at upload) instead of duplicated. No spaces, and the same value must always name the same company within one file.
location_external_id Optional Your own reference for this particular location, stored as the location’s external reference in Shopify. On a re-upload it decides which location of the matched company the row updates. No spaces, and each value may appear on only one row per file.
payment_terms Optional The payment terms this customer already has with you, written as the name of one of your store’s payment terms templates (for example Net 30). Matched against your own store’s templates, ignoring capitalisation. A name that does not exist on your store is reported in the preview — listing the names that are available — so nothing lands on the wrong terms. Leave it empty to keep whatever the location would get by default.
order_review_required Optional Whether this customer’s orders go to you as a draft for review before payment, instead of straight through checkout. Accepts yes, no, true, false, 1 or 0; anything else is reported in the preview rather than guessed. Empty leaves the Shopify default untouched.
editable_shipping_address Optional Whether the buyer may change the shipping address at checkout, or is held to the addresses on file. Same accepted values as order_review_required. Empty leaves the Shopify default untouched.
deposit_percentage Optional Deposit taken at checkout, as a percentage of the order — a number above 0 and up to 100 (decimals allowed). Deposits are a Shopify Plus feature: on any other plan the row still imports and the results file records the deposit as not_applied (plan) so you can see exactly which companies to revisit. Empty means no deposit.
billing_address1 Optional Street and number of a separate billing address — for the customers who want invoices at head office and deliveries at a depot. This column anchors the whole billing group: fill it and the created location gets a billing address distinct from its shipping address; leave the entire group empty and billing stays the same as shipping, exactly as before. Because billing is the address that differs, address1 must be filled on the same row, and filling any other billing_* column without this one is reported in the preview rather than imported as half an address.
billing_address2 Optional Suite, floor, department or care-of line of the billing address.
billing_city Optional Town or city of the billing address.
billing_province Optional State, province or region of the billing address. Same rules as province: the code or the full name, checked against billing_country and reported in the preview when it does not resolve.
billing_country Optional ISO code or full country name for the billing address, resolved exactly like country. It is used for the address only: tax numbers stay keyed to the row’s main country, so a billing address in another country never changes which registry tax_id is checked against.
billing_zip Optional Postal or ZIP code of the billing address.
catalogs Optional Names of catalogs that already exist in your store, separated by semicolons — the same convention other importers use (for example Trade;Gold). Names are matched against your existing company‑location catalogs, ignoring capitalisation; a name that does not match is reported in the preview together with the catalog names your store does have, so you can correct the file before importing. Nothing is created here: catalogs, price lists and product prices are set up in Shopify, this column only says which of them a new location joins. On Shopify Plus the location created by the row is attached to each named catalog directly. On other plans catalogs reach company locations through a B2B market instead, so the row still imports and the results file records the value as not_applied (assign via B2B market) — assign those locations to a B2B market afterwards. You can have that happen automatically: set a Default B2B market under Settings → B2B Company and every location the import creates is added to it during approval, with the results file’s market column naming the market for each row. Catalogs that belong to a market (for example a country catalog) cannot be named here — they reach buyers automatically through their market, and the preview says so if you name one. One thing to know: when a location has its own catalogs, those take precedence for that buyer — market catalogs then no longer apply to them, they do not combine.
contact_role Optional Which of the two B2B roles this row’s contact gets at the company location: Ordering only (can place orders for the location) or Location admin (can also manage the location’s other contacts). Capitalisation does not matter; anything else is reported in the preview. Leave it empty to use your default contact role from Settings. Roles are per‑location permissions, so when several rows share a company each contact keeps its own role.
note Optional Your internal note about this account, written to the company’s note field in Shopify — the delivery instructions, the account history, whatever your old system kept. Up to 5,000 characters. The note belongs to the company, so when several rows share one company the note is taken from the first of those rows; a later row with a different note is pointed out in the preview and the first row’s note is the one that is written. Nothing is ever added to a company that already exists.
tags Optional Customer tags for this row’s contact, separated by commas (for example wholesale,north-region). They are added on top of the approval tags you configured in Settings, never instead of them, and duplicates are ignored. Tags are per‑contact, so each row of a shared company keeps its own. Up to 20 tags per row. A tag is a label you can segment and filter on — never a marketing subscription; see Marketing consent below.
credit_limit Optional The credit limit this account had on your old platform, as a number. Shopify B2B has no credit-limit field, so it is stored on the company for your reference as a metafield — namespace b2b_onboard, key source_credit_limit — and it is not enforced: nothing in B2B Onboard or in checkout blocks an order because of it. A value that is not a number is reported in the preview rather than stored as-is. Company‑level, like note: taken from the first row of a shared company.
customer_group_code Optional The customer group, price group or segment code this account carried on your old platform. Stored on the company for your reference in the same way — namespace b2b_onboard, key customer_group_code — so you can see what an account used to be entitled to while you rebuild those tiers as Shopify catalogs. It drives no pricing by itself. Company‑level: taken from the first row of a shared company.

File limits: 5,000 rows and 20 MB per file. A larger list splits cleanly across several files — each run gets its own preview and its own results file.

This table reflects today’s template, and it will grow: further columns are on the roadmap, and this page is updated as each one ships. Columns not listed here are ignored on upload, so an export from your old platform can keep its extra fields — only the thirty above are read.

Purchase-order numbers are deliberately not an import column. Requiring a PO number at checkout is a store-level Shopify checkout setting rather than something held per company location, so there is nothing per-customer for the file to carry — set it once in Shopify and it applies to every B2B customer.

Re-uploading a file

Migrations rarely land in one pass. When rows carry an external_id, a second upload of the corrected file recognises the companies it already created, so you choose — before the run starts — what should happen to them:

The results file records the outcome of every row — created, updated, skipped, matched, held back or failed — alongside a matched_by column naming which key claimed it (external_id, tax ID, email domain or company name), so you can see exactly why a row landed where it did.

The same file also carries a payment_terms column, naming the terms that were applied to each newly created location, and a deposit column reading applied or not_applied (plan) — so a store that is not on Shopify Plus gets a precise list of the companies whose deposit still needs setting by hand.

Every tax ID checked against the registry, recorded as evidence

A tax ID that was typed into your old system five years ago has never been checked by anybody. During an import, each row’s tax ID is checked against the official registry for that company’s country:

The result and the reference behind it are recorded as evidence on the row and on the company it created, so months later you can see what was checked, when, and against what. Companies whose number qualifies receive the matching tax treatment as part of the run.

Where a registry cannot confirm a number, the row still imports — marked unavailable, flagged for you, and with tax still collected. Nothing is quietly exempted on a number nobody could confirm; collecting tax is the safe default, and the flag tells you which rows to look at. A registry that was simply unreachable at the time is re-checked automatically in the background afterwards, so an outage costs you nothing and you do not re-upload for it. A number that is genuinely wrong you correct and re-import, or re-check from the company later.

This is decision support, not tax advice. The app checks, records and flags; the decision on any individual account stays with you and your accountant.

Login invitations are a separate, explicit step

Imported customers do not receive login instructions automatically. Sending them is a single checkbox at upload time, and it is off by default. Move the data quietly this week, check a sample of accounts, brief your sales team — and send the welcome email when you are ready for the replies. Nothing about the import reaches your customers until you decide it should.

What we deliberately don’t import, and why

The import creates companies, company contacts and company locations. Everything below is left alone on purpose — each for a reason worth knowing before you plan the cutover.

Orders and order history

An order carries payment, tax and fulfilment records from the system that processed it. Recreating those in Shopify produces documents that look real and reconcile against nothing. Keep the old system as a read-only archive, or move historical orders with a dedicated order-migration tool.

Account balances and open invoices

Balances belong to your accounting or ERP system, which remains the book of record. A balance written into a storefront is a number nothing reconciles against — and the first payment applied against it makes both systems wrong.

Products and inventory

Your catalogue is a separate migration with its own mapping, media and variant decisions, and Shopify already has good tooling for it. Import products first, then bring the accounts across on top of them.

Price lists and negotiated pricing

B2B pricing lives in Shopify’s own catalogue and price-list model, not in a customer file. Set your price lists up in Shopify and attach them to companies there — the import does not touch pricing, so it can never overwrite what you have configured.

Passwords

Passwords cannot move between platforms. They are stored as one-way hashes, so no previous platform can hand over anything usable — and Shopify would not accept it if it did. Your customers set a new password when you invite them, which is also the moment they confirm the address you imported still reaches them.

Marketing consent

Marketing consent is never imported: contacts arrive not subscribed. Consent given on another platform, for another sender and another purpose, does not travel with a CSV row — a re-permission pass needs its own lawful basis under the GDPR. Ask again from Shopify and the list you end up with is one you can actually stand behind.

Plan availability

Importing wholesale accounts as native B2B companies works on every paid Shopify plan — Basic, Shopify, Advanced and Plus. Company creation stopped being a Plus-only capability in 2026, so a Basic or Advanced store gets real companies, company contacts and company locations from the same file.

One caveat, stated plainly because you will meet it eventually: assigning a catalogue directly to a company location is a Shopify Plus capability. Below Plus, a B2B catalogue reaches a company through B2B markets instead. That is how Shopify works, not something the import changes. What the importer does with it is worth stating precisely. On Plus, the catalogs column attaches the named catalogues to the location the row creates. On Basic, Shopify (Grow) and Advanced, if you have set a Default B2B market under Settings → B2B Company, every company location the import approves — newly created, matched to an existing company, or updated by external ID — is added to that market — so it picks up that market’s catalogues and prices without any follow-up work, and the results file’s market column names the market for each row it applied to. If no Default B2B market is set, the import still creates the companies, contacts and locations, and you assign them to a market afterwards. One caveat either way: a B2B market carries its own currency and region settings, so pick the market that matches the buyers in the file rather than assuming one market fits every country you import. Imported rows are never evaluated against your form rules either — a market assigned by a rule’s Assign B2B market action only applies to applications submitted through your storefront form, so set the Default B2B market for the companies you import.

Bring the wholesale base you already have

Install free, download the template, and run a dry-run preview against your real list before you commit to anything. The preview creates nothing — it just tells you what your data looks like from Shopify’s side.

Keep reading: the applications guide covers the Imported badge and filter, and the getting started guide walks through the first form and approval.