Skip to content

Build1 publisher2 min readPublished

Shopify's bulk product feed omits the four variant fields an inventory roll-up needs

Two unauthenticated endpoints on the same store return different variant shapes. Getting the four inventory fields costs one request per product, and one of them answers 999,999 for goods nobody tracks.

The Engineer · Build desk

Illustration accompanying Shopify's bulk product feed omits the four variant fields an inventory roll-up needs

What happened

  • The per-product route returns the same variant ids with all four fields filled in, matched 13 for 13 on an Allbirds shoe, including a real UPC and inventory_quantity 0 on a sold-out size.
  • An Allbirds digital add-on called Free Returns Coverage comes back with inventory_management null and inventory_quantity 999999, the value Shopify uses to mean stock is not tracked.
  • Adding inventory_quantity across one catalogue's variants yielded 1,981,856 units, a figure the post calls off by roughly six orders of magnitude.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Anyone who needs barcodes or stock levels pays one storefront request per product on top of the bulk pages, so a single page of 250 products becomes 251 calls against the merchant's domain.
  • exposure An integration that coerces the absent key to 0 makes every merchant who withholds stock look sold out everywhere, and the consumer downstream cannot tell that state from a genuine zero.
  • constraint A product promising real inventory counts across Shopify stores stays conditional on each merchant's own theme settings, so coverage is a per-store fact the integrator does not control.
  • decision Teams now have to decide separately what a variant may display and what it may contribute to a total, with inventory_management as the gate on the second.

The four fields sit behind a fanout. One bulk page returns up to 250 products [3]. Recovering inventory_quantity, inventory_management, inventory_policy and barcode for those products takes one call to /products/<handle>.json each [6], so every page of the catalog costs 250 more requests [3]. Neither endpoint asks for a key [1][6]. Both answer on the storefront domain, whether that is a myshopify.com backend or the merchant's own DNS [2].

The keys are absent from the bulk variants, not null and not empty strings [4]. So `if (variant.barcode)` evaluates false on every variant of every store, and a test like that says Shopify stores do not publish barcodes [5]. But the barcodes exist: the post read a real UPC, 196942208243, off an Allbirds variant [7]. A missing field should map to null and never to 0, because 0 is a real value here and means sold out [11].

Shopify writes 999,999 into inventory_quantity to mean the item is not tracked [12]. Summing `inventory_quantity ?? 0` across one real catalog produced 1,981,856 units [13], which the post describes as "off by roughly six orders of magnitude" [13]. The injected value is on that order in absolute terms, but measured against the printed total it is 50.5 percent of the number [1]. Only one such variant can be inside that total, since two would sum to 1,999,998 on their own [2].

The correction is one predicate before the reduce: keep only variants with a truthy inventory_management, which is "shopify" or the name of a fulfillment service [14]. Raw per-variant values can still be exposed verbatim; they just never enter an aggregate [14].

Whether any of it is populated is a merchant and theme setting, independent of which endpoint you call [9]. brooklinen.com fills barcode with an internal SKU, COR-T1, and publishes no quantity; rothys.com publishes barcodes and no quantities [10]. A populated barcode is therefore not proof of a GTIN, and a validator has to check the shape of the string.

Currency is not on the product payload; it comes from /meta.json, fetched once per store and attached to every item from that store [15]. There is no on-sale flag either, though compare_at_price set higher than price marks a discounted variant, and min price against min compare-at rolls that up per product [16]. Merchants can also switch the feed off, because Shopify allows opting out [17]. The per-product .js and .json routes agreed on every field the author compared [8].

What to watch

  • Whether Shopify documents 999,999 as the untracked sentinel or changes the value, which would turn the gating predicate into a contract.
  • How many merchants take the opt-out and disable /products.json, since that sets the ceiling on any catalogue-wide scrape.
  • Whether the per-product .js and .json routes keep agreeing field for field as themes change.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories