Products CSV format

The products CSV powers both the bulk import at /dashboard/inventory/import and the catalog export at /dashboard/inventory/export. This page documents every column, every validation rule, and every header synonym the parser recognizes.

Canonical columns

The full column list, in the order they appear in the export and sample template:

sku,name,description,category,unit,price,stock,isActive

Four of these are required, four are optional:

ColumnRequiredDescription
skuUnique identifier for the product within your catalog. Matching key on import.
nameHuman-readable product name.
descriptionLong-form description. Can be blank.
categoryFree-text category string. Can be blank.
unitUnit of measure (e.g. each, case, kg). Defaults to each if omitted.
priceUnit price. Non-negative, up to 2 decimal places.
stockCurrent stock on hand. Non-negative whole number.
isActivetrue or false. Defaults to true if omitted.

If any of the four required columns can't be mapped from your CSV, the preview panel will surface a column-mapping UI so you can pick which of your columns to use.

Header name matching

Headers don't need to match the canonical names exactly. Before parsing, each header is lowercased and stripped of spaces, underscores, and dashes — then matched against a list of synonyms per field. So SKU, Sku, item code, Item_Code, Item-Code, and itemCode all map to sku.

Here's the full synonym table:

CanonicalAlso recognized
skuitemcode, item_code, productcode, product_code, itemnumber, item_number, itemid, item_id, partnumber, part_number
nameproductname, product_name, title, itemname, item_name, product
descriptiondesc, productdescription, product_description, longdescription
categorycat, productcategory, product_category, group, department
unituom, units, unitofmeasure, unit_of_measure
priceunitprice, unit_price, listprice, list_price, costprice, cost, sellprice, sell_price, msrp
stockquantity, qty, onhand, on_hand, inventory, available, stocklevel, stock_level, stockqty
isActiveis_active, active, status, enabled, published

These synonyms cover exports from Shopify, QuickBooks, NetSuite, most spreadsheet templates, and hand-rolled inventory lists. If your CSV has a column the parser doesn't recognize, adjust it in the mapping dropdowns in the preview panel — you don't need to rename columns in the file itself.

Validation rules

Every row is validated against this schema. Failures are collected into the issue list (with row numbers) rather than aborting the import, so you can see everything wrong with a file in one pass.

sku

  • Type: string
  • Trimmed of surrounding whitespace
  • Min length: 1 (required — empty cells fail)
  • Max length: 100
  • Must be unique within the CSV. Duplicates are flagged with the row number of the first occurrence: Duplicate sku "WDG-001" also on row 14

name

  • Type: string
  • Trimmed
  • Min length: 1
  • Max length: 200

description

  • Type: string (optional)
  • Trimmed
  • Max length: 2000
  • Empty string or missing → stored as null

category

  • Type: string (optional)
  • Trimmed
  • Max length: 100
  • Empty string or missing → stored as null

unit

  • Type: string (optional)
  • Trimmed
  • Max length: 50
  • Default: each if omitted or empty

price

  • Type: number
  • Must be non-negative (≥ 0)
  • At most 2 decimal places9.99 OK, 9.999 rejected
  • Coerced from string, so "9.99" is accepted

stock

  • Type: integer
  • Must be a whole number (no decimals)
  • Must be non-negative
  • Coerced from string

isActive

  • Type: boolean
  • Default: true if omitted or empty

Case-insensitive string values accepted:

ValueParsed as
true, 1, yes, y, activetrue
false, 0, no, n, inactivefalse
anything elsetrue (fail-open default)

How imports match existing products

Rows are matched to existing products by sku scoped to your company. For each row:

  • No existing product with that SKU → row is flagged create.
  • Existing product, every field identical → row is flagged unchanged (skipped on apply — no-op).
  • Existing product, any field differs → row is flagged update.

"Unchanged" is computed by comparing name, description, category, unit, price (to 2dp), stock, and isActive against the stored values. Unchanged rows don't count against your plan limit.

The preview panel shows the counts for all three, plus a sample of up to 50 rows with their SKU, name, and action.

Plan limit enforcement

Before the apply step runs, Distribu checks whether creating the new rows would push you over your plan's product cap:

  • Starter — 50 products
  • Growth — 500 products
  • Enterprise — unlimited

If the projected total (current_count + new_rows) exceeds the plan max, the import is blocked with:

Import would exceed your plan's product limit (50). Upgrade or reduce new rows.

The preview panel also surfaces this warning before you click Apply, so you can trim rows instead of hitting it at commit time. Update-only imports (no creates) are never blocked on plan limits.

See Billing → Plans for the full limit table.

The commit is atomic

On apply, all creates and updates run in a single database transaction. If any row fails mid-apply, every write is rolled back — you never end up with a half-imported file.

Imports are logged in the audit trail as ProductBulkImported with metadata showing the number of rows created and updated.

Sample template

Download the live template at /dashboard/inventory/import/template (requires login). The file is exactly:

sku,name,description,category,unit,price,stock,isActive
WIDGET-001,Example Widget,A short description of the product,Widgets,each,9.99,100,true
WIDGET-002,Another Widget,,Widgets,case,24.50,0,true

Two rows, valid data, one showing how description can be left blank.

Export format

The export at /dashboard/inventory/export serializes every product in your catalog using the same column order and headers as the import schema. That means:

  • Edit the exported file in a spreadsheet.
  • Re-upload it at /dashboard/inventory/import.
  • Changes show up as updates, new rows as creates, unchanged rows as unchanged.

Export details:

  • Filename: products-{YYYY-MM-DD}.csv
  • Sort order: category ascending, then name ascending
  • Blank fields serialized as empty string (not null)
  • price formatted to 2 decimal places (9.99, not 9.9)
  • isActive serialized as true / false
  • stock serialized as a plain integer

Empty catalogs still export a valid CSV — just the header row, no body.

Common pitfalls

Leading zeroes in SKUs

Excel strips leading zeroes when you open a CSV — 0123 becomes 123. Before saving, format the SKU column as Text (not General), or open the file in a plain-text editor to preserve SKUs with leading zeroes.

Thousand separators in price / stock

$1,299.99 or 1,299 won't parse as a number. Strip commas and currency symbols before export. Prices should be plain decimals: 1299.99.

Trailing whitespace in SKUs

"WDG-001 " and "WDG-001" are treated as the same SKU — whitespace is trimmed before the comparison. Avoids the classic "why won't this match" import bug.

Exporting from Google Sheets

File → Download → Comma-separated values (.csv) produces UTF-8 CSV by default. Don't pick ".tsv" — Distribu only accepts comma separators today.

Semicolon-separated CSVs

Some European locales default to semicolons instead of commas. Distribu's parser expects commas. Re-save the file with comma separators, or use Find → Replace in a text editor.


Next: Customers format.