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/categoriesinstead. - description
- Long-form description. Stored as sanitized HTML, render with
set:htmlor 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:
- Direct match on
Part.partNumber, the canonical SKU. - Alias fallback, if the direct match misses, look the value up in the tenant's
TenantSkuAliastable. 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:changeCustomEvent when a variant selector changes the SKU. Subscribe to it and call/api/t/parts/:partNumberagain with the new SKU. - Prefer Shopify metafields when present. The PowersportOS theme checks the
custom.manual_pdfmetafield 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) orv-html(Vue) ordangerouslySetInnerHTML(React) is the correct approach. Plain-text rendering will leave literal HTML tags visible.