Walmart Product Scraper for extracting product prices, titles, specifications, ratings, images and seller data from Walmart.com. This repo has a free Walmart product script you can run right now, and a Walmart product data API that returns real structured JSON.
Last updated: 2026-07-20. Working against Walmart.com as of July 2026, and re-verified whenever Walmart changes their markup.
The uncut responses behind every block on this page are committed in walmart_product_api_data/, captured 2026-07-20, and each block states what was trimmed from it. Every code example calls the API and is runnable from walmart_product_api_codes/.
This repo covers one endpoint in depth. For Walmart search listings, reviews, bestsellers and category filters, see the Walmart Scraper.
pip install requests
export CHOCODATA_API_KEY="your_key" # free: 1,000 requests, one-time, no card
python walmart_product_api_codes/product.py 19075520026Those three lines return this, live from Walmart.com (general.images and specifications shown as counts, and description truncated where marked, to keep the lead readable; the full sample is uncut):
{
"general": {
"title": "Lenovo IdeaPad Slim 3x 15.3\" Laptop - AI Snapdragon X, 16GB RAM, 512GB SSD, Luna Grey",
"brand": "Lenovo",
"description": "Elevate your computing experience with the Lenovo IdeaPad Slim 3x...",
"images": "[ 18 urls ]",
"meta": { "gtin": "199274646943", "sku": "19075520026" }
},
"price": { "currency": "USD", "price": 449 },
"rating": { "rating": 4.5, "count": 222 },
"seller": { "name": "Walmart.com" },
"specifications": "[ 36 key/value pairs ]"
}...multiplied out to 10 top-level fields, 36 specifications and 18 image URLs:
That is the whole point of this repo. The rest of this page is the free script, the wall it hits, and the endpoint with its parameters and response.
- Free Walmart Product Scraper
- Avoid getting blocked when scraping Walmart products
- Walmart Product Scraper API reference
- Monitor Walmart price and stock changes
- Measured latency
Walmart server-renders its item pages into a __NEXT_DATA__ JSON blob, and the full product rides along in it, so when a request gets through you can extract the title, price, specifications and images without a headless browser or JavaScript rendering. No key, no cost:
python free_scraper/walmart_product_free_scraper.py 19075520026Source: free_scraper/walmart_product_free_scraper.py. It fetches /ip/{id}, finds the __NEXT_DATA__ blob, walks to props.pageProps.initialData.data.product, and emits title, brand, price, currency, rating, review_count, images, model.
After running the command, your terminal should look something like this:
That is the outcome, and the next section is about why.
We ran exactly that script against Walmart.com while writing this README, 6 times, on 6 different item ids. All 6 returned the same thing, and every run is recorded in free_scraper_survey.json:
NO DATA: the response carried no product node.
A 200 does not mean success. Check for the interstitial before parsing.
Here is the raw evidence behind that message:
| What we measured | Value |
|---|---|
| HTTP status | 200 (not 403, not 429) |
| Response size | 15,195 bytes (the same page through the API is 406,096 bytes) |
<title> |
Robot or human? |
__NEXT_DATA__ blob |
absent |
| Product parsed | 0 fields on 6 of 6 item ids |
| Swapping in a browser User-Agent | changed nothing: same 200, same 15,195 bytes, same title |
walmart.com/ from the same client |
200, 256,504 bytes |
robots.txt, User-agent: * |
no Disallow covering /ip/ |
A 15 KB page titled "Robot or human?" returned with a 200 status. That single row is the whole problem in miniature, and it is the most important thing to understand about scraping Walmart product pages:
| What bites you | Why | What it costs you |
|---|---|---|
| HTTP 200 is not success | Walmart serves its bot check with a 200 status, not a 403. A naive scraper parses it, finds nothing, and logs "0 fields" rather than "blocked". |
The expensive failure is not a crash. It is weeks of empty rows that looked like "this product has no data". |
| Complying does not get you in | Walmart's robots.txt carries no Disallow for /ip/, so the product page is robots-allowed. It is refused anyway, and a browser User-Agent did not change the result. |
Reading robots.txt and obeying it does not get you the product. The block happens before your parser runs. |
| The same client gets served elsewhere | walmart.com/ returns a normal 256 KB page to the same client, same IP, same headers, seconds later. Only /ip/ came back as the interstitial. |
Your smoke test against the homepage passes and tells you nothing about the path you actually need. |
| Where you run it is part of the input | Every measurement on this page was taken from a residential connection. Cloud egress ranges are published and widely filtered, so a result from your laptop does not carry over to CI unchanged. | Whatever you measure locally has to be re-measured where the job will actually run. |
out_of_stock is not deterministic |
On 12 identical back-to-back calls to one item, price, rating and specifications were identical every time; out_of_stock came back True on one of the 12 (see availability_survey.json). |
Treat availability as a signal, not a guarantee. Track price on its own field, and poll a few times before trusting a stock flag. |
| The JSON path moves | The __NEXT_DATA__ shape changes a few times a year. Your parser silently returns nothing. |
Ongoing maintenance, plus alerting smart enough to tell "empty" from "broken". |
Worth knowing before you go shopping for a free alternative: a plain HTTP client fetching /ip/ is the shape of request that got the interstitial here, and it is the shape most free Walmart scrapers use, so check a repo's own recent output rather than its README. Several of the popular ones that look like they work are wrappers that require a paid vendor's API key, which is worth checking for before you clone.
The managed option, and the one this repo is built around. The Chocodata Walmart Product Scraper API returns the full Walmart product as parsed JSON for Walmart product data extraction at scale, with a ~99% success rate against the bot check and no proxy management. Free for the first 1,000 requests.
Below is the Walmart Product Scraper API reference to get you started: authentication, the parameters, errors and rate limits, then the endpoint with its response.
curl "https://api.chocodata.com/api/v1/walmart/product?api_key=YOUR_KEY&id=19075520026"import requests
r = requests.get(
"https://api.chocodata.com/api/v1/walmart/product",
params={"api_key": "YOUR_KEY", "id": "19075520026"},
timeout=90,
)
d = r.json()
print(d["general"]["title"], d["price"]["price"], d["price"]["currency"])
# Lenovo IdeaPad Slim 3x 15.3" Laptop - AI Snapdragon X, 16GB RAM, 512GB SSD, Luna Grey 449 USDAfter running the command, your terminal should look something like this:
Get a key at chocodata.com (1,000 requests, one-time, no card).
Pass api_key as a query parameter on every request. That is the whole auth model.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
api_key |
string | yes | - | Your Chocodata API key. |
id |
string (digits) | one of id/url |
- | Walmart us_item_id, e.g. 19075520026. /ip/{id} is built from it. |
url |
string (URL) | one of id/url |
- | Full Walmart item URL. Takes precedence over id. |
country |
string (ISO-2) | no | us |
Egress location. Walmart localises its pages to the location it sees, so pin this for reproducible results. |
add_html |
boolean | no | false |
Also return the raw upstream HTML alongside the parsed JSON (debugging). |
Each request costs 5 credits (= 1 request). Responses are billed only on success (2xx).
There is no page and no sort parameter on this surface: one call returns one item. Unknown parameters are ignored rather than rejected, so page=2 returns HTTP 200 with the same item.
id and url are two ways to name the same item and they agree: on 2026-07-20 both forms returned identical payloads for 19075520026, field for field.
Nothing below is billed: you are only charged on a 2xx.
| Status | error code |
Meaning | Billed | What to do |
|---|---|---|---|---|
400 |
invalid_params |
A required param is missing or the wrong type. Body lists the exact issue and path. |
no | Fix the query string. |
401 |
INVALID_API_KEY |
Key missing, unrecognised, or revoked. | no | Check api_key. Get one at chocodata.com. |
402 |
INSUFFICIENT_CREDITS |
Balance exhausted. | no | Top up or upgrade at chocodata.com. |
404 |
item_not_found |
The target returned 404: the us_item_id does not exist. retryable: false. |
no | Fix the id. Retrying will not help. |
429 |
RATE_LIMITED |
Over the rate limit or your plan's concurrency. | no | Back off and retry; see Rate limits. |
502 |
target_unreachable |
Walmart refused every attempt for this request. retryable: true. |
no | Retry once after a few seconds. See the note below. |
Two response shapes exist: auth and billing errors nest under error.code (uppercase), while scrape-layer errors are flat with a lowercase error string plus retryable.
The scripts in this repo map each status onto an actionable message, so a typo'd key does not hand you a stack trace:
A bad key, verbatim:
curl "https://api.chocodata.com/api/v1/walmart/product?api_key=totally_invalid_key_123&id=19075520026"{"error":{"code":"INVALID_API_KEY","message":"Api key not recognised."}}Calling it with neither id nor url, verbatim:
curl "https://api.chocodata.com/api/v1/walmart/product?api_key=YOUR_KEY"{"error":"invalid_params","issues":[{"code":"custom","message":"either url or id is required","path":[]}]}walmart.product is a flaky surface rather than an unreliable one. On 2026-07-20 a single item id returned 502 target_unreachable on 1 of 20 back-to-back single calls and 200 on the other 19. The 502 is retryable: true, it is not billed, and the same id succeeds on the next attempt, so the scripts here retry once after an 8-second pause before giving up. A 404 item_not_found is different: it is retryable: false, it means the id does not exist, and retrying will not change it.
Two limits apply at once, and the one that binds first is usually the rate limit, not the concurrency cap.
Rate limit: 120 requests per 60 seconds, per key (sliding window). That is a hard ceiling of 2 requests/second sustained, whatever your plan.
Concurrency: how many requests you may have in flight at once, which varies by plan:
| Plan | Concurrent requests |
|---|---|
| Free | 10 |
| Vibe | 30 |
| Pro | 50 |
| Custom | 100 to 500+ |
Exceed either and you get 429, not a queue. Every request is a synchronous GET: there is no webhook, callback, or async job to poll. The examples use timeout=90 because a request that runs into the bot check and is re-attempted takes longer than the median suggests.
Size against the rate limit rather than the concurrency figure. One call covers one item, so a 500-product catalogue is 500 calls, or roughly 4.2 minutes at 120/minute, and a daily sweep of 5,000 products is about 42 minutes. Keep your pool at or under your plan's concurrency and pace it to the rate limit:
from concurrent.futures import ThreadPoolExecutor
import requests
def one(item_id):
r = requests.get("https://api.chocodata.com/api/v1/walmart/product",
params={"api_key": KEY, "id": item_id}, timeout=90)
d = r.json() if r.ok else {}
return item_id, d.get("price", {}).get("price"), d.get("rating", {}).get("count")
items = ["19075520026", "13626250252", "10226953067", "5250574665"]
with ThreadPoolExecutor(max_workers=10) as pool: # <= your plan's concurrency
for item_id, price, reviews in pool.map(one, items):
print(item_id, price, reviews)Full detail for a single Walmart item: the product object Walmart renders on the item page, flattened into nested leaf names (general, price, rating, seller, fulfillment, breadcrumbs, specifications) plus the canonical us_item_id, product_id and model.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string (digits) | one of id/url |
- | Walmart us_item_id. |
url |
string (URL) | one of id/url |
- | Full Walmart item URL. |
curl "https://api.chocodata.com/api/v1/walmart/product?api_key=YOUR_KEY&id=19075520026"Real response. general.images cut to 1 of 18 and specifications cut to 3 of 36 (both noted inline); every other field is complete and verbatim, description truncated where marked (full sample):
{
"general": {
"title": "Lenovo IdeaPad Slim 3x 15.3\" Laptop - AI Snapdragon X, 16GB RAM, 512GB SSD, Luna Grey",
"brand": "Lenovo",
"url": "https://www.walmart.com/ip/IdeaPad-Slim-3x-15-Laptop-Snapdragon-X-X1-26-100-16GB-RAM-512GB-SSD-Luna-Grey/19075520026",
"description": "Elevate your computing experience with the Lenovo IdeaPad Slim 3x. This 15.3-inch laptop provides crisp visuals...",
"main_image": "https://i5.walmartimages.com/seo/IdeaPad-Slim-3x-15-Laptop-Snapdragon-X-X1-26-100-16GB-RAM-512GB-SSD-Luna-Grey_c2916a8d-bbad-4bad-a8a9-3b185447a088.8043aee12ac65bae952d1723d1df243d.jpeg",
"images": ["... 18 image urls ..."],
"meta": { "gtin": "199274646943", "sku": "19075520026" }
},
"price": { "currency": "USD", "price": 449 },
"rating": { "rating": 4.5, "count": 222 },
"seller": { "id": "F55CDC31AB754BB68FE0B39041159D63", "name": "Walmart.com", "official_name": "Walmart.com" },
"fulfillment": { "out_of_stock": false, "free_shipping": false },
"breadcrumbs": [
{ "category_name": "Electronics", "url": "/cp/electronics/3944" },
{ "category_name": "... 5 more ...", "url": null }
],
"specifications": [
{ "key": "Brand", "value": "Lenovo" },
{ "key": "Proc. brand", "value": "Qualcomm (Snapdragon)" },
{ "key": "Processor", "value": "Snapdragon X X1-26-100" },
{ "key": "... 33 more ...", "value": "..." }
],
"us_item_id": "19075520026",
"product_id": "24H72HS7PA9J",
"model": "83N3006BUS"
}general.meta.gtin is the field most people come for: it is the barcode (UPC) that lets you join Walmart rows to Amazon catalogues or a supplier feed. It was present on all 6 items in product_survey.json. Two more things worth coding against, both visible in the committed samples:
seller.nametells 1P from 3P. On first-party items it isWalmart.com; on marketplace items it is the third-party seller, e.g.Hamilton Beach Brands IncorYiwu Yingli Electronic Commerce Co., Ltd.in the survey. Theseller.idis stable across items sold by the same seller.specificationslength is item-specific. Across the six sampled items it ran from 9 to 41 pairs, and Walmart occasionally repeats a key (the laptop returnsResolutiontwice with different values). Treat it as a list of pairs, not a dict, or you will drop the duplicate.
A note on url vs id: they resolve to the same item. When you pass a non-Walmart URL the endpoint fetches it but fails cleanly rather than guessing: url=https://example.com returns 502 extraction_failed (no product node), not a fake 200. The control run is committed.
Runnable: walmart_product_api_codes/product.py
Watching a product's price and availability move is the main reason people scrape Walmart product pages, so that use case is in the repo end to end rather than as a snippet. price_monitor.py polls a list of items, stores every observation as a local dataset in SQLite, and prints what moved since the last run:
python walmart_product_api_codes/price_monitor.py 19075520026 13626250252 10226953067That is a real first run: nothing to compare against yet, so it seeds the database and says so. Run it again after prices move and each change prints as a diff line (CHG 13626250252 ... | price 238 -> 219 (-19.00) | +14 review(s)). Track price on its own field rather than inferring it from the stock flag, which (as measured above) is not deterministic per call.
Export the history with one command:
sqlite3 -header -csv walmart_products.db "select * from observations;" > out.csvOne API call per item per run. 1,000 free requests covers a month of daily checks on ~30 products.
Real end-to-end wall-clock, measured from a laptop against the live API on 2026-07-20. This includes the upstream fetch, the anti-bot handling, and the parse:
| Endpoint | Median | Range | n |
|---|---|---|---|
/walmart/product |
3.9s | 3.0 to 5.4s | 14 |
Read the range, not just the median. A request that runs into Walmart's bot check is re-attempted upstream until it comes back with real data, which is where the tail comes from, and absorbing that silently is the thing you are actually buying. The free script in this repo hits the same wall and simply stops. Budget a retry rather than a failure for the occasional 502, which is retryable and not billed. Small sample (n=14); reproduce it yourself with the scripts here.
MIT. See LICENSE.







