CSV import format
PowersportOS supports bulk catalog import via CSV, used both by the platform admin (ingesting central catalog data from brands and distributors) and by tenant users (importing their own proprietary parts and vehicles). One column shape, downloadable template, year-range syntax that handles model-year ranges with gaps, idempotent re-imports.
Where you import from
- Admin CSV import
- At admin.powersportos.com/import. Used by PowersportOS staff to ingest central-catalog brand data, fitment tables from distributors, multi-brand product packs. Result lands in the central catalog visible to all tenants subscribed to the relevant brands. Full column set including fitments.
- Tenant CSV import (own parts)
- At app.powersportos.com/import. Used by tenant users to add their own proprietary parts and vehicles (the TenantPart / TenantVehicle tables). Same format, scoped to the calling tenant.
- Managed-parts CSV import (data provider / manufacturer / distributor)
- At app.powersportos.com/managed/parts. Used by DATA_PROVIDER, MANUFACTURER, and DISTRIBUTOR tenants to write directly to the central catalog under the brands they hold permission on. Reduced column set (PIM fields + lifecycle + assets, no fitments in v1). Rows whose brand is outside the tenant's permission set skip with a warning instead of erroring the batch. See Data ingest for the broader picture.
Download the template first
Both surfaces have a Download template button that produces a CSV with the current column shape and example rows. Always re-download when starting a fresh import, the column list grows as new fields are added to the schema, and an out-of-date template means missing data.
Required columns
- part_number
- Unique identifier. Used as the match key on re-import, same part_number means update the existing row, not duplicate.
- brand
- Brand string. Free-text but should match a Brand row's name exactly if you want the rich metadata to flow (logo, description, country). Joined by string, not by id.
- name
- Customer-facing product name.
- category_path or category
- Either: a slash-separated category path (
Engine/Air filters) resolved against the central category tree; or the legacy free-text category column. Path is preferred for new data.
Optional fitment columns
Include all three to attach fitments. Each row's fitment fields produce zero, one, or many Fitment rows depending on the year-range syntax.
- make, model, years
- All three together. Each year in the range produces one Fitment row linking this part to that (make, model, year, submodel) vehicle.
- submodel
- Empty = wildcard, fits all submodels of (make, model, year). Filled = fits only that specific submodel.
Year-range syntax
The years column accepts:
- Single year,
2024 - Range,
2020-2024(inclusive both ends, so 2020, 2021, 2022, 2023, 2024) - Range with gap,
"2012-2016,2018-2023"(everything except 2017). Must be quoted in CSV, the comma inside would otherwise be parsed as a column boundary.
Optional data columns
- oem_part_numbers, oem_brands
- Semicolon-separated lists. Each entry becomes one OemCrossReference row. Order matters: the first entry is the primary, surfaced in the legacy
Part.oemPartNumbercache. - description
- HTML or plain text. HTML is sanitised server-side (scripts / iframes / event handlers / javascript: URLs stripped); plain text is wrapped in
<p>blocks automatically. Storage format is sanitised HTML in both cases, matches Shopify's body_html convention. - price, stock
- Initial values. On the admin side these populate the central Part's fields; on the tenant side they populate the TenantPart's. Both are nullable.
- weight_kg, length_cm, width_cm, height_cm
- Logistics dimensions as decimals. Used for shipping calculations and the Shopify product push.
- ean
- Free-text barcode, typically 13-digit EAN-13. 12-digit UPC and 8-digit EAN-8 also accepted.
- country_of_origin
- ISO 3166-1 alpha-2 (SE, TW, US, DE). The importer also accepts common country names (Sweden, Taiwan) and normalises them to the code on write, so legacy CSV files don't break.
- hs_code
- Harmonized System code for customs (6, 8, or 10 digits). Maps to Shopify variant.harmonizedSystemCode at push time.
- position
- Fitment position, LH / RH / FRONT / REAR / FL / FR / RL / RR. Applied to every Fitment generated by this row.
- fitment_notes
- Free-text caveats. Applied to every Fitment generated by this row.
- image_url_1 … image_url_9
- Up to nine pre-hosted image URLs per row. image_url_1 becomes the primary product image; image_url_2..9 fill the gallery in order. Each URL is fetched server-side at import time and stored on PowersportOS object storage. Re-import is additive and dedupes by source URL, so running the same import twice doesn't duplicate gallery entries. The legacy single image_url column still works as a fallback for image_url_1.
- manual_url
- Pre-hosted PDF URL. Same fetch-and-store flow as images. One per row.
- variant_group_name + option1_name + option1_value + variant_position
- Variant grouping. When variant_group_name is set the row joins (or creates) a PartVariantGroup with that name. option1_name is required on the first row that creates the group; option1_value sets this row's value; variant_position is the sort order within the group.
No-fitment rows
Leave make, model, and years all empty for parts that don't fit any vehicle, apparel, branded merchandise, generic tools. The row creates the Part with no Fitment.
Re-importing is idempotent
Running the same CSV twice produces the same end state. Match keys per entity:
- Part
part_number- Vehicle (admin) / TenantVehicle (tenant)
make + model + submodel + year- Fitment
part_id + vehicle_id- OEM reference
part_id + oem_part_number
A re-import updates existing rows; new rows in the CSV create new entities. The result page reports created vs existing counts per type so you can tell at a glance whether your file was a fresh-add or a maintenance update.
Error handling, row-level
A bad row fails with a clear error in the response (missing column, unparseable year, image-fetch failure, etc.) but the rest of the file still imports. The result UI shows row numbers and reasons. A Download failed.csv button returns just the failing rows in the same column shape as your input, fix the errors in your spreadsheet, re-upload, done.
Encoding + delimiter
- Encoding: UTF-8. The importer detects BOM and strips it.
- Delimiter: comma. Semicolons aren't supported as primary delimiter (some European spreadsheet tools default to it, switch your export option, or use Save As with explicit comma).
- Line endings: LF or CRLF, doesn't matter.
- Header row: required. First row of the file must be the column names matching the template.
Brand-subscription auto-activation
On the admin side, when a new Part lands in the central catalog via import, every tenant with an active Brand subscription for the part's brand automatically gets a TenantCatalogItem activation. This is the load-bearing automation behind "subscribe to Alphatrac and forever get all their parts", new SKUs added later by import propagate to subscribers without anyone touching the tenant side.
Tenant-side imports (TenantPart) don't have an analogous auto-activation, they're proprietary to the tenant by definition.