Storefront API: YMM chain
The YMM chain is five endpoints called in cascade. Each populates the next dropdown. This article documents the request shape and response shape of each call, plus the contract invariants you need to honour client-side.
Base URL and auth
All endpoints live under https://api.powersportos.com and require an X-API-Key header carrying the tenant API key. CORS is open (Access-Control-Allow-Origin: *) so a Shopify theme calls these directly from the browser. The key is tenant-scoped and rotation-safe, so it appearing in your theme source is intentional and fine.
The cascade
The customer makes selections top-down. Each selection narrows the next dropdown. The chain looks like:
year → makes(year) → models(makeId, year) → vehicles(modelId, year) → parts(vehicleId)
Only catalog parts the tenant has activated count towards "having fitments", every endpoint pre-filters by that. So if a tenant has only activated CFMOTO parts, /api/t/makes will only return CFMOTO even if the central catalog has dozens of brands.
GET /api/t/years
Returns the descending list of years the tenant has parts available for. No query parameters.
GET /api/t/years
X-API-Key: pst_live_xxx
[2026, 2025, 2024, 2023, ...]
Returns [] if the tenant has no activated parts yet. Cache-Control is set to public, max-age=600.
GET /api/t/makes
Returns the makes available for a given year. Accepts an optional year query parameter.
- year
- Optional integer. When supplied, only makes with at least one fitting part for that year are returned. When omitted, returns every make ever in the tenant's catalog. The Shopify theme always passes year, calling without it gives the wrong dropdown content for a YMM cascade.
GET /api/t/makes?year=2026
X-API-Key: pst_live_xxx
[
{ "id": "cm...", "name": "CFMOTO" },
{ "id": "cm...", "name": "Can-Am" }
]
Returns [] if no makes match. Cache-Control is public, max-age=600.
GET /api/t/models
Returns the models for a (year, make) pair.
- makeId
- Optional. Filter to a single make. Pass the id returned by
/api/t/makes. - year
- Optional integer. Same filter semantics as on
/makes.
GET /api/t/models?makeId=cm...&year=2026
X-API-Key: pst_live_xxx
[
{ "id": "cm...", "name": "CFORCE 1000", "makeId": "cm..." },
{ "id": "cm...", "name": "ZFORCE 950", "makeId": "cm..." }
]
Returns [] if no models match. Cache-Control is public, max-age=600.
GET /api/t/vehicles
Returns the submodel variants for a (year, model) pair. This is where the cascade often ends visually, if there's only one submodel for the chosen year+model, the theme typically auto-selects it; if there are several it renders a fourth "Variant" dropdown.
- modelId
- Optional. Filter to a single model. Pass the id returned by
/api/t/models. - year
- Optional integer. Same filter semantics as elsewhere.
- makeId
- Optional. Rarely useful in practice since
modelIdalready pins the make.
GET /api/t/vehicles?modelId=cm...&year=2026
X-API-Key: pst_live_xxx
[
{
"id": "cm...",
"year": 2026,
"submodel": "GEN 3 MV",
"model": { "id": "cm...", "name": "CFORCE 1000", "make": { "id": "cm...", "name": "CFMOTO" } }
}
] GET /api/t/parts
Returns the parts that fit a specific vehicle (submodel). This is the call that finalises the YMM cascade, the array of partNumber values feeds into the collection-page filter or the fitment-badge check on the product page.
- vehicleId
- Required. The vehicle id returned by
/api/t/vehicles. Missing this parameter is the only condition under which this endpoint returns 400; an unknown id returns[].
GET /api/t/parts?vehicleId=cm...
X-API-Key: pst_live_xxx
[
{
"partNumber": "55716013",
"oemPartNumber": "703501249",
"brand": "Alphatrac",
"name": "Air Filter G3",
"category": "AIR FILTERS",
"imageUrl": "https://hel1.your-objectstorage.com/powersportos-assets/...",
"manualUrl": null,
"price": 24.90,
"stock": 12
}
]
Wildcard fitments (Vehicle rows where submodel is empty) are automatically merged in. So a part with a "fits all submodels of CFORCE 1000 2026" fitment shows up alongside parts with explicit-submodel fitments. The result is deduplicated by partNumber.
price and stock come from the per-tenant TenantCatalogItem and may be null if the tenant hasn't set them. Cache-Control is public, max-age=60, shorter than the catalog-shape endpoints above, because price and stock change more often.
Recommended client-side behaviour
- Always pass
yearwhen calling/makes,/models,/vehicles. The parameter is technically optional but the dropdown content is only correct when scoped to the customer's year. - Treat empty arrays as a valid result, render an empty state, don't surface a "404 / not found" error. The theme uses this invariant heavily.
- Cache the chosen vehicle in
sessionStorageso the fitment context survives across page loads within the tab session. The PowersportOS theme uses the keypowersportos_vehicleholding{ vehicleId, label }. - Re-render fitment badges on variant change. The theme dispatches a
powersportos:variant:changeCustomEvent on the document when the selected Shopify variant changes. Listen for this and re-evaluate fitment against the new SKU.