Skip to content

Commerce Data

The commerce layer reads your store through one consistent, scope-bound contract: normalized orders, a unified customer view, subscriptions, the live product catalog, and connected integrations. Every response is bound to your brand’s connected shop — see Tenant Isolation & Scope.

The commerce, customers, insights, and integrations endpoints require the beta:cdp-commerce permission on your brand and a connected Shopify integration. Both checks fail as a structured 403 with error: "forbidden" and a machine-readable code, never a 500:

Condition403 body
Brand not enrolled in the commerce beta{ "error": "forbidden", "code": "beta_not_enabled" }
No connected Shopify integration / no resolvable shop{ "error": "forbidden", "code": "integration_not_connected" }

Branch on the code, not on the error_description text. Once you clear both, every read is scoped to the shop resolved from your key — you never pass a scope, and you can never widen it.

Reads fall into two serving modes, and it matters because it drives freshness:

DataServed fromFreshness
Orders, unified customers, subscriptions, insightsData synced from your storeReflects the most recent sync from your store
Product catalogRead live from Shopify (proxied)Live, behind a short read-through cache (~30s)
  • Synced reads (orders, unified customers, subscriptions, and all insights) come from a copy of your store data. No live Shopify call happens on these reads, so they stay fast and predictable; their freshness reflects the last sync.
  • The product catalog (list and single product) is read live from Shopify through a proxy. A short read-through cache of about 30 seconds sits in front so bursts of requests don’t hammer Shopify.

Responses are normalized into a stable, Voyage-owned shape. The raw Shopify or provider payload, the integration’s settings, and any credentials are never exposed. Because the contract is decoupled from the underlying provider, it stays the same even if the serving backend changes later — your app depends on the documented fields, not on Shopify’s raw API shape.

  • Money is a dollar-denominated decimal string in the store’s currency, such as "120.00" — never an integer count of cents.
  • Monetary fields count completed orders only (state = "complete"). Orders in any other state — pending, draft, delivered, cancelled, or refunded — are excluded from spend totals, lifetime value, and every insight. Order-listing endpoints still return all states; the completed-only rule applies to the monetary math.

The product catalog is the one live-proxied surface, so it has a few semantics of its own.

  • Cursor pagination. Product pages use an opaque nextCursor (Shopify’s cursor), not offset paging. Follow nextCursor until it is null. See Pagination.

  • Requires products read access. The connected Shopify token must be able to read products — the read_products scope, or write_products, which Shopify treats as granting read access. If it can’t, the proxy returns a clean 403 — never a 500:

    {
    "error": "forbidden",
    "code": "missing_shopify_scope",
    "missing_shopify_scope": "read_products",
    "error_description": "The connected Shopify token is missing read_products."
    }
  • Throttling always surfaces as 429 with Retry-After. The proxy has a dedicated per-brand throttle that protects your Shopify request budget, and it also passes through Shopify’s own throttling after bounded back-off. Either way you get a 429 and a Retry-After header telling you how long to wait — never a 500:

    ResponseMeaning
    429 { "error": "rate_limited", "code": "products_proxy_throttled" }The per-brand proxy throttle tripped. Retry-After: 1.
    429 { "error": "shopify_throttled" }Shopify throttled the request. Honor the Retry-After header.
  • Unknown product id404 { "error": "not_found" }.