Integrations API: stock & location-stock
The integrations surface is the primary inbound path for stock data into PowersportOS. A tenant's POS / ERP / WMS, or a small worker reading Shopify's Admin API on a schedule, posts bulk arrays to these endpoints. Idempotent, X-API-Key authenticated, server-to-server.
When to use which surface
PowersportOS exposes two classes of API. Pick by where the call comes from:
- /api/t/*
- Browser-callable storefront endpoints. Read-only. Used by the bundled PowersportOS Shopify theme (or any other storefront) to render widgets. Documented in the other Storefront API articles.
- /api/integrations/*
- Server-callable bulk-update endpoints. Idempotent. Used by a cron job in the tenant's POS / ERP / WMS, or a small worker reading Shopify's Admin API, to push data into PowersportOS. This article.
Base URL and auth
All integration endpoints live under https://api.powersportos.com/api/integrations/ and authenticate via the X-API-Key header carrying the tenant API key, same key as the storefront surface uses. CORS is open so manual testing from a script works, but these endpoints are not designed for browser callers.
POST /api/integrations/stock-feed
Bulk-update stock for the authenticated tenant by part number. Tries to update TenantCatalogItem first (central-catalog parts the tenant carries), falls back to TenantPart (the tenant's own SKUs), and falls back finally to a SKU alias lookup against the tenant's TenantSkuAlias table. Anything that still doesn't match comes back in the response as "not found" so you can fix your data.
Request
POST /api/integrations/stock-feed
X-API-Key: pst_live_xxx
Content-Type: application/json
[
{ "partNumber": "55716013", "stock": 12 },
{ "partNumber": "55716067", "stock": 0 },
{ "partNumber": "LP-12345", "stock": 4 }
] - partNumber
- The SKU to update. Matched against canonical part numbers first, then against the tenant's alias table.
- stock
- Non-negative integer. Floats are floored, negative values rejected.
0is a valid update (clears stock).
Response
HTTP 200 OK
{
"received": 3,
"updated": 2,
"resolvedViaAlias": 1,
"notFound": 0,
"notFoundSample": []
} - received
- Count of valid rows in the payload (after dropping malformed entries).
- updated
- Count of rows that successfully wrote a new stock value.
- resolvedViaAlias
- Subset of
updatedthat needed alias-table resolution. Useful to monitor, if this number grows unbounded, your POS is pushing aliases more than canonical SKUs and an alias-table cleanup may be due. - notFound
- Count of partNumbers that didn't match anything. Surface this to your operator so they can register an alias or onboard the SKU.
- notFoundSample
- Up to 20 of the unmatched partNumbers, verbatim. For diagnostics, not the full list.
Idempotency
Sending the same payload twice produces the same end state, the endpoint upserts rather than appending. This is intentional: schedule a cron without worrying about retry logic. If a push gets cut off, just send it again.
How it's typically called
- Cron from the tenant's ERP or POS. The primary path: the customer's own system reads its current stock state and posts deltas (or the full state, idempotently) to this endpoint every few minutes. Hands-on onboarding wires this up.
- Small worker against Shopify Admin API, for tenants who already keep authoritative stock inside Shopify. Reads
inventory_levelson a cron and forwards to/stock-feed. Same shape, runs on the tenant's own infrastructure or a hosted scheduler. - Manual stock entry in the portal, fallback when neither of the above applies. Already exists in the My Catalog page.
CSV upload is intentionally not supported on this endpoint, stock changes too fast for human-curated uploads to be useful.
POST /api/integrations/location-stock
Bulk-update per-location stock for a Retail tenant. Each row identifies a specific dealer (= physical store) on the tenant and a partNumber/stock value. Idempotent; upserts on (dealerId, partNumber).
Request
POST /api/integrations/location-stock
X-API-Key: pst_live_xxx
Content-Type: application/json
[
{ "dealerId": "cm...", "partNumber": "55716013", "stock": 5 },
{ "dealerId": "cm...", "partNumber": "55716067", "stock": 0 }
] - dealerId
- The id of a Dealer row owned by the calling tenant. Rows for dealers not owned by this tenant are skipped and returned in
invalidDealerIds, defense-in-depth against pushing stock to someone else's locations. - partNumber
- The SKU to update. No alias resolution on this endpoint (yet), pass canonical SKUs.
- stock
- Non-negative integer. Same validation as
/stock-feed.
Response
HTTP 200 OK
{
"received": 2,
"updated": 2,
"invalidDealerIds": []
} - received
- Count of valid rows in the payload.
- updated
- Count of rows that successfully wrote a new stock value.
- invalidDealerIds
- Up to 20 dealerIds from the payload that didn't belong to the calling tenant. Empty array is the normal case.
Where the data ends up
Rows land in the DealerStock table, keyed on (dealerId, partNumber). The same rows feed the /api/t/dealers/stock endpoint, which is what the manufacturer's storefront calls to render "find this part nearby". So pushes here become visible to the storefront within the /dealers/stock cache window (60 s).
Error responses
- 400
- Malformed JSON, body is not an array, or zero valid rows after filtering. Body:
{ "error": "…" }. - 401
- Missing or invalid
X-API-Keyheader, or the tenant is inactive. - 403
- Tenant has an allowed-domain restriction configured and the request came from a browser with a disallowed Origin. Server-to-server callers don't send Origin and aren't affected.
Rate limits and batching
- Batch size: there's no hard cap published yet, but ≤ 1000 rows per request is the practical sweet spot. Larger payloads work but the response gets slower.
- Throttle: one request per second is plenty if you're pushing continuously. Don't fire dozens in parallel, the database write side is the bottleneck.
- Idempotent retry: on network error, just retry the same payload. The endpoint will not double-count anything.
Formal rate-limit enforcement comes before public general availability.
Activity tracking
Each successful call records timestamp and counters on the tenant for portal-side "last sync" status display. The customer can see when their integration last ran without leaving their PowersportOS portal, useful when their POS goes silent.