Skip to content
Coritan Docs

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.

View as Markdown

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.

  • Every 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.

  1. In the dashboard, open the organization, then Commerce, then Products, then Imports.
  2. 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.
  3. 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.
  4. Under What to do, keep Check it first.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

After 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:

  1. On the Import into the store card, choose the kind, the file and the settings as for a check.
  2. Under What to do, choose Import it.
  3. 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.

Each 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_id the 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_id of 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 made

A 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 as status, keeps the store's value. The line's metadata keys 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's image is 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 its stock sets 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 metadata keys 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. Its password_hash replaces 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, metadata keys and, when it has none, addresses. A customer imported before mode=upsert existed counts as one the store made. On any customer, accepts_marketing follows 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, description and combinable, so a field the line leaves out goes back to its default, free_shipping and conditions.include_shipping included, and the uses the store lacks. A use with used_at matches an imported use with the same email and time, and one without matches an imported use by the same email. Its usage_count becomes the line's plus the uses the store's checkout recorded since, so an order placed in the store still counts. Its metadata stays the store's. A code the store made stays as it is, and so does a code imported before mode=upsert existed.
  • 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 metadata keys 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's number when 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 status and coupon, and no coupon when the line names none. It gains the referrals for orders that earned none, the payouts it lacks (by amount and paid_at) and the lifetime customers no affiliate has. A referral the import made takes the line's status, unless a payout made in the store includes it. The clicks the import recorded become the line's clicks, 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 before mode=upsert existed.
  • Leads: an imported lead takes the line's source, variant and consent_text, and the line's metadata keys over its own. A lead the storefront collected keeps its values. On any lead, consent turns from false to true when the line says true, unless the address is on the suppression list. It never turns back to false, 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's impressions in 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 says sent. 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.

In 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 Handle are 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 Tracker turns 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 Status column, or from Published. Without either column, a new product is a draft.
  • Variant Inventory Qty sets the stock count at the location you chose.
  • Product Category becomes 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.

Products, 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: 1999 is 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.com and ada@example.com are 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.

JSON
{"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's external_id, or else its handle, is updated, as Update what an earlier import made describes. Another organization's product with the same handle or external_id is 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 titled title. 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 an image that is not one of the product's images is a problem on the line, and the note names the field.
  • A product without options has 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.
JSON
{"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, city or country_code is left out, and the line's note says so. The customer is still created. The first address marked is_default_shipping is the default for shipping, or the first address when none is, and the same for billing.
JSON
{"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 percentage code without percentage, or a condition the code cannot have, is a problem on the line.
  • free_shipping is for a percentage or fixed code whose target is order or items. We ignore it, without a problem, on a code whose value_type is free_shipping and on one whose target is shipping, because those already take shipping off. A free_shipping key that is not true or false is a problem on the line.
  • conditions.include_shipping set to true on a percentage code whose target is order takes the same percentage off shipping too. It has no other spelling. Setting it on any other code, or beside free_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.skus matches 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 any conditions.variant_ids the 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": true to a code that the old platform let shoppers combine.
  • usage_count is kept as given, so a code with a usage_limit goes on counting from where it was.
  • Each entry of usages is recorded as a use of the code by that email, and by the store's customer with that email when there is one. A per_customer_limit counts 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.
JSON
{"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 (quantity times unit_price), plus shipping_total, plus the fees' amount, plus tax_total, less discount_total, must add up to total. 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, tracking and refunds become the order's fees, timeline notes, shipped fulfillments and refunds. No carrier, fulfilment provider or payment gateway is asked anything.
  • number becomes 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 refunded order'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 pending order, 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.

JSON
{"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_code is 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.

JSON
{"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.created webhook, 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.
JSON
{"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_id makes 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 same cart.external_id add 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.

The 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.

  • 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.
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 Handle column. 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_id values, because the file was imported before. Nothing is imported twice. To bring the orders in line with a newer export, import it with mode=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 Status or Published column, 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.

POST 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, a text/csv body, or JSON {"csv": "...", "filename": "..."}, with currency_code and location_id.
  • For the other kinds, send the NDJSON file as the multipart field file, an application/x-ndjson body, or JSON {"ndjson": "...", "filename": "..."}. A catalog import also takes location_id, the stock location its stock counts 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 answers 404.

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.

Shell
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"