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:
| Column | Required | Description |
|---|---|---|
sku | ✅ | Unique identifier for the product within your catalog. Matching key on import. |
name | ✅ | Human-readable product name. |
description | Long-form description. Can be blank. | |
category | Free-text category string. Can be blank. | |
unit | Unit of measure (e.g. each, case, kg). Defaults to each if omitted. | |
price | ✅ | Unit price. Non-negative, up to 2 decimal places. |
stock | ✅ | Current stock on hand. Non-negative whole number. |
isActive | true 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:
| Canonical | Also recognized |
|---|---|
sku | itemcode, item_code, productcode, product_code, itemnumber, item_number, itemid, item_id, partnumber, part_number |
name | productname, product_name, title, itemname, item_name, product |
description | desc, productdescription, product_description, longdescription |
category | cat, productcategory, product_category, group, department |
unit | uom, units, unitofmeasure, unit_of_measure |
price | unitprice, unit_price, listprice, list_price, costprice, cost, sellprice, sell_price, msrp |
stock | quantity, qty, onhand, on_hand, inventory, available, stocklevel, stock_level, stockqty |
isActive | is_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:
eachif omitted or empty
price
- Type: number
- Must be non-negative (≥ 0)
- At most 2 decimal places —
9.99OK,9.999rejected - 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:
trueif omitted or empty
Case-insensitive string values accepted:
| Value | Parsed as |
|---|---|
true, 1, yes, y, active | true |
false, 0, no, n, inactive | false |
| anything else | true (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:
categoryascending, thennameascending - Blank fields serialized as empty string (not
null) priceformatted to 2 decimal places (9.99, not9.9)isActiveserialized astrue/falsestockserialized 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.
