Skip to main content
Status Sign in

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 modelId already 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 year when 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 sessionStorage so the fitment context survives across page loads within the tab session. The PowersportOS theme uses the key powersportos_vehicle holding { vehicleId, label }.
  • Re-render fitment badges on variant change. The theme dispatches a powersportos:variant:change CustomEvent on the document when the selected Shopify variant changes. Listen for this and re-evaluate fitment against the new SKU.