Skip to main content
Status Sign in

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.oemPartNumber cache.
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.