Skip to main content
Status Sign in

Storefront API: dealers & stock locator

Two geographical widgets share these endpoints: a store-locator that maps the tenant's dealers, and a 'find this part nearby' widget that aggregates stock across resellers. They both consume /api/t/config for the Mapbox token.

GET /api/t/config

Per-tenant public configuration. Today it carries the Mapbox token a tenant has set in their portal. Future integrations get their own top-level keys, but the shape stays flat, easy to read in a Shopify Liquid template.

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

{ "mapboxToken": "pk.eyJ1Ijoi..." }

Returns "" for mapboxToken if the tenant hasn't configured one. Cache-Control is public, max-age=300. The PowersportOS Shopify theme caches the response in sessionStorage under powersportos_config and invalidates if mapboxToken is empty.

GET /api/t/dealers

Returns the full list of active dealers for the tenant. Used by the store-locator widget to render pins on the map plus the side-list of locations.

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

[
  {
    "name": "Acme Powersports",
    "address": "Storgatan 12",
    "city": "Stockholm",
    "postalCode": "11122",
    "country": "Sweden",
    "phone": "+46812345678",
    "website": "https://acme.example/",
    "lat": 59.3293,
    "lng": 18.0686
  }
]
phone / website
May be empty strings, treat empty as 'not provided', skip rendering the field.
lat / lng
Decimal numbers. If null, the dealer should appear in the list but not on the map. PowersportOS geocodes addresses server-side on create/update using Nominatim, so this is rarely null in practice.
country
Full country name (e.g. 'Sweden', not 'SE'). Drives the country-filter dropdown in the store-locator. The set of countries is whatever's in the data, there's no hard-coded master list.

Sorted by country then name. Returns [] if no dealers exist. Cache-Control is public, max-age=300.

GET /api/t/dealers/stock

Returns the nearest dealers that have a specific part in stock. This is the engine behind a "Find this part at a reseller near you" widget on a manufacturer's product page, given the customer's location and the variant SKU, return resellers (or own retail locations) sorted by distance, with stock badges.

partNumber
Required. The SKU to look up. Returns 400 if missing.
lat / lng
Optional decimal-number pair. Both must be supplied together to enable distance sort. If omitted, results sort alphabetically by city instead.
limit
Optional integer, default 10, clamped to 1–50.
GET /api/t/dealers/stock?partNumber=55716013&lat=59.3293&lng=18.0686&limit=5
X-API-Key: pst_live_xxx

[
  {
    "name": "Acme Powersports",
    "address": "Storgatan 12",
    "city": "Stockholm",
    "postalCode": "11122",
    "country": "Sweden",
    "phone": "+46812345678",
    "website": "https://acme.example/",
    "lat": 59.3293,
    "lng": 18.0686,
    "distanceKm": 12.3,
    "stock": 5,
    "level": "in_stock"
  }
]

Response fields

level
Always present. One of "in_stock", "low", "out". Drives the badge colour client-side. Bucketed by the backend: ≤ 0 → out, 1–3 → low, ≥ 4 → in_stock.
stock
Numeric stock count, only present when the reseller has chosen EXACT precision in their stock-sharing settings. Most resellers use LEVEL precision, in which case stock is absent and only level is meaningful. Check typeof stock === 'number' before rendering a count.
distanceKm
Decimal kilometres from the supplied lat/lng to the dealer, rounded to one decimal. null if lat/lng weren't supplied or the dealer has no coordinates.
phone / website
Same empty-string-means-missing convention as /dealers.

Two paths, one response

The endpoint serves two flows with identical response shapes:

  • Manufacturer-tenant flow: the dealers under the calling tenant have linkedTenantId set, pointing at the reseller's PowersportOS tenant. Stock is read from the linked reseller's catalog, gated by an approved TenantStockShare consent row pointing back at the calling manufacturer. Precision (EXACT vs LEVEL) comes from the share row.
  • Retail-tenant flow: the dealers under the calling tenant are the tenant's own physical stores (no linkedTenantId). Stock is read from DealerStock rows attached to each dealer. No consent gate, it's the tenant's own data. Always EXACT precision.

A single tenant can have both flavors of dealer in their catalog (a manufacturer that also runs its own retail outlets, for example). Both contribute to the response transparently.

Privacy and opt-in

For the manufacturer flow, only resellers who've actively opted in (approved a TenantStockShare row in their portal) are included. Resellers who haven't opted in are silently filtered out, they don't appear as "no stock", they just don't appear at all. Resellers can also choose LEVEL precision so the actual count never leaves their system; only the in/low/out bucket is exposed.

Caching

Cache-Control is public, max-age=60. Stock changes more often than the catalog or dealer list, so the TTL is shorter. The PowersportOS Shopify theme caches geocoded postcodes in sessionStorage (key powersportos_geo_<postal>_<country>) so the customer doesn't pay the Mapbox round-trip on every variant change.

Recommended client-side behaviour

  • Geocode postcodes via Mapbox using the token from /api/t/config. Don't ship your own token, the tenant's token has the correct URL-origin restrictions configured.
  • Cache the customer's resolved lat/lng in sessionStorage so they don't have to re-approve geolocation or re-enter their postcode on every product page. The PowersportOS theme uses powersportos_user_location.
  • Re-fetch on variant change. Listen for the powersportos:variant:change CustomEvent and call /dealers/stock again with the new SKU.
  • Branch the badge on level, not on stock. Most responses don't include stock, only level is guaranteed.
  • Empty array is normal. Render an empty state ("No resellers in your area carry this part") rather than treating it as an error.