Import products, customers, promotions and orders
Move a store over from another platform. Import its products, customers, promotions, orders, affiliates, leads and pending flow emails, then update them.
In the dashboard
Imports brings a store over from another platform. It reads eight kinds of file:
- Products from Shopify: the products CSV that Shopify exports, with variants, prices, images and stock counts.
- Products (NDJSON): an NDJSON file of products from any platform, with their variants, prices in each currency, images, stock counts and metadata.
- Customers: an NDJSON file of customer accounts, with their addresses and password hashes, so they sign in with the passwords they already have.
- Promotions: an NDJSON file of discount codes, with their conditions and how often each was used.
- Orders: an NDJSON file of past orders, recorded as they were on the old platform.
- Affiliates: an NDJSON file of the store's affiliates, with their codes, clicks, referrals, payouts and referred customers.
- Leads: an NDJSON file of the addresses shoppers left on the old storefront's sign-up forms, and how often each popup design was shown.
- Flow emails: an NDJSON file of the abandoned cart and restock emails the old platform still had to send, and those it already sent.
Each kind can be imported again to update what an earlier import made, so a store can import a week before it moves and again on the day it moves. See Import the same file again.
Check a file first: a check reads every line and reports what an import would do, without writing anything.
Before you begin
Section titled Before you beginEvery member can open Imports. Each kind of file takes its own role, to upload it and to read its imports:
File To upload it To read its imports Products from Shopify Admin or Owner Every member Products (NDJSON) Admin or Owner Every member Customers Tier 3 support or higher Tier 1 support or higher Promotions Admin or Owner Admin or Owner Orders Tier 2 support or higher Tier 1 support or higher Affiliates Tier 3 support or higher Tier 1 support or higher Leads Tier 3 support or higher Tier 1 support or higher Flow emails Tier 3 support or higher Tier 1 support or higher A products CSV can be up to 10 MB and 5,000 rows. An NDJSON file can be up to 20 MB and 50,000 lines. Split a larger file and import each part.
A store moving over imports its products first, then its customers, then its promotions, then its orders, then its affiliates, then its leads, and its flow emails last. A promotion names products by SKU and an order's items link to variants by SKU, so the products come first. An order links to the customer with its email, and a promotion's past uses link to the customers with theirs, so the customers come before both. An affiliate is a customer, its referrals name orders and its coupon names a promotion, so affiliates come after them. An abandoned cart email names products by SKU and a restock email names an imported order, so the flow emails come last.
For products, the store must sell in the currency of every price. Add it under Store in Settings first. To import stock counts, the store needs a stock location that is turned on, as Add a stock location describes.
Check a file
Section titled Check a file- In the dashboard, open the organization, then Commerce, then Products, then Imports.
- On the Import into the store card, under What the file holds, choose Products from Shopify, Products (NDJSON), Customers, Promotions, Orders, Affiliates, Leads or Flow emails. The choices are the kinds your role may upload.
- Under the file field, such as Products CSV or Customers file, select Choose a CSV file… or Choose an NDJSON file… and choose the file.
- Under What to do, keep Check it first.
- For an NDJSON file, under the records the store already has, keep Leave them as they are, or choose Update what an import made to update them as Update what an earlier import made describes.
- For products from Shopify only:
- Under Products the store already has, choose Update them or Leave them as they are.
- Under Prices are in, choose the currency of the prices. Shopify's export has no currency column.
- For either kind of products, under Stock counts are for, choose the location that the stock counts belong to. It starts at the store's default location.
- Select Check the file.
The check's report opens and follows the check until it finishes. What an import would do gives the counts, and the report lists what an import would do with each line. It says Nothing was written.
Import a file
Section titled Import a fileAfter a check in the same session, select Import this file on the check's report. It imports the same file with the same settings.
Otherwise:
- On the Import into the store card, choose the kind, the file and the settings as for a check.
- Under What to do, choose Import it.
- Select Import the file.
The import writes the file in batches, and its report shows the progress. Each batch is saved with its part of the report as it finishes. You can leave the page while it runs. One import runs at a time, of any kind: while one is running, the card shows its progress, and you can send the next file once it finishes.
Import the same file again
Section titled Import the same file againEach kind recognises what the store already has. By default, an NDJSON import only adds what is new, so sending a file again, or the rest of a file after an import stopped, adds only what is missing:
- A product in an NDJSON file whose handle the store already has is skipped.
- A customer whose email the store already has is skipped.
- A promotion whose code the store already has is skipped.
- An order whose
external_idthe store already has is skipped. - An affiliate whose code the store already has, or whose customer is already an affiliate, is skipped.
- A lead whose email the store already has is skipped, and so is a popup design's day that the store already counts.
- A flow email whose flow, step and cart or order the store already has is skipped.
- A line that repeats the handle, email, code or
external_idof an earlier line of the same file is skipped too.
Products from Shopify work differently: with Update them, a product the store already has is updated from the file. See Products from Shopify.
Update what an earlier import made
Section titled Update what an earlier import madeA store that keeps selling on the old platform while it gets ready can import everything a week before it moves, then export the files again on the day it moves and import them with Update what an import made (mode=upsert in the API). An upsert creates what is new and updates what an earlier import made from the line, so the store gets the products, orders, customers and leads that changed in between. It never deletes anything, and it leaves alone what the store made itself and what your team changed, as the list below says for each kind. A line that matches what the store already holds is skipped with the note "Already up to date."
- Products: the store's product with the line's
external_id, or else with its handle, is updated whoever made it, since the file describes the store's catalogue. It takes the fields the line gives, and a field the line leaves out, such asstatus, keeps the store's value. The line'smetadatakeys go over the product's own, and a variant's over the variant's. The product joins the line's categories and collections and stays in its others. The images it does not have are added after its own, a variant'simageis linked when no other variant has it, and a product without a thumbnail takes its first image. A variant is matched by its SKU, then by its option values, so a SKU changed on the old platform renames the variant. Its prices become the line's, and itsstocksets the count. A value the line adds to an option joins it, and a variant the line does not name stays. The product takes the line's handle unless another product has it, and the note says so. A line whose options have other titles than the product's is a problem; change the product's options in Products first. - Customers: a customer an import made takes the line's names and phone, its
metadatakeys over the customer's own, and the addresses it does not have yet. Its defaults stay, and an address is compared without case or extra spaces. Itspassword_hashreplaces the customer's while the customer still has the hash the import gave: one who reset their password, or signed in with an older format so the store now keeps a bcrypt hash, keeps their password, and the note says so. A customer the store made only gains what it lacks: a name or phone, a password when it is a guest without one,metadatakeys and, when it has none, addresses. A customer imported beforemode=upsertexisted counts as one the store made. On any customer,accepts_marketingfollows the line while the consent the store has came from an import or was never given, so an opt out made in the store stays. An address on the suppression list never gets consent. - Promotions: a code an import made takes the line's value,
conditions, limits, dates,status,descriptionandcombinable, so a field the line leaves out goes back to its default,free_shippingandconditions.include_shippingincluded, and the uses the store lacks. A use withused_atmatches an imported use with the same email and time, and one without matches an imported use by the same email. Itsusage_countbecomes the line's plus the uses the store's checkout recorded since, so an order placed in the store still counts. Itsmetadatastays the store's. A code the store made stays as it is, and so does a code imported beforemode=upsertexisted. - Orders: an imported order whose line changed since the import that wrote it is written again in place. The import keeps a fingerprint of each line, so an order whose line is the same is skipped. The order's status, payment, fulfilment, email, addresses, totals, codes and dates become the line's, and the line's
metadatakeys join the order's. Its items, shipping, fees, notes, tracking and refunds from the import are replaced by the line's. The order keeps its id, its number and everything your team added: their notes, shipments and refunds stay, and refunds your team recorded still count in the payment. The order takes the line'snumberwhen no other order has it; otherwise it keeps its own and the note says so. While a return, a document, a reserved item or your team's own shipment or refund names the order, its items stay as they are and the note says so. The timeline gets an entry saying an import updated the order, and nothing is sent or emitted. An order the store took itself stays as it is. - Affiliates: an affiliate an import made, found by its code or else by its customer, takes the line's
statusand coupon, and no coupon when the line names none. It gains the referrals for orders that earned none, the payouts it lacks (byamountandpaid_at) and the lifetime customers no affiliate has. A referral the import made takes the line'sstatus, unless a payout made in the store includes it. The clicks the import recorded become the line'sclicks, and the clicks the storefront counted stay. Found by its customer, it keeps its code, and the note says so. An affiliate the store made, or a code the store gives another customer, stays as it is, and so does an affiliate imported beforemode=upsertexisted. - Leads: an imported lead takes the line's
source,variantandconsent_text, and the line'smetadatakeys over its own. A lead the storefront collected keeps its values. On any lead,consentturns fromfalsetotruewhen the line saystrue, unless the address is on the suppression list. It never turns back tofalse, so when a shopper opts out on the old platform after the first import, opt them out in the store too. A design's day takes the line'simpressionsin place of the count the store has, so leave out the day of the move, when the store counts its own. - Flow emails: an email an import scheduled that has not gone out yet moves to the line's
scheduled_at, or is marked sent when the line sayssent. An email the store's flow made, sent, skipped or canceled stays as it is, and so does every email of a cart the shopper changed in the store.
Products from Shopify
Section titled Products from ShopifyIn your Shopify admin, open Products, choose Export, and save the CSV file. A spreadsheet file, such as one saved as .xlsx, is refused.
- Rows that share a
Handleare one product. The first row holds the product and its first variant, and the rows after it add variants or images. - With Update them, a product whose handle the store already has is updated. A variant matches the store's by SKU, then by its option values, and a variant the file does not mention is left alone. A blank cell keeps what the store has, except that a blank
Variant Inventory Trackerturns off stock tracking for the variant, as it does in Shopify. - With Leave them as they are, those products are skipped, and only products with a new handle are created.
- A product takes its status from the
Statuscolumn, or fromPublished. Without either column, a new product is a draft. Variant Inventory Qtysets the stock count at the location you chose.Product Categorybecomes a category that the storefront does not show.- Images stay at the addresses in the file, which must start with
https://. The store links them without copying them, as Add images explains.
NDJSON files
Section titled NDJSON filesProducts, customers, promotions, orders and affiliates can each come as an NDJSON file, one kind per file. The tool that exports them from the old platform writes this format:
- The file is UTF-8 text with one JSON object per line. A blank line is ignored, and it still counts in the line numbers that the report gives.
- Amounts are whole numbers of minor units:
1999is 19.99 EUR. - Dates are ISO 8601, such as
2024-05-01T12:00:00Z. A date without an offset is UTC. - Emails are compared in lower case, so
Ada@Example.comandada@example.comare the same person. - A field the kind does not read is ignored. A field that is left out, or
null, takes its default. - A line that is not JSON, not an object, or not what its kind reads is a problem on that line. The rest of the file still imports.
The examples below are spread over several lines to be readable. In the file, each object is on one line.
Products
Section titled Products{"handle": "calm-tea", "title": "Calm tea", "subtitle": "Loose leaf", "description": "<p>Chamomile and lemon balm.</p>",
"status": "published", "tags": ["tea", "herbal"], "seo": {"title": "Calm tea", "description": "A caffeine free tea."},
"metadata": {"origin": "EG"}, "external_id": "4711",
"categories": [{"handle": "tea", "name": "Tea"}], "collections": [{"handle": "new", "title": "New in"}],
"images": [{"url": "https://cdn.example.com/calm.jpg", "alt": "A tin of tea"},
{"url": "https://cdn.example.com/calm-100.jpg"}],
"thumbnail": "https://cdn.example.com/calm.jpg",
"options": [{"title": "Size", "values": ["50 g", "100 g"]}],
"variants": [{"sku": "CALM-50", "title": "50 g", "options": {"Size": "50 g"}, "prices": {"EUR": 900},
"compare_at_prices": {"EUR": 1200}, "weight_g": 60, "stock": 40, "barcode": "4006381333931",
"metadata": {"bogo": true, "coa": "https://cdn.example.com/coa/calm-50.pdf"}},
{"sku": "CALM-100", "options": {"Size": "100 g"}, "prices": {"EUR": 1600}, "stock": 12,
"image": "https://cdn.example.com/calm-100.jpg"}],
"tax_code": "food", "discountable": true}
| Field | Required | What it holds |
|---|---|---|
handle |
yes | The product's handle, up to 160 characters. It is made a handle as the dashboard makes one: lower case, words joined by hyphens, accents dropped. |
title |
yes | Up to 255 characters. |
subtitle |
no | Up to 255 characters. |
description |
no | HTML, as the product's description takes it. |
status |
no | published (the default) or draft. |
tags |
no | Up to 250 words. |
seo |
no | An object with title (up to 255 characters) and description (up to 500), the product's SEO title and description. |
metadata |
no | An object of up to 16 KB, checked as the Commerce API checks a product's metadata. |
external_id |
no | The product's id on the old platform, up to 100 characters. It is kept in the product's metadata as external_id. |
categories |
no | Up to 100, each with handle and optionally name. |
collections |
no | Up to 100, each with handle and optionally title. |
images |
no | Up to 250, each with url, which starts with https://, and optionally alt. |
thumbnail |
no | An https:// address. The default is the first image. |
options |
no | Up to 10, each with title and its values, such as {"title": "Size", "values": ["50 g", "100 g"]}. |
variants |
yes | At least one and up to 500, with the fields in the next table. |
tax_code |
no | Up to 32 characters. |
discountable |
no | false when promotions never discount the product. The default is true. |
Each variant takes these fields:
| Field | Required | What it holds |
|---|---|---|
sku |
yes | Up to 100 characters, unique in the store whatever its case. |
title |
no | Up to 255 characters. |
options |
when the product has options | One value of each of the product's options, by the option's title, such as {"Size": "50 g"}. |
prices |
yes | The price in each currency, in minor units, such as {"EUR": 900, "USD": 1000}. Each currency must be one the store sells in. |
compare_at_prices |
no | The price before a sale, in the same form. Each currency must also be in prices. |
weight_g |
no | The weight in grams. |
manage_inventory |
no | false when the store does not track the variant's stock. The default is true. |
allow_backorder |
no | true when shoppers can buy it out of stock. The default is false. |
stock |
no | The stock count at the location you chose. A count below zero is set as 0. Left out, the variant has no count there. |
barcode |
no | Up to 64 characters. |
tax_code |
no | Up to 32 characters. |
metadata |
no | An object of up to 16 KB, checked as the Commerce API checks a variant's metadata. Keys such as bogo or coa are kept as given, for a storefront to read. |
image |
no | The url of one of the product's images, which the storefront shows for this variant. |
A product is created exactly as the Commerce API creates one from the same fields, with the same checks, and is sold in the store's default sales channel:
- A handle the store already has is skipped. With
mode=upsert, the product with the line'sexternal_id, or else its handle, is updated, as Update what an earlier import made describes. Another organization's product with the same handle orexternal_idis a different product. - A category or collection is matched by its handle. One the store does not have is created: a category the storefront shows, named
name, or a manual collection titledtitle. Without a name or title, it is named after its handle. A smart collection chooses its own products, so naming one is a problem on the line. - A SKU that another product of the store has, or that an earlier line of the file gave to another product, is a problem on the line, and the note names the variant.
- A currency the store does not sell in, an option value that is not one of the values in
options, or animagethat is not one of the product'simagesis a problem on the line, and the note names the field. - A product without
optionshas one variant. Two variants with the same option values are a problem on the line. - An image belongs to one variant. When two variants name the same image, the first gets it, and the line's note names the variant that does not.
- Images stay at the addresses in the file. The store links them without copying them, as Add images explains.
Customers
Section titled Customers{"email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "phone": "+44 20 7946 0000",
"password_hash": "$wp$2y$10$...", "accepts_marketing": true, "marketing_consent_at": "2023-04-01T09:00:00Z",
"created_at": "2021-06-01T10:00:00Z",
"addresses": [{"first_name": "Ada", "last_name": "Lovelace", "address_1": "12 St James's Square",
"city": "London", "postal_code": "SW1Y 4JH", "country_code": "GB",
"is_default_shipping": true, "is_default_billing": true}],
"metadata": {"loyalty_tier": "gold"}, "external_id": "4711"}
| Field | Required | What it holds |
|---|---|---|
email |
yes | The customer's email address. |
first_name, last_name |
no | Up to 100 characters each. |
phone |
no | Up to 50 characters. |
password_hash |
no | The hash of the customer's password on the old platform, in one of the formats below. |
accepts_marketing |
no | true when the customer agreed to marketing email. The default is false. |
marketing_consent_at |
no | When they agreed. |
created_at |
no | When the account was created. The default is the time of the import. |
addresses |
no | Up to 50 addresses, each with first_name, last_name, company, address_1, address_2, city, province, postal_code, country_code (two letters), phone, is_default_shipping and is_default_billing. |
metadata |
no | An object of up to 50 keys, checked as the Commerce API checks metadata. |
external_id |
no | The customer's id on the old platform, up to 64 characters. It is kept in the customer's metadata. |
password_hash is kept exactly as given, and never hashed again. Customer sign-in accepts these formats:
| Format | How the hash starts |
|---|---|
| bcrypt | $2a$, $2b$ or $2y$ |
| WordPress 6.8 and later | $wp$, then a bcrypt hash |
| phpass, as older WordPress and phpBB write | $P$ or $H$ |
| MD5 | 32 hexadecimal digits, and nothing else |
Any other value is a problem on the line, and the customer is not created. A customer without password_hash has no password yet, like a guest at checkout, and signs in once they set one with a password reset. When a customer whose hash is in an older format signs in, the store checks the password against it and then keeps a bcrypt hash of the same password instead.
- A customer whose email the store already has is skipped, unless the import updates what an earlier import made, as Update what an earlier import made describes. Another organization's customer with the same email is a different person.
- No welcome or verification email is sent, and the email address is not marked verified.
- An address that lacks
address_1,cityorcountry_codeis left out, and the line's note says so. The customer is still created. The first address markedis_default_shippingis the default for shipping, or the first address when none is, and the same for billing.
Promotions
Section titled Promotions{"code": "WELCOME10", "value_type": "percentage", "percentage": 10, "target": "order",
"conditions": {"min_subtotal": 5000, "allowed_emails": ["*@example.com"]}, "currency_code": "EUR",
"usage_limit": 500, "usage_count": 212, "per_customer_limit": 1,
"starts_at": "2024-01-01T00:00:00Z", "ends_at": "2026-12-31T23:59:59Z", "status": "active",
"description": "Ten percent off a first order",
"usages": [{"email": "ada@example.com", "used_at": "2024-02-03T10:00:00Z"}]}
| Field | Required | What it holds |
|---|---|---|
code |
yes | Letters, digits, - and _, starting with a letter or digit, up to 64 characters. It is stored in capitals. |
value_type |
yes | percentage, fixed or free_shipping. |
percentage |
for percentage |
The percentage off. |
amount |
for fixed |
The amount off, in minor units. |
currency_code |
for fixed, or with min_subtotal |
The currency of the amounts, as a three-letter code. |
target |
no | order (the default), items or shipping. A free_shipping code is always shipping. |
conditions |
no | The conditions a cart must meet, with the same names and rules as in Create promotions. A condition that names ids, such as product_ids, names this store's own ids. conditions.skus also names variants, by SKU. conditions.include_shipping and conditions.free_shipping take shipping off too, as the bullets below describe. |
conditions.skus |
no | Up to 500 SKUs. Each becomes the store's variant with that SKU, in conditions.variant_ids. |
combinable |
no | true when the code can be used beside other codes and automatic promotions. The default is false. |
free_shipping |
no | true when shipping also costs nothing while the code applies, as WooCommerce's "Allow free shipping" does. A metadata.free_shipping set to true counts the same, because converters from WooCommerce write it there. The default is false. |
usage_limit |
no | How many times the code can be used in all. |
per_customer_limit |
no | How many times one customer can use it. |
usage_count |
no | How many times it was used on the old platform. The default is the number of usages. |
starts_at, ends_at |
no | When the code can be used. |
status |
no | active (the default) or disabled. |
description |
no | Up to 255 characters. |
usages |
no | Up to 10,000 past uses, each with email and used_at. |
- A promotion is checked by the same rules as one created in the dashboard, so a
percentagecode withoutpercentage, or a condition the code cannot have, is a problem on the line. free_shippingis for apercentageorfixedcode whosetargetisorderoritems. We ignore it, without a problem, on a code whosevalue_typeisfree_shippingand on one whosetargetisshipping, because those already take shipping off. Afree_shippingkey that is nottrueorfalseis a problem on the line.conditions.include_shippingset totrueon apercentagecode whosetargetisordertakes the same percentage off shipping too. It has no other spelling. Setting it on any other code, or besidefree_shipping, is a problem on the line.- A code the store already has is skipped. With
mode=upsert, a code an import made is updated, as Update what an earlier import made describes. - A SKU in
conditions.skusmatches the store's variant with that SKU whatever its case, as an order's items do. A SKU that no variant of the store has is a problem on the line, and the note names it, so import products first. The variants join anyconditions.variant_idsthe line gives. - Without
combinable, an imported code is used alone: a shopper cannot enter another code beside it, and the store's automatic promotions, such as a free gift, do not apply while it does. Give"combinable": trueto a code that the old platform let shoppers combine. usage_countis kept as given, so a code with ausage_limitgoes on counting from where it was.- Each entry of
usagesis recorded as a use of the code by that email, and by the store's customer with that email when there is one. Aper_customer_limitcounts these uses, so a customer who used a one-time code on the old platform cannot use it again. An imported use names no order and no discount amount.
Orders
Section titled Orders{"external_id": "wc-10452", "number": 10452, "email": "ada@example.com", "status": "completed",
"currency_code": "EUR", "created_at": "2025-11-02T14:05:00Z",
"items": [{"sku": "TEE-M-BLK", "title": "Tee, M, black", "quantity": 2, "unit_price": 2000,
"discount_total": 400, "tax_total": 684}],
"shipping_total": 490, "shipping_tax_total": 93, "discount_total": 400, "tax_total": 797, "total": 4887,
"fees": [{"code": "gift_wrap", "label": "Gift wrap", "amount": 100, "tax_total": 20}],
"shipping_address": {"first_name": "Ada", "address_1": "Hauptstr. 1", "city": "Berlin",
"postal_code": "10115", "country_code": "DE"},
"billing_address": null, "promo_codes": ["WELCOME10"],
"notes": [{"body": "Left with the neighbour.", "created_at": "2025-11-04T09:00:00Z", "visibility": "customer"}],
"tracking": [{"carrier": "DHL", "number": "00340434161234567890", "url": "https://www.dhl.com/track",
"shipped_at": "2025-11-03T16:00:00Z"}],
"refunds": [{"amount": 1000, "reason": "Damaged in transit", "created_at": "2025-11-10T12:00:00Z"}],
"metadata": {"wc_payment_method": "stripe"}}
| Field | Required | What it holds |
|---|---|---|
external_id |
yes | The order's id on the old platform, up to 64 characters. The store recognises the order by it. |
number |
no | The order number shoppers know, such as 10452 or "#10452". |
email |
yes | The shopper's email. |
status |
yes | pending, processing, completed, canceled or refunded, as in the table below. |
currency_code |
yes | A three-letter code. |
created_at |
yes | When the order was placed. |
items |
yes | At least one and up to 500, each with title, quantity and unit_price, and optionally sku, discount_total and tax_total. |
shipping_total, shipping_tax_total |
no | Shipping and its tax. |
discount_total, tax_total |
no | The whole order's discount and tax, including shipping and fees. Left out, each is the sum of its parts. |
total |
yes | What the shopper paid. |
fees |
no | Up to 50, each with code, label, amount and optionally tax_total. |
shipping_address, billing_address |
no | Addresses with the same fields as a customer's. |
promo_codes |
no | Up to 20 codes the order used. |
notes |
no | Each with body, optionally created_at, and visibility: internal (the default) or customer, which the shopper sees. |
tracking |
no | Each with number, and optionally carrier, url and shipped_at. |
refunds |
no | Each with amount and created_at, and optionally reason. |
metadata |
no | An object, checked as the Commerce API checks metadata. |
An order is recorded as it was, and never priced again:
- The totals are the file's. Amounts exclude tax. The items (
quantitytimesunit_price), plusshipping_total, plus the fees'amount, plustax_total, lessdiscount_total, must add up tototal. When they do not, the line is a problem and its note gives each part. In the example, 4000 + 490 + 100 + 797 - 400 = 4887. - An item links to the store's variant with the same SKU when there is one. Otherwise it keeps its title and SKU and links to no product. Each item with tax gets one tax line of the imported tax, and so does shipping.
fees,notes,trackingandrefundsbecome the order's fees, timeline notes, shipped fulfillments and refunds. No carrier, fulfilment provider or payment gateway is asked anything.numberbecomes the order's number when no order of the store has it. When one does, the line is a problem. An order without a number takes the store's next number. The store's next number starts above the highest number in the file, so no order placed during or after the import takes an imported number.- The order's customer is the store's customer with the order's email, when there is one. Import customers first.
status sets the order's status, its payment and its fulfilment:
status |
Order | Payment | Fulfilment |
|---|---|---|---|
completed |
Completed | Captured | Delivered with tracking, otherwise fulfilled |
processing |
Pending | Captured | Shipped with tracking, otherwise not fulfilled |
pending |
Pending | Awaiting payment | Shipped with tracking, otherwise not fulfilled |
canceled |
Canceled | Canceled, or refunded when it has refunds | Canceled, or shipped with tracking |
refunded |
Canceled | Refunded | Canceled, or shipped with tracking |
- A paid order's refunds make its payment partially refunded, or refunded once they cover the total.
- A
refundedorder's refunds cover its total. When the refunds in the file come to less, one more refund for the rest is recorded, and the line's note says so. - Refunds above what was paid, or on a
pendingorder, are a problem on the line.
An imported order is history, so it changes nothing else:
- It reserves no stock and counts no use of a promotion.
- It adds nothing to your balance and issues no invoice.
- It sends no email and no webhook.
- Its timeline starts with a note saying it was imported, with its
external_id.
An order whose external_id the store already has is skipped. With mode=upsert, an imported order whose line changed is written again in place, as Update what an earlier import made describes.
Affiliates
Section titled Affiliates{"email": "ada@example.com", "code": "ADA20", "status": "active", "created_at": "2024-02-01T09:00:00Z",
"clicks": 1520,
"referrals": [{"order_external_id": "wc-10452", "amount": 4000, "commission": 800, "status": "paid"}],
"payouts": [{"amount": 800, "paid_at": "2024-06-01T00:00:00Z", "note": "PayPal"}],
"lifetime_customers": ["grace@example.com"]}
| Field | Required | What it holds |
|---|---|---|
email |
yes | The email of the store's customer who is the affiliate. Import customers first. |
code |
yes | 3 to 32 letters, digits and hyphens, starting with a letter or digit. It is stored in capitals. |
status |
no | active (the default) or disabled. |
coupon_code |
no | The code of the store's promotion whose use credits the affiliate. Import promotions first. |
created_at |
no | When they joined. The default is the time of the import. |
clicks |
no | How many clicks their links had, recorded on the day of created_at, or today without it. |
referrals |
no | Up to 5,000, each with order_external_id, amount (the order's merchandise), commission, and status: draft, unpaid (the default), paid or rejected. |
payouts |
no | Up to 1,000, each with amount and paid_at, and optionally currency_code (the store's default currency when left out) and note. |
lifetime_customers |
no | Up to 10,000 emails of customers the affiliate referred, whose later orders earn the affiliate the repeat rate. |
- A line whose email the store has no customer for is a problem on the line.
- A code the store already has, or a customer who is already an affiliate, is skipped. With
mode=upsert, an affiliate an import made is updated, as Update what an earlier import made describes. coupon_codeis matched in capitals, as promotion codes are. A code the store has no promotion for, a single use code minted from another promotion, or a promotion that another affiliate already has as its coupon, is a problem on the line. So is a promotion that an earlier line of the file gave to another affiliate.- A referral names an imported order by its
external_id, and keeps the file's amounts in the order's currency. Its rate is the commission as a percent of the amount. A referral whose order the store does not have, or whose order already has a referral, is left out, and the line's note says so. - An email that another affiliate of the store already has among its customers is left out.
- An imported payout is a record of what was paid on the old platform. It is not tied to the imported paid referrals.
An imported affiliate sends no email and no webhook. Its referred customers' next orders are credited as the program's settings say, once the program is on.
Leads
Section titled Leads{"email": "ada@example.com", "source": "welcome_popup", "variant": "gold", "consent": true,
"consent_text": "Email me offers and news.", "created_at": "2024-05-01T12:00:00Z",
"metadata": {"landing_page": "/shop/"}}
{"variant": "gold", "impressions": 1830, "day": "2024-05-01"}
A line is either a lead or a count of how often the storefront showed a popup design on one day. A line with impressions is a count; every other line is a lead.
| Field | Required | What it holds |
|---|---|---|
email |
yes, for a lead | The address the shopper left, up to 254 characters. |
source |
no | The form they used, such as welcome_popup, up to 64 characters. |
variant |
yes, for a count | The popup design, up to 64 characters. A lead may name the design it signed up through. |
consent |
no | true when they agreed to marketing email. The default is false. |
consent_text |
no | The words they agreed to, up to 500 characters. |
created_at |
no | When they signed up. The default is the time of the import. |
metadata |
no | An object of up to 4 KB. A key that starts with _coritan is a problem on the line. |
impressions |
yes, for a count | How many times the design was shown that day, a whole number from 0. |
day |
no | The day of the count, such as 2024-05-01, in UTC. The default is the day the import started. |
- Leads and counts are written in the store's mode: live once the store takes live payments, test before. They appear in Leads, and the counts in its comparison of popup designs.
- A lead whose email the store already has is skipped, and so is a design's day that the store already counts.
- An address on the store's suppression list, such as one that unsubscribed, is imported without consent, and the line's note says so.
- An imported lead sends no email and no
lead.createdwebhook, so an integration that welcomes new sign ups does not take the old platform's leads for new ones. - When an imported lead signs up again on the storefront, it becomes the store's own lead, and a later import leaves it alone, as Import the same file again describes.
Flow emails
Section titled Flow emails{"flow": "abandoned_cart", "step": 2, "email": "ada@example.com", "status": "scheduled",
"scheduled_at": "2026-10-09T09:30:00Z",
"cart": {"external_id": "cart-381", "updated_at": "2026-10-08T09:30:00Z",
"items": [{"sku": "CALM-50", "quantity": 2}], "metadata": {"source": "old-platform"}}}
{"flow": "restock", "email": "ada@example.com", "status": "sent", "sent_at": "2026-09-20T09:00:00Z",
"order_external_id": "wc-10452"}
A line is one email of the store's abandoned cart or restock flow: one the old platform still had to send, so the store sends it when it is due, or one it already sent, kept as history.
| Field | Required | What it holds |
|---|---|---|
flow |
yes | abandoned_cart or restock. |
step |
no | The step of the abandoned cart flow, 1 to 5. A restock email has one step. The default is 1. |
email |
yes | The address the email goes to: the cart's email, or the email of the order. |
status |
no | scheduled (the default) or sent. |
scheduled_at |
yes, for a scheduled email | When the store sends it. For a sent email, the default is sent_at. |
sent_at |
yes, for a sent email | When the old platform sent it. |
cart |
yes, for an abandoned cart | The cart the email brings the shopper back to: external_id (up to 100 characters), updated_at (when the shopper last changed it), items (1 to 100, each a sku and a quantity from 1 to 999, 1 when left out) and metadata. |
order_external_id |
yes, for a restock email | The external_id of an order an orders import made. Import orders first. |
- The first line that names a
cart.external_idmakes the cart: open, with the line's email, in the store's mode, its default region and currency, holding each item as the store's product with that SKU at today's price. Later lines with the samecart.external_idadd their steps to the same cart, so one cart has all its emails. The cart is made once and never updated. - An item the store does not sell now, such as a SKU it does not have or a product that is out of stock, is left out, and the line's note says so. A cart with none of its items is a problem on the line.
- A restock email counts from the order's first shipment, or from when the order was placed.
- The store's flow sends a scheduled email when it is due, with every check it makes for its own emails: nothing goes out while the flow or its step is off, to an address on the suppression list, for a cart that was checked out or emptied, for a cancelled order, or more than a day late. A test cart's emails go out only while Email test carts and orders is on.
- The store's flow does not schedule an imported cart's emails again, nor add steps the file left out. When the shopper changes the cart, through the email's link, the cart is the store's own, and its flow schedules the steps it has not sent from then on.
- A sent email is never sent again. It counts when the store spaces out the emails to one address, as an email the store sent does.
- An email whose flow, step and cart or order the store already has is skipped.
- A run sends nothing and emits no webhook.
Read an import's report
Section titled Read an import's reportThe Imports card lists every import, newest first, with its File, Status, the Result, the Problems and when it was Uploaded. Check only marks a check. Select an import to open its report.
| Status | What it means |
|---|---|
| Queued | It waits to start, and says Starts shortly. |
| Running | It is reading the file, and says how many products or lines it has done. |
| Completed | It read the whole file. |
| Failed | It stopped part way. The report says why. |
The counts are under What it did, or What an import would do for a check, or So far while it runs.
For products from Shopify, the counts are Products created, Products updated, Products skipped, Variants created, Variants updated, Images added, Stock counts set and Categories created. Rows it could not use gives each skipped row's number in the file, its Handle and the Problem. A product with no usable variant row is skipped whole.
For an NDJSON file, the counts are what was created, what was updated, what was skipped and the Lines with problems. What each line did, or What each line would do for a check, lists every line of the file with its Line number, its Result, the record it created or matched, and a Note:
Result What it means Created The line created the record, such as a product, customer or order. The note says what of it was left out, such as an address. Updated An upsert brought the record an earlier import made in line with the line. The note says what it kept, such as a password the customer changed. Skipped The store already has it, an earlier line of the file named it, or an upsert found it already up to date. The note says which. Problem The line could not be used. The note names the field and why. It opens on Problems when there are any. Choose Every line, Problems, Skipped, Created or Updated to show those lines, and search by line number, record number or note.
The rest of the file still imports when some lines have problems. Select Download as CSV to fix the lines in the file. The notes name fields and amounts, never a person's details, and a password hash is never shown.
Result
Section titled Result- Imported products appear in Products, with their stock counts at the location you chose in Inventory. Products from Shopify are priced in the currency you chose, and products from an NDJSON file in each currency of their
prices. - Imported customers appear in Customers and can sign in to the storefront with their old passwords.
- Imported promotions appear in Discounts, with their usage counts.
- Imported orders appear in Orders with their numbers, and in each customer's order history.
- A check changes nothing in the store.
Troubleshooting
Section titled Troubleshooting- Check the file or Import the file cannot be selected
- Another import is running. Wait for it to finish, or select See its progress to follow it.
- A kind is missing under What the file holds
- Your role cannot upload that kind. The table in Before you begin gives the role each kind takes.
- The card says Could not start the import
- Another import started first, or the file was refused, and the alert gives the reason. See that import opens the import that is running.
- The file is refused
- A products CSV is over 10 MB or 5,000 rows, empty, a spreadsheet file rather than CSV, or not Shopify's product export, such as a file without a
Handlecolumn. An NDJSON file is over 20 MB or 50,000 lines, empty, or has no line that is a JSON object. Split the file, or export it again. - Every line of an orders import is skipped
- The store already has orders with those
external_idvalues, because the file was imported before. Nothing is imported twice. To bring the orders in line with a newer export, import it withmode=upsert. - Imported orders are not linked to their customers
- The customers were not in the store when the orders were imported. An order is linked to a customer only when it is imported, so import the customers first next time.
- A line says the order's parts do not add up to its total
- The items, shipping, fees, tax and discounts in the line come to a different amount than
total. The note gives each part; correct the export and import the file again. The orders already imported are skipped. - A customer cannot sign in with their old password
- The line had no
password_hash, so the customer has no password yet. They set one with a password reset, as Verify an address and reset a password describes. - The report says The import stopped
- The alert gives the reason. The batches it finished stay imported, so send the file again to import the rest.
- Imported products are not on the storefront
- A Shopify file had no
StatusorPublishedcolumn, or an NDJSON line gave"status": "draft", so the new products are drafts. Publish them in Products. - Every line of a products NDJSON import is skipped
- The store already has products with those handles, because the file was imported before. To bring them in line with a newer export, import it with
mode=upsert, or change them in Products. - Stock counts were not imported
- The store had no stock location turned on, and Stock counts are for said No location. Add a stock location, then import the file again.
- Import this file is missing from a check's report
- The dashboard forgets the file when you reload the page. Select Upload the file, then choose the file again with Import it.
- The report says No such import
- The link points to an import this store does not have, or one your role cannot read. Select All imports to go back to the list.
Related
Section titled Related- Add and edit your store's products
- Track stock and stock locations
- Customer sign-in
- Change your store's settings
With the API
Section titled With the APIPOST https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/imports queues an import and answers 202 with it. kind says what the file holds: products (the default, Shopify's CSV), catalog (products as NDJSON), customers, promotions, orders, affiliates, leads or flow_messages (flow emails). Send it in the query or as the multipart field kind; any other word answers 422 with the words it takes. Add dry_run=true for a check.
mode says what a line does with a record the store already has. For the NDJSON kinds, create_only (the default) skips it and upsert updates what an earlier import made, as Import the same file again describes. For products from Shopify, upsert (the default) updates it and create_only skips it. Any other word answers 422 with the words the kind takes. The import shows its mode in params.mode.
- For products, send the CSV as the multipart field
file, atext/csvbody, or JSON{"csv": "...", "filename": "..."}, withcurrency_codeandlocation_id. - For the other kinds, send the NDJSON file as the multipart field
file, anapplication/x-ndjsonbody, or JSON{"ndjson": "...", "filename": "..."}. Acatalogimport also takeslocation_id, the stock location itsstockcounts are for. Without it, the store's default location is used, or its first location that is turned on. A location that is not the store's answers404.
GET /commerce/imports/{import_id} follows an import, and GET /commerce/imports?kind=customers lists the store's imports of one kind. An NDJSON import's report holds created, updated, skipped, error_count and lines, one {"line", "action", "id", "message"} per line of the file, where action is created, updated, skipped or error. An API key needs the scope of the kind: commerce.catalog:write (for both kinds of products), commerce.customers:write (for customers, affiliates, leads and flow emails), commerce.promotions:write or commerce.orders:write. Import products from Shopify has the details.
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/imports?kind=customers&dry_run=true" \
-H "Authorization: Bearer $CORITAN_TOKEN" \
-F "file=@customers.ndjson"