GET /v1/products/{id}
The primary product-lookup endpoint. Resolves any UPC, EAN, ISBN, GTIN, ASIN, Walmart item_id, or Best Buy SKU / product id and returns the full normalized product including price history, marketplace fees, and Bucket-1 facts.
Request
GET https://api.retailerapi.com/v1/products/{id}
Authorization: Bearer rk_live_...Path parameters
| Name | Required | Description |
|---|---|---|
id | yes | UPC-12, EAN-13, ISBN-10/13, GTIN-14, Amazon ASIN (B0XXXXXXXX), Walmart item_id (6-12 digits), Best Buy SKU (7-8 digits) or Best Buy product id (10 characters, e.g. JXJVXGFQKQ), or any retailer PDP URL. URLs must be URL-encoded when passed in the path. Cost is 1 token regardless of identifier type.Best Buy identifiers. A Best Buy product id is unambiguous and resolves directly. A Best Buy SKU is 7-8 digits, which overlaps the Walmart item_id space, so it is tried only after the Walmart catalog returns nothing — a numeric identifier that Walmart carries always resolves as Walmart. Both come back with _meta.source: "bestbuy" or with Best Buy named as the price source in _meta. Pass them bare: Best Buy identifiers are not accepted alongside ?retailer=, and since force_refresh is only valid with ?retailer=, they cannot be force-refreshed either. Use a Best Buy PDP URL if you need ?retailer=bestbuy&force_refresh=true.Walmart item ids and ISBN-10. With ?retailer=walmart, a bare 10-digit number is read as a Walmart item_id, even when its check digit would also make it a valid ISBN-10 (about 1 in 11 ten-digit item ids do). Pass format=ISBN to look up a book. With ?retailer=walmart and a Walmart item_id, _meta.title_source is walmart, and an id that Walmart redirects to a different item returns 404 not_found (reason: item_id_redirected) instead of the other item.URL submission works on any retailer that ships Product structured data. The 7 vetted retailers (Amazon, Walmart, Target, Home Depot, Lowe's, Best Buy, eBay) have dedicated by-URL parsers and return the richest shape. The ~2 million Shopify-hosted stores work via Shopify's public /products/<handle>.json endpoint. Any other retailer with Product JSON-LD or OpenGraph product tags in its initial HTML works through the generic-URL path.Examples:
|
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
format | string | auto-detect | One of UPC, EAN, ISBN, GTIN, ASIN, item_id. Set when input could be ambiguous. ASIN routes through Amazon-first; include_offers_reviews returns the full Amazon per-seller offer list (AOD) for ASIN / ?retailer=amazon. include_offers_history stays Walmart-only. On an ASIN lookup, read asin_match before pricing against the response — see below. |
include_offers_reviews | boolean | false | Live marketplace sellers + review summary (rating, sentiment, verified-purchase counts) + a monthly review trend. Individual review bodies (reviews.top_reviews) are included when the source exposes them — Walmart currently returns aggregates only. +5 tokens. |
include_offers | boolean | false | Live marketplace sellers ONLY (no separate reviews block — the review summary already rides the base call for free). Use this instead of include_offers_reviews when you only need the seller list. +5 tokens. |
include_cross_retailer | boolean | false | Include the cross_retailer block — a map keyed by retailer slug of cached per-retailer cells (price, in_stock, Bucket-1 fields). +2 tokens. Served from our cache; when fewer than three stores hold an answer, the call also reads the missing stores itself, inside its own time budget. Each cell's read says whether that store was read in this call, read earlier (with when), or not read and why. A store that was not read is never an out-of-stock answer. |
include_seller_context | boolean | false | Live seller-side state under cross_retailer.<retailer>.seller_context.restricted and .wfs_eligibility, plus top-level seller_context.restricted aggregation. +3 tokens. Marketplace fees (referral_fee_usd, wfs_fee_usd) are FREE in the base call — this flag adds the live-state fields only. Applies to marketplace retailers; first-party retailers emit seller_context: null. |
include_offers_history | boolean | false | Historical marketplace offers — the price/seller archive for this item, each entry stamped with an observed_at snapshot time (plus was_price). +5 tokens. Walmart only for now. Distinct from the free include_history (price-history shape control) — this is the Keepa-style marketplace-offers archive. Returns an empty array when we have no archived offers for the item yet. |
include_price_series | boolean | false | The raw day-by-day price series under price_history.series — one bar per day carrying open, close, low, high. +10 tokens. Walmart only. The 30/60/90/180-day aggregates under stats.windows are FREE in the base call; this flag buys the daily bars underneath them. Charged only when we actually hold bars — an item with no history costs nothing extra. |
identifier_match | any | confirmed | any | Exact-match filter over the cross_retailer block. Free — it filters data include_cross_retailer already paid for. any returns every cached retailer row, each labelled with how it was matched under cross_retailer.<retailer>.identity. confirmed returns ONLY rows the retailer's own record ties to the identifier you searched — rows we matched by a title/keyword guess, and rows where the retailer publishes a different barcode, are dropped. An empty cross_retailer object is a real answer: we hold nothing for that product we can vouch for. Any other value returns 400. |
retailer | string | (unified) | Anchor the response to a specific retailer's data. Slug format: lowercase alphanumeric only, 2-30 chars, no TLD or separators (homedepot not home-depot). Vetted retailers have custom parsers; other slugs route through a self-extending WebFetch waterfall. Cost: 1 token flat. |
force_refresh | boolean | false | Bypass cache and force a fresh scrape of the retailer specified by ?retailer=<retailer>. Only valid with ?retailer=<retailer> — passing force_refresh alone returns 400. No additional token cost beyond the retailer surcharge. This is the ONLY way to force fresh data from the API. |
allow_stored | boolean | false | With force_refresh=true. When a time-limited live read cannot finish in time, answer from the last stored record instead of a retryable 503 (reason: live_read_unavailable). Such an answer is labeled _meta.price_read: "stored"; _meta.price_read_at gives when that stored price was read, never the time of the request, and _meta.price_age_s its age in seconds. With include_offers=true, a seller list that could not be read live in time is answered from the last stored list the same way, labeled _meta.offers_read: "stored", _meta.offers_read_at and _meta.offers_age_s. A live answer carries _meta.price_read: "live", or "recent_live" when a live read of the same item from the last 3 minutes was served. No extra token cost. |
zip | string | 90210 | US ZIP code to use as the shopper's location, sent as a 5-digit string (keep leading zeros, e.g. 02134). The format is not checked. Some retailers set delivery dates, store price and stock by location. When a lookup anchored to a retailer (?retailer=<retailer> or a retailer product URL) is answered for a location, _meta.zip in the response is the ZIP that was used: yours, or 90210 when you send none. A response without _meta.zip was not answered for a location, so the ZIP did not change it. Lookups without ?retailer= or a product URL are not location-specific, and ?retailer=walmart lookups (with or without force_refresh) and Amazon lookups do not use the ZIP. zip=_national asks for the retailer's answer with no location set; _meta.zip is then null. No extra token cost. |
Cost summary
| Call shape | Tokens | What you get |
|---|---|---|
| Base lookup | 1 | title, brand, image, price, identifiers, weight, dimensions, MSRP, description, categories, 30/60/90/180-day price aggregates, retailer_links, Bucket-1 facts (sold_tag, estimated_sales, is_best_seller, pack_count, hazmat), computed marketplace fees (referral_fee_usd, wfs_fee_usd) |
+ include_cross_retailer | 3 | + cross_retailer block (a cell for every retailer, from our cache plus in-call reads when fewer than three stores hold an answer) |
+ include_seller_context | 4 | + live restriction + WFS eligibility state |
+ include_offers_reviews | 6 | + live marketplace sellers + review summary (sentiment + monthly trend) |
+ include_offers_history | 6 | + historical marketplace offers (price/seller archive, Walmart) |
+ include_price_series | 11 | + the raw day-by-day price bars under price_history.series (Walmart). The aggregates are already free in the base call. |
| All three flags | 11 | Full reseller view (cross_retailer + seller_context + offers_reviews) |
?retailer=<r> | 1 | Anchor to a specific retailer (replaces base call shape) |
?retailer=<r>&force_refresh=true | 1 | Same as above but bypasses cache + scrapes fresh. Only way to force fresh data from the API. |
Marketplace fees (referral_fee_usd, wfs_fee_usd) are computed from public rate cards and are always free in the base call — same model as Keepa's referralFeePercentage. include_seller_context only pays for the live-state fields (restricted, wfs_eligibility).
A Walmart fee that is null says why in _meta when we know: _meta.wfs_fee_reason: "over_wfs_size_limit" means the item's recorded longest side is over 120 inches, past every size Walmart's WFS fee page prices, so we do not estimate its WFS fee. The size comes from the catalog's dimensions, which are sometimes wrong (a label printer listed at its 118-inch maximum label length, for example), so check the dimensions before you treat the item as unshippable. A big and bulky item (over 150 lb, longest side over 108 up to 120 inches, or length plus girth over 165 inches) is priced as Walmart's page prices it: $155 plus $0.80 per pound over 90 lb. A wfs_fee_usd that is null for any other cause (no weight, no price, over 500 lb) carries no reason key. _meta.referral_fee_reason and _meta.wfs_fee_reason set to "price_withheld" mean we withheld the item's price because we could not confirm it, so both fees, which depend on it, are null too.
The reason in Walmart's restriction and wfs_eligibility (top level and cross_retailer.walmart.seller_context) and the top-level seller_context.restricted.primary_reason is one of our own generic messages, or null. It never repeats the marketplace's own wording. A null reason says nothing about the flag: read flag and enabled for the state.
eBay's referral_fee_usd is eBay's final value fee for US sellers without an eBay Store, at the rate eBay sets for the item's eBay category (eBay's published schedule, checked November 10, 2026). For example, Books & Magazines, Movies & TV and Music are 15.3% of the sale up to $7,500 and 2.35% of the portion above. An item with no category, or a category eBay does not price on its own, gets eBay's Most-categories rate: 13.6% up to $7,500 and 2.35% of the portion above. It is computed on the item price. eBay also charges the fee on buyer-paid shipping, handling and sales tax; those are not added, because the cell does not know them. eBay's separate per-order fee ($0.30 for an order of $10.00 or less, $0.40 above) is not included.
Response
200 OK with a JSON body. Fields below show a UPC lookup with all opt-in flags enabled.
{
"item_id": "19667262713",
"upc": "086279195869",
"ean": null,
"isbn": null,
"gtin": "00086279195869",
"asin": null,
"mpn": "EP-PC8",
"model": "EP-PC8",
"title": "12-in-1 Electric Pressure Cooker 6 QT...",
"brand": "Cooks Essentials",
"image_url": "https://i5.walmartimages.com/...",
"description": "Cook everything from rice to roast...",
"current_price": 100.56,
"buybox_price": 100.56,
"secondary_offer_price": 99.50,
"msrp": 149.99,
"weight_lbs": 12.5,
"weight_assembled_lbs": 12.5,
"weight_package_lbs": 14.2,
"dimensions": { "height_in": 13.2, "width_in": 13.0, "length_in": 12.8 },
"dimensions_assembled": { "height_in": 13.2, "width_in": 13.0, "length_in": 12.8 },
"dimensions_package": { "height_in": 15.0, "width_in": 14.5, "length_in": 14.0 },
"offers_count": 4,
"total_offers": 6,
"delivery_date": "2026-06-23",
"fulfillment": {
"shipping": { "available": true, "cost": 0, "free": true, "date": "2026-06-23" },
"delivery": { "available": false, "cost": null, "date": null },
"pickup": { "available": true, "cost": 0, "date": "2026-06-21" }
},
"seller_name": "Walmart",
"seller_id": "F55CDC31AB754BB68FE0B39041159D63",
"average_rating": 4.3,
"review_count": 1247,
"categories": ["Home", "Appliances", "Pressure Cookers"],
"specs": [
{ "name": "Model", "value": "EP-PC8" },
{ "name": "Capacity", "value": "6 quart" },
{ "name": "Color", "value": "Stainless Steel" }
],
"walmart_url": "https://www.walmart.com/ip/...",
"in_stock": true,
"condition": "new",
"seller_type": "first_party",
"seller_rating": null,
/* Walmart marketplace economics — FREE in the base call (Keepa parity), derived
from the record. Platform/item-wide, not seller-specific. */
"referral_fee_usd": 15.08,
"wfs_fee_usd": 11.95,
/* reason: one of our own generic messages, or null (see above). */
"restriction": { "flag": false, "reason": null },
"wfs_eligibility": { "enabled": true, "reason": null },
/* top-level UPC-scoped restriction view — present when include_seller_context=true */
"seller_context": {
"restricted": { "any": false, "retailers": [], "primary_reason": null }
},
/* Price history, from our daily-bar archive — one bar per day.
days_covered is how many days we actually hold for this item, so every
figure below can be read against the evidence behind it. */
"price_history": {
"first_observed_date": "2026-07-01",
"last_observed_date": "2026-08-02",
"days_covered": 29,
/* series is present only with include_price_series=true (+10). */
"series": [
{ "date": "2026-08-02", "open": 99.99, "close": 99.99, "low": 99.99, "high": 99.99, "observations": 1 },
{ "date": "2026-08-01", "open": 104.99, "close": 99.99, "low": 99.99, "high": 104.99, "observations": 3 }
]
},
/* Aggregated price stats — FREE in the base call. One entry per window.
A window we hold no bars for is null, never a fabricated number, and
days_covered states the real depth behind each row. A Walmart item we
hold rank or badge observations for but no priced day still gets stats:
every window null, and no price_history. */
"stats": {
"current_price": 99.99,
"windows": {
"30d": { "low": 89.99, "high": 119.99, "average": 101.42, "days_covered": 28, "observation_count": 28 },
"60d": { "low": 89.99, "high": 124.99, "average": 103.10, "days_covered": 29, "observation_count": 29 },
"90d": { "low": 89.99, "high": 124.99, "average": 103.10, "days_covered": 29, "observation_count": 29 },
"180d": { "low": 89.99, "high": 124.99, "average": 103.10, "days_covered": 29, "observation_count": 29 }
},
/* Walmart purchase-badge demand signals over the daily bars dated in the
last 30 UTC days (today included, so up to 31 calendar days). FREE.
Present only when we hold bars in that window. Walmart badges are
thresholds ("1K+ bought since yesterday"), so every number is a FLOOR.
observed_days_30d counts every day we hold a bar, including days seen
by a source that does not read badges, so badge_days_30d divided by it
understates how often the badge shows. A day without a badge means
"not shown when we looked", never zero sales. */
"demand_30d": {
"observed_days_30d": 21, /* days we hold a bar for */
"badge_days_30d": 9, /* of those, days with an "N+ bought since yesterday" badge */
"badge_floor_units_30d": 9000, /* sum of those daily thresholds: units sold on the badge days alone, at least */
"monthly_badge_floor_max_30d": 20000, /* highest "N+ bought in past month" threshold seen, or null */
"last_badge_date": "2026-10-02" /* latest day with an "N+ bought since yesterday" badge, or null */
},
/* Walmart category rank history, one entry per listing (best-seller
category page) the item holds a rank in, over the daily bars dated in
the last 30 UTC days (today included, so up to 31 calendar days; the
7-day figures likewise cover up to 8). FREE. Present only when at least
one bar in that window holds a rank. A rank is a POSITION in the
listing, so lower is better and "best" is the lowest number. A rank is
recorded only on days we checked the listing, so days_seen_* is the
evidence behind every figure: a day with no rank is not known to mean
the item left the listing. */
"rank_history": {
"nodes_tracked": 3, /* listings the item held a rank in during the window */
"nodes": [ /* up to 10, most days seen first, then best rank */
{
"listing": "id:4125_546956_1208159_1079385", /* stable key of the listing */
"node": "4125_546956_1208159_1079385", /* Walmart's browse id, or null for a /shop/ page */
"path": "Sports & Outdoors > Boats & Marine > Boat covers",
"latest_rank": 41, /* newest day's closing position */
"latest_date": "2026-10-23",
"best_7d": 38, /* best position reached in the last 7 days, or null if not seen */
"median_7d": 40,
"days_seen_7d": 2,
"best_30d": 33,
"median_30d": 41,
"days_seen_30d": 5
}
]
}
},
/* free for barcode lookups; absent for item_id */
"retailer_links": [
{ "retailer": "walmart", "url": "https://walmart.com/ip/...", "found_via": "native_catalog" },
{ "retailer": "amazon", "url": "https://amazon.com/dp/B0...", "found_via": "amazon_catalog" }
],
/* Map keyed by retailer slug. Only present when include_cross_retailer=true.
Served from our cache; with fewer than three stores answered, the call
reads the missing stores itself (each cell's "read" says which). To force
a fresh read of one retailer, use ?retailer=<retailer>&force_refresh=true. */
"cross_retailer": {
"walmart": {
"retailer": "walmart",
"status": "ok",
"price": 100.56,
"url": "https://www.walmart.com/ip/...",
"in_stock": true,
"unavailable": false,
"sold_tag": "100+ bought since yesterday",
"estimated_sales": 1200,
"is_best_seller": true,
"pack_count": 1,
"hazmat": false,
"seller_context": {
"referral_fee_usd": 15.08,
"wfs_fee_usd": 11.95,
"restricted": { "flag": false, "reason": null },
"wfs_eligibility": { "enabled": true, "reason": null }
},
"_source": "primary",
"_freshness": { "refreshed_at": "2026-05-16T10:00:00Z" },
/* HOW this row was tied to the identifier you searched — and nothing else.
It is not a claim that the price is current or the variant is right;
freshness is _freshness, stock is in_stock.
state: "confirmed" the retailer's own record published the barcode
you searched.
"unconfirmed" no barcode we could read, so the match rests on
a title/keyword guess. Never treat as confirmed.
"mismatch" the retailer's record published a DIFFERENT
barcode; the product was re-filed under the code
it actually carries, so price and url are null.
method: "catalog_key" the retailer's catalog was keyed by this
identifier and its record echoes it back.
"page_barcode" the retailer's product page published a barcode
in its own markup and we compared it.
null nothing resolved (always with "unconfirmed").
identity itself is null on rows written before the check existed.
Ask for only the confirmed rows with ?identifier_match=confirmed. */
"identity": {
"state": "confirmed",
"method": "catalog_key",
"page_barcode": "194629116676",
"checked_at": "2026-05-16T10:00:00Z"
},
/* WAS THIS STORE READ, AND WHEN. When fewer than three stores hold an
answer, the call reads the missing stores itself, inside its own time
budget, and reports them here.
state: "read_now" read during this call.
"read_earlier" an earlier read; read_at says when.
"not_read" no usable answer; reason says why.
reason (only with not_read):
"not_checked_yet" no read on file yet.
"no_listing_found" searched by this identifier, no listing
found. Not the same as out of stock.
"identity_mismatch" the listing found is a different product.
"no_price_or_stock" a listing, but no price or stock answer.
"blocked" the store refused the read.
"timed_out" a read started in this call did not
finish in time; try again shortly.
"fetch_failed" the read failed for another reason.
A store that was not read is never an out-of-stock answer. */
"read": { "state": "read_now", "read_at": "2026-05-16T10:00:00Z", "reason": null }
},
"amazon": {
"retailer": "amazon",
"status": "ok",
"price": 105.99,
"url": "https://www.amazon.com/dp/...",
"in_stock": true,
"unavailable": false,
"sold_tag": null,
"estimated_sales": null,
"is_best_seller": null,
"pack_count": null,
"hazmat": null,
"seller_context": {
"referral_fee_usd": 15.90,
"wfs_fee_usd": null,
"restricted": null,
"wfs_eligibility": null
},
"_source": "secondary",
"_freshness": { "refreshed_at": "2026-05-16T09:00:00Z" },
"identity": {
"state": "unconfirmed",
"method": null,
"page_barcode": null,
"checked_at": "2026-05-16T09:00:00Z"
},
"read": { "state": "read_earlier", "read_at": "2026-05-16T09:00:00Z", "reason": null }
}
/* + ebay, lowes, target, bestbuy, homedepot, e.g.
"target": { "retailer": "target", "status": "not_found", ...,
"read": { "state": "not_read", "read_at": "2026-05-16T09:30:00Z", "reason": "no_listing_found" } } */
},
/* present when include_offers_reviews=true. offers[] is the full live per-seller
list (every competing seller), and offers_count reflects its length. Available
for Walmart (item_id) AND Amazon (ASIN or ?retailer=amazon, via the AOD panel).
Every offer carries a cross-retailer fulfilled_by_marketplace flag (Walmart
"wfs" / Amazon "fba"); Amazon offers also carry "fba", and seller_type is "1p"
(sold by the retailer itself) or "3p" (a marketplace seller), independent of
fulfillment. condition is a canonical enum shared across retailers.
On Amazon the buy box winner is marked only on evidence: root seller_name (and
seller_id when the page carries the merchant id) is the featured seller read from
the product page, and the one offers[] row it names has is_buy_box true. With no
such evidence no row is the buy box. A page price of 0 is served as null, with
price_reason "zero_price_ignored", never as 0. */
"offers": [
{ "seller_id": "101030561", "seller_id_hex": "C4E1FFE689DE4B388E0EDABC848B3078", "seller_name": "iTech361", "price": 100.56, "is_buy_box": true, "in_stock": true, "wfs": false, "fulfilled_by_marketplace": false, "pro_seller": true, "shipping_cost": 0, "delivery_date": "2026-07-06", "seller_type": "3p", "offer_id": "AB376F76122C494CBE9C13C605A1ADAD", "max_delivery_date": "2026-07-09", "max_order_quantity": 12, "fulfillment_type": "marketplace", "returnable": true, "return_window_days": 30, "free_returns": true, "condition": "new", "quantity_available": 12 },
/* Amazon (ASIN / ?retailer=amazon) offer shape — the AOD all-sellers list: */
{ "seller_id": "A1JD8CBFFS2JH6", "seller_id_hex": null, "seller_name": "Prime National Distribution", "price": 12.00, "is_buy_box": false, "in_stock": true, "wfs": null, "fba": false, "fulfilled_by_marketplace": false, "seller_type": "3p", "shipping_cost": 0, "delivery_date": "2026-07-08", "condition": "new", "quantity_available": null }
],
"review_summary": {
"average_rating": 4.3,
"total_reviews": 1247,
"rating_distribution": { "5": 812, "4": 234, "3": 98, "2": 53, "1": 50 }
},
"reviews": {
"summary": { "average_rating": 4.3, "total_reviews": 1247, "sentiment": { "positive": 1040, "neutral": 110, "negative": 97 }, "verified_purchases": 1180 },
"monthly_breakdown": [
{ "month": "2026-05", "total_reviews": 41, "average_rating": 4.4, "sentiment": { "positive": 34, "neutral": 4, "negative": 3 } }
],
"top_reviews": [] /* individual review bodies, when the source exposes them. Walmart currently returns aggregates only, so this is typically empty. */
},
/* present when include_offers_history=true (Walmart). offers_history[] is the
price/seller archive — same per-offer field names as offers[], plus
observed_at (the snapshot time) and was_price. offers_history_count is the
total archived rows. Empty array when we have no archive for the item yet. */
"offers_history_count": 248,
"offers_history": [
{ "observed_at": "2026-06-21T14:02:00Z", "seller_id": "101030561", "seller_id_hex": "C4E1FFE689DE4B388E0EDABC848B3078", "seller_name": "iTech361", "seller_type": "3p", "price": 104.99, "was_price": 119.99, "shipping_cost": 0, "wfs": false, "pro_seller": true, "condition": "new", "quantity_available": 12, "fulfillment_type": "marketplace", "delivery_date": "2026-06-27", "offer_id": "AB376F76122C494CBE9C13C605A1ADAD", "returnable": true, "return_window_days": 30, "free_returns": true, "seller_rating": 4.6, "seller_review_count": 1820 }
],
"_meta": {
"title_source": "walmart",
"weight_lbs_source": "amazon",
"dimensions_source": "amazon",
"data_quality_score": 1.0
},
"tokens_consumed": 11,
"tokens_remaining": 856249
}Field types
- Identifiers: always strings.
item_idis Walmart-internal;upcis the GTIN equivalent. - Prices: USD floats with 2 decimal places.
nullwhen out of stock. - Timestamps: ISO 8601 UTC.
- data_fetched_at: ISO 8601 timestamp or
nullwhen unknown. Present on cached Target answers from a non-force?retailer=targetidentifier lookup; records when the data was fetched. - stale: boolean, present on those cached Target answers.
truewhen the fetch time is missing or older than 30 days; otherwisefalse. Stale answers still return HTTP 200 with the cached product and schedule a background refresh. - in_stock:
truewhen at least one retailer reports the product is in stock;falseonly when every retailer with a known signal reports out-of-stock;nullwhen no retailer has reported yet. - in_stock_basis:
"price_only",null, or absent."price_only"means a Target answer had a buyable price but no stock signal, so the top-levelin_stockis inferred from the price (it is thentrue). It is only set on the top-levelin_stockof a Target answer, never oncross_retailercells.nullor absent meansin_stockwas not inferred this way. - cross_retailer.<retailer>.status:
ok|stale|indexing|not_found|blocked|error - cross_retailer.<retailer>.seller_context:
nullpermanently for first-party retailers (target, bestbuy, lowes, homedepot) — they don't have a marketplace. - retailer_links: where the product is found across retailers (URL only). Free on every barcode lookup.
Cross-retailer field semantics
Bucket-1 facts (sold_tag, estimated_sales, is_best_seller, pack_count, hazmat) populate where each retailer supports the concept. Most are populated for Walmart today; other retailers backfill as we add per-retailer parsers. Null doesn't mean "we forgot to fetch" — it means "this retailer doesn't expose this concept".
Walmart sold_tag and is_best_seller. Both come from the badges Walmart currently shows on the product. sold_tag is the purchase badge as Walmart words it (for example "1K+ bought since yesterday"); when a product shows more than one, the shortest period wins (since yesterday, then past week, then past month), then the larger count. "In 500+ people's carts" is not a purchase and never becomes a sold_tag. is_best_seller is true when Walmart's "Best seller" badge is on the product and false when we hold the product's badges and it is not; null means we hold no badge information for the product. A badge is a threshold, so sold_tag is a floor, not a count; use stats.demand_30d for how often it shows.
Walmart stats.rank_history. Rank is the product's position in a Walmart best-seller category listing, so a lower number is better. It is kept per listing because the same product sits at different positions in different listings; compare a listing only with itself. latest_rank is the newest day we saw it, best_* the best position reached in the window, median_* the median of the daily closing positions, and days_seen_* how many days in the window we saw it ranked. A rank is recorded only on days we checked the listing, so a day without a rank means we have no reading for it, not that the product dropped out. Windows are counted like stats.demand_30d: bars dated 30 (or 7) days ago through today, so a day count can be one higher than its label. The block is absent when we hold no ranked day for the product in the last 30 days.
Top-level weight_lbs, dimensions, description, mpn, model use the best source we have across retailers. When one retailer's payload is missing weight or dimensions, another retailer's value backfills it automatically (Amazon's catalog tends to be most reliable for physical specs because FBA forces sellers to declare them). _meta.weight_lbs_source records which retailer the value came from on every response.
Package vs assembled. Retailers that distinguish the boxed-for-shipping weight from the product's own weight expose both as weight_assembled_lbs + weight_package_lbs (and the parallel dimensions_assembled + dimensions_package). Top-level weight_lbs / dimensions are the derived "best available": assembled wins (it's what the product actually is); package fills in when no assembled value is known. Retailers that only expose one weight populate weight_lbs and leave the explicit pair as null — call weight_assembled_lbs ?? weight_lbs when you need the product weight and don't care about the source.
Diagnostic _meta block
Every barcode lookup includes a _meta object describing which source each top-level field came from and how strongly sources agreed on identity. Safe to ignore for most uses.
{
"_meta": {
"title_source": "amazon",
"brand_source": "walmart",
"weight_lbs_source": "amazon",
"dimensions_source": "amazon",
"data_quality_score": 0.667,
"disagreements": [
{
"retailer": "target",
"reason": "title-mismatch",
"similarity": 0.21,
"their_title": "Different product entirely",
"consensus_title": "12-in-1 Electric Pressure Cooker 6 QT..."
}
]
}
}How fresh the seller list is (_meta.data_quality)
Separate from data_quality_score (identity agreement across retailers), _meta.data_quality tells you how the seller list in offers was served and which fields hold a stand-in rather than a value we read, so you can decide for yourself whether to act on it. It is added only when there is something to say; we never send { "degraded": false }, and we never send 0 to mean "unknown".
// A seller list we had stored, read 20 hours ago:
{ "_meta": { "data_quality": { "offers_cell_age_ms": 72000000 } } }
// A seller list read live for this request:
{ "_meta": { "data_quality": { "offers_cell_age_ms": 0 } } }
// A full seller-list request answered with the buy-box offer only:
{ "_meta": { "data_quality": { "degraded": true, "reasons": ["offers_degraded_to_buybox"] } } }offers_cell_age_ms: how old the seller list inoffersis, in milliseconds, measured from when that list itself was read.0means we read the list live for this request. A list our own reader fetched while you waited reports the age of that read, usually under a second. A larger number is the age of a list read earlier and served from storage. It is sent on: an Amazon offers call (an ASIN, or?retailer=amazon, withinclude_offers_reviews=trueorinclude_offers=true), on any plan; a Walmart offers call on the Enterprise plan, which returns the full live seller list; a lookup by walmart.com product URL withinclude_offers_reviews=truethat we answered from a stored record carrying a seller list, on plans below Enterprise too; and a?retailer=<r>lookup answered from a stored retailer record that carries a seller list, even when you did not ask for offers. When we read a Walmart product page and it shows the item is not purchasable,offersis empty and the age is the age of that page read. It is not sent on a Walmart offers call by item ID or barcode on plans below Enterprise (that answer carries the listing's buy-box offer, not a stored seller list), on a?retailer=<r>lookup that was not answered from a stored retailer record, or when our availability check, without reading the page, told us the item is not purchasable. When the key is absent, treat the age as unknown, not as "live".degraded:trueonly when you asked for the full Walmart seller list and we answered with the buy-box offer instead.offersthen holds that one offer, andtotal_offerscan be larger thanoffers_count. Absent otherwise.reasons: machine-readable causes, sent only withdegraded: true. Today the one value isoffers_degraded_to_buybox.undetermined: the fields on this response that hold a stand-in rather than a value we read, each with a code saying why. It is sent only when there is at least one, and it does not setdegraded. The field values themselves are unchanged, so read this before you act on them.
Fields that hold a stand-in (_meta.data_quality.undetermined)
// A lookup with no seller list read, and no buy-box price read:
{ "offers_count": 0, "buybox_price": 24.97,
"_meta": { "data_quality": { "undetermined": {
"offers_count": "no_seller_list_read",
"buybox_price": "no_buy_box_read" } } } }
// include_offers_history=true when the history read failed:
{ "_meta": { "data_quality": { "undetermined": {
"offers_history": "history_read_failed",
"offers_history_count": "history_read_failed" } } } }offers_count: "no_seller_list_read":offers_countis0because neither a seller list nor a seller total was read for this answer, so the0is not a count of sellers. A seller list that was read and found empty, or atotal_offerswe read, is a real count and is not named here.buybox_price: "no_buy_box_read": the top-levelbuybox_pricewas not taken from a buy-box read and carries the item's listed price. Whenoffersis present, the offer markedis_buy_box: trueis the buy box as read; use its price.offers_historyandoffers_history_count: "history_read_failed": you asked forinclude_offers_history=trueand the history read failed or timed out. Both fields are left out and the history add-on is not charged; retry later. A history read that found no rows returnsoffers_history: []andoffers_history_count: 0, not this code.
Errors
404when the identifier doesn't resolve (multiple format fallbacks are attempted before returning 404)400when the identifier doesn't match any expected format429when you exceed your throttle or monthly quota503(offers_unavailable) when live offers are momentarily unavailable for a Walmart item. Returned only for live-offers requests (include_offers_reviews=true) so you never receive non-live offer data. Honor theRetry-Afterheader (seconds) and retry; the request is not billed.
Examples
Walmart item_id
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/19667262713"UPC with cross-retailer
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/194629116676?include_cross_retailer=true"Exact matches only (no fuzzy cross-retailer rows)
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/194629116676?include_cross_retailer=true&identifier_match=confirmed"Returns only the retailers whose own record carries the barcode you sent. Same price as the unfiltered call. If cross_retailer comes back {}, that is the honest answer — we hold nothing for that product we can vouch for, and you should treat it as a miss rather than fall back to a guess. Leave the parameter off (or send identifier_match=any) to get every row plus the identity label on each, and decide per row yourself.
What identity does and does not claim. It reports how the row was matched to your identifier — nothing more. A confirmed row can still be stale or out of stock: freshness is _freshness.refreshed_at and availability is in_stock. It is not a guarantee about the price, the variant, or the condition.
asin_match — did an ASIN lookup return the product you asked for?
Present on ASIN lookups only, and separate from identity above — that field describes barcode matching inside cross_retailer; this one describes the top-level body of an ASIN lookup.
The asin field in the response is echoed from your request. We resolve an ASIN by reading that ASIN's own product page. For a product with size or colour variants, Amazon sometimes serves a sibling variant on that URL — a different sellable unit, with its own price and its own availability. asin_match is how you tell the two apart.
state | What it means |
|---|---|
exact | The listing we read is the ASIN you asked for. |
substituted | Amazon served a different product. This never appears on a 200. A substitution is refused at the source and by the cache gate alike, so it reaches you only as 404 identity_mismatch, whose body carries this same asin_match object naming what was served. Branch on the code, not on a 200 body. |
unknown | The source carried no ASIN, so we never checked. Not a pass. |
matched_asin names the listing we actually read, or null when state is unknown.
So on a 200, state is exact or unknown. Do not write a state === 'substituted' branch and expect it to fire — two gates make it unreachable on a success: the live fetch refuses a page whose own ASIN differs, and the cache refuses to serve a stored row whose payload names a different ASIN. The substitution reaches you as the 404 below. unknown is the one to guard against on a 200: it means the source carried no ASIN, so nothing was checked.
404 identity_mismatch — not the same as 404 not_found
An ASIN lookup has two distinct 404s, and the code is what separates them. If you refresh prices or stock from this endpoint, treat them differently.
code | What happened | What to do |
|---|---|---|
not_found | Amazon serves its own 404 for this ASIN. The listing is gone. | A real verdict. Safe to mark the item unavailable. |
identity_mismatch | The page exists but resolves to a different product (asin_match.matched_asin). We know nothing about the ASIN you sent. | Not a stock verdict. Keep your last known good price and stock. Retrying will not change it. |
This is not a rare branch: on a 50-ASIN sample of real supplier catalogue data, 8 resolved to a sibling. A refresh job that reads any 404 as "delisted" would wrongly deactivate roughly one item in six.
Which retailers can be confirmed, and why
This differs by retailer because the underlying lookups differ, and a retailer that cannot currently be confirmed will be absent from a identifier_match=confirmed response. Read this table before you switch the filter on.
| Retailer | Can be confirmed? | How we look it up |
|---|---|---|
| Walmart | Yes — catalog_key | Our catalog is queried by your identifier and the record it returns echoes it back. |
| eBay | Yes — catalog_key | eBay's catalog is queried by barcode, so a hit is an identifier match by construction. eBay's secondary search-page path is unconfirmed. |
| Best Buy | Yes — page_barcode | The product page publishes its own barcode and we compare it. |
| Lowe's | Yes — page_barcode | Same — the page publishes a barcode. |
| Home Depot | Yes — page_barcode | Same, where the page publishes one. |
| Amazon | No — always unconfirmed | Amazon is reached by a keyword search that accepts a barcode as the search text. Amazon never tells us the listing carries that code, so we will not claim it does. Amazon rows are dropped by identifier_match=confirmed. |
| Target | No — always unconfirmed | Target is reached through a search engine, and the product page does not publish a barcode we can read. Target rows are dropped by identifier_match=confirmed. |
If you need Amazon or Target rows, use the default (identifier_match=any) and read each row's identity yourself — you will get the row plus an honest label, rather than silence.
Coverage is still filling in. The identity check began recording in September 2026, and cached rows written before then carry identity: null until they are next refreshed. Treat identifier_match=confirmed as a high-precision filter that is still low recall: what it returns is trustworthy, but it does not yet return everything we hold.
Reseller full view (cross-retailer + seller context)
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/194629116676?include_cross_retailer=true&include_seller_context=true"EAN-13 (international)
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/5060214160102"ISBN-13 (book)
curl -H "Authorization: Bearer rk_live_..." \
"https://api.retailerapi.com/v1/products/9780553213119?format=ISBN"