retailerapi / docs

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

NameRequiredDescription
idyesUPC-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:
  • amazon.com/dp/B07ABC1234 — Amazon ASIN extracted, full canonical shape
  • walmart.com/ip/Some-Item/19667262713 — Walmart item_id extracted, full canonical shape (also /p/, /grocery/ip/)
  • target.com/p/.../A-89377627 — Target TCIN-based PDP via Target's RedSky JSON endpoint
  • bestbuy.com/product/.../JXJ62C6654 — Best Buy SKU-based PDP, title + price from JSON-LD + embedded GraphQL
  • homedepot.com/p/.../204287095 — Home Depot PDP with JSON-LD inline
  • lowes.com/pd/.../1000318555 — Lowe's PDP with JSON-LD inline
  • ebay.com/itm/322204481859 — eBay listing
  • any-shopify-store.com/products/some-handle — Shopify's universal .json endpoint
  • patagonia.com/product/.../25528.html — generic JSON-LD parser path
Variant query params + fragments are preserved. Bare URLs of known retailers extract to the underlying identifier (fast path, cache-friendly). URLs with query strings or fragments are fetched directly so variant selectors aren't stripped silently.

Query parameters

NameTypeDefaultDescription
formatstringauto-detectOne 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_reviewsbooleanfalseLive 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_offersbooleanfalseLive 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_retailerbooleanfalseInclude 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_contextbooleanfalseLive 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_historybooleanfalseHistorical 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_seriesbooleanfalseThe 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_matchany | confirmedanyExact-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.
retailerstring(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_refreshbooleanfalseBypass 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_storedbooleanfalseWith 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.
zipstring90210US 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 shapeTokensWhat you get
Base lookup1title, 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_retailer3+ 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_context4+ live restriction + WFS eligibility state
+ include_offers_reviews6+ live marketplace sellers + review summary (sentiment + monthly trend)
+ include_offers_history6+ historical marketplace offers (price/seller archive, Walmart)
+ include_price_series11+ the raw day-by-day price bars under price_history.series (Walmart). The aggregates are already free in the base call.
All three flags11Full reseller view (cross_retailer + seller_context + offers_reviews)
?retailer=<r>1Anchor to a specific retailer (replaces base call shape)
?retailer=<r>&force_refresh=true1Same 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_id is Walmart-internal; upc is the GTIN equivalent.
  • Prices: USD floats with 2 decimal places. null when out of stock.
  • Timestamps: ISO 8601 UTC.
  • data_fetched_at: ISO 8601 timestamp or null when unknown. Present on cached Target answers from a non-force ?retailer=target identifier lookup; records when the data was fetched.
  • stale: boolean, present on those cached Target answers. true when the fetch time is missing or older than 30 days; otherwise false. Stale answers still return HTTP 200 with the cached product and schedule a background refresh.
  • in_stock: true when at least one retailer reports the product is in stock; false only when every retailer with a known signal reports out-of-stock; null when 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-level in_stock is inferred from the price (it is then true). It is only set on the top-level in_stock of a Target answer, never on cross_retailer cells. null or absent means in_stock was not inferred this way.
  • cross_retailer.<retailer>.status: ok | stale | indexing | not_found | blocked | error
  • cross_retailer.<retailer>.seller_context: null permanently 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 in offers is, in milliseconds, measured from when that list itself was read. 0 means 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, with include_offers_reviews=true or include_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 with include_offers_reviews=true that 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, offers is 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: true only when you asked for the full Walmart seller list and we answered with the buy-box offer instead. offers then holds that one offer, and total_offers can be larger than offers_count. Absent otherwise.
  • reasons: machine-readable causes, sent only with degraded: true. Today the one value is offers_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 set degraded. 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_count is 0 because neither a seller list nor a seller total was read for this answer, so the 0 is not a count of sellers. A seller list that was read and found empty, or a total_offers we read, is a real count and is not named here.
  • buybox_price: "no_buy_box_read": the top-level buybox_price was not taken from a buy-box read and carries the item's listed price. When offers is present, the offer marked is_buy_box: true is the buy box as read; use its price.
  • offers_history and offers_history_count: "history_read_failed": you asked for include_offers_history=true and 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 returns offers_history: [] and offers_history_count: 0, not this code.

Errors

  • 404 when the identifier doesn't resolve (multiple format fallbacks are attempted before returning 404)
  • 400 when the identifier doesn't match any expected format
  • 429 when you exceed your throttle or monthly quota
  • 503 (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 the Retry-After header (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.

stateWhat it means
exactThe listing we read is the ASIN you asked for.
substitutedAmazon 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.
unknownThe 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.

codeWhat happenedWhat to do
not_foundAmazon serves its own 404 for this ASIN. The listing is gone.A real verdict. Safe to mark the item unavailable.
identity_mismatchThe 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.

RetailerCan be confirmed?How we look it up
WalmartYes — catalog_keyOur catalog is queried by your identifier and the record it returns echoes it back.
eBayYes — catalog_keyeBay's catalog is queried by barcode, so a hit is an identifier match by construction. eBay's secondary search-page path is unconfirmed.
Best BuyYes — page_barcodeThe product page publishes its own barcode and we compare it.
Lowe'sYes — page_barcodeSame — the page publishes a barcode.
Home DepotYes — page_barcodeSame, where the page publishes one.
AmazonNo — always unconfirmedAmazon 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.
TargetNo — always unconfirmedTarget 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"