Skip to main content
Status Sign in

Storefront API: product detail

Once a customer is on a product page, the storefront often needs richer content than the cascade returned, a manual PDF link, the OEM cross-reference, the canonical category, or a long description. The single-part lookup endpoint serves that. There's also a flat-tree categories endpoint for navigation.

GET /api/t/parts/:partNumber

Returns a single part by part number, scoped to what the tenant has activated. The path parameter is URL-encoded, if your SKU contains slashes, dots, or spaces, encode them client-side before fetching.

GET /api/t/parts/55716013
X-API-Key: pst_live_xxx

{
  "partNumber": "55716013",
  "oemPartNumber": "703501249",
  "brand": "Alphatrac",
  "name": "Air Filter G3",
  "category": "AIR FILTERS",
  "description": "<p>Heavy-duty paper element designed for the Can-Am Outlander G3 platform...</p>",
  "imageUrl": "https://hel1.your-objectstorage.com/powersportos-assets/...",
  "manualUrl": "https://hel1.your-objectstorage.com/powersportos-assets/...",
  "weightKg": 0.45,
  "price": 24.90,
  "stock": 12
}

Response fields

partNumber
Canonical SKU. Always the central catalog's part number, even if you requested via an alias, this field returns the canonical.
oemPartNumber
OEM cross-reference number (or null). Useful for 'Replaces OEM:' messaging on the product page.
brand
Brand name as a string.
name
Product name.
category
Free-text category string on the part itself. For structured navigation use /api/t/categories instead.
description
Long-form description. Stored as sanitized HTML, render with set:html or equivalent, do not double-escape.
imageUrl
Primary product image URL (or null). Hosted on PowersportOS object storage.
manualUrl
Owner's manual / install instructions PDF URL (or null). Driver for the manual-link section in the PowersportOS Shopify theme.
weightKg
Part weight as a decimal number of kilograms (or null).
price
Per-tenant retail price (or null). Comes from the tenant's TenantCatalogItem row, not a central price, different tenants can have different prices for the same central part.
stock
Per-tenant stock count (or null). Same source as price, comes from TenantCatalogItem.

SKU alias resolution

The endpoint does two passes:

  1. Direct match on Part.partNumber, the canonical SKU.
  2. Alias fallback, if the direct match misses, look the value up in the tenant's TenantSkuAlias table. If a row exists pointing at a central part the tenant has activated, return that part.

This means a storefront URL like /products/LP-12345 can resolve to the correct central part even though the canonical SKU is the manufacturer's 55716013, the tenant just needs to register LP-12345 as an alias in their portal. The response always returns the canonical partNumber, never the alias.

Not-found behaviour

If neither pass resolves to an active TenantCatalogItem, the endpoint returns:

HTTP 404 Not Found
{ "error": "Not found" }

This is the one endpoint in the /api/t/* surface that returns 404 for missing data, the list endpoints always return []. Treat 404 here as "this SKU is not in the tenant's catalog" and render the page without the enriched fields (the PowersportOS theme silently hides the manual-link section, for example).

Caching

Cache-Control is public, max-age=60, shorter than the YMM catalog endpoints because the response carries price and stock, which change more often than catalog shape.

GET /api/t/categories

Returns the full part-category tree as a flat array. Order is parent-first, then by the order field, then alphabetical. Build the tree client-side from the parentId pointers.

GET /api/t/categories
X-API-Key: pst_live_xxx

[
  { "id": "cm...", "name": "Engine", "slug": "engine", "parentId": null, "order": 1 },
  { "id": "cm...", "name": "Air filters", "slug": "air-filters", "parentId": "cm...", "order": 1 },
  { "id": "cm...", "name": "Oil filters", "slug": "oil-filters", "parentId": "cm...", "order": 2 },
  ...
]

Cache-Control is public, max-age=300. The tree is global (shared across tenants), not per-tenant, so it's safe to cache aggressively.

Recommended client-side behaviour

  • 404 means "hide enrichment", not "broken page". A product page should render fine with just the Shopify-side data when the API misses; the API just adds extras when they're available.
  • Re-fetch on variant change. The PowersportOS Shopify theme dispatches a powersportos:variant:change CustomEvent when a variant selector changes the SKU. Subscribe to it and call /api/t/parts/:partNumber again with the new SKU.
  • Prefer Shopify metafields when present. The PowersportOS theme checks the custom.manual_pdf metafield first and only falls back to the API if it's missing, that pattern saves a round trip and works offline of the API.
  • Don't double-escape the description. It's already sanitized HTML, so rendering with set:html (Astro) or v-html (Vue) or dangerouslySetInnerHTML (React) is the correct approach. Plain-text rendering will leave literal HTML tags visible.