Skip to main content
Status Sign in

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. 0 is 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 updated that 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

  1. 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.
  2. Small worker against Shopify Admin API, for tenants who already keep authoritative stock inside Shopify. Reads inventory_levels on a cron and forwards to /stock-feed. Same shape, runs on the tenant's own infrastructure or a hosted scheduler.
  3. 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-Key header, 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.