Walmart Reviews Scraper for extracting review text, star ratings, rating distributions and review dates from Walmart.com. This repo has a free Walmart reviews script you can run right now, and a Walmart review 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_reviews_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_reviews_api_codes/.
This repo covers one endpoint in depth. For Walmart search listings, product pages, 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_reviews_api_codes/reviews.py 19075520026Those three lines return this, live from Walmart.com (reviews cut to 1 of 10 and that review shown with 4 of its 10 fields, to keep the lead readable; the full sample is uncut):
{
"us_item_id": "19075520026",
"overall_rating": 4.5,
"total_reviews": 222,
"rating_distribution": { "1": 11, "2": 8, "3": 3, "4": 28, "5": 172 },
"reviews": [
{
"reviewer_name": "Colton",
"rating": 5,
"review_date": "6/17/2026",
"verified_purchase": true
}
]
}...multiplied by 10 review bodies with 10 fields each, plus a histogram covering all 222:
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 Reviews Scraper
- Avoid getting blocked when scraping Walmart reviews
- Walmart Reviews Scraper API reference
- Track a Walmart product's rating over time
- Measured latency
Walmart server-renders its item pages into a __NEXT_DATA__ JSON blob, and the reviews ride along in it, so when a request gets through you can extract review text and star ratings without a headless browser or JavaScript rendering. No key, no cost:
python free_scraper/walmart_reviews_free_scraper.py 19075520026Source: free_scraper/walmart_reviews_free_scraper.py. It fetches /ip/{id}, finds the __NEXT_DATA__ blob, walks to props.pageProps.initialData.data.reviews, and emits review_id, reviewer_name, rating, title, text, review_date.
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, 8 times, on 8 different item ids. All 8 returned the same thing, and every run is recorded in free_scraper_survey.json:
NO DATA: the response carried no reviews 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 458,619 bytes) |
<title> |
Robot or human? |
__NEXT_DATA__ blob |
absent |
| Reviews parsed | 0 on 8 of 8 item ids |
walmart.com/ from the same client |
200, 1,202,418 bytes |
robots.txt, User-agent: * |
Allow: /reviews/product/, and 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 reviews:
| 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 reviews" rather than "blocked". |
The expensive failure is not a crash. It is weeks of empty data that looked like "this product has no reviews". |
| Complying does not get you in | Walmart's robots.txt explicitly allows /reviews/product/ and carries no Disallow for /ip/, so the page holding the reviews is robots-allowed. It is refused anyway. |
Reading robots.txt and obeying it does not get you reviews. The block is enforced before your parser runs. |
| The same client gets served elsewhere | walmart.com/ returns a normal 1.2 MB 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. |
| Incentivized reviews are mixed into the same list | Walmart prefixes them with a literal [This review was collected as part of a promotion.]. On the sampled item, 3 of the 10 bodies carry it. |
Sentiment scoring that does not filter them measures marketing copy, not customers. |
| The JSON path moves | The __NEXT_DATA__ shape changes a few times a year. Your parser silently returns []. |
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 Reviews Scraper API returns Walmart customer reviews and the full rating distribution as parsed JSON for Walmart review 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 Reviews 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/reviews?api_key=YOUR_KEY&id=19075520026"import requests
r = requests.get(
"https://api.chocodata.com/api/v1/walmart/reviews",
params={"api_key": "YOUR_KEY", "id": "19075520026"},
timeout=90,
)
d = r.json()
print(d["overall_rating"], d["total_reviews"], d["rating_distribution"])
# 4.5 222 {'1': 11, '2': 8, '3': 3, '4': 28, '5': 172}After 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 the review block Walmart renders on the item page, which is 10 bodies plus a distribution covering every review. Unknown parameters are ignored rather than rejected, so page=2 returns HTTP 200 with the same ten reviews. Do not build a pagination loop on it; it will collect the same rows until you stop it.
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. This is the same bot check the free scraper in this repo met on 8 of 8 attempts on 2026-07-20. |
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 every documented 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/reviews?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/reviews?api_key=YOUR_KEY"{"error":"invalid_params","issues":[{"code":"custom","message":"either url or id is required","path":[]}]}A dead us_item_id returns 404 item_not_found with retryable: false. The message names the cause and confirms you were not charged: The target returned 404 for this request - the item or identifier does not exist. Check the id or URL. You were not charged.
A 502 does not mean your input was wrong: it is retryable: true, it was not billed, and the same id typically succeeds on the next attempt. Retry once after a few seconds, which is what the scripts in this repo do.
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/reviews",
params={"api_key": KEY, "id": item_id}, timeout=90)
d = r.json() if r.ok else {}
return item_id, d.get("overall_rating"), d.get("total_reviews")
items = ["19075520026", "504346078", "467647446", "15172921575"]
with ThreadPoolExecutor(max_workers=10) as pool: # <= your plan's concurrency
for item_id, rating, total in pool.map(one, items):
print(item_id, rating, total)Customer reviews for a single Walmart item: the review bodies Walmart renders on the item page, plus rating_distribution, which is the star histogram across every review the item has.
| 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/reviews?api_key=YOUR_KEY&id=19075520026"Real response. reviews cut to 1 of the 10 returned; the review object is complete, all 10 fields verbatim, text truncated where marked (full sample):
{
"us_item_id": "19075520026",
"overall_rating": 4.5,
"total_reviews": 222,
"rating_distribution": { "1": 11, "2": 8, "3": 3, "4": 28, "5": 172 },
"reviews": [
{
"review_id": "428901392",
"reviewer_name": "Colton",
"rating": 5,
"title": null,
"text": "I got this laptop for nursing school and I really like it. I usually buy HP but this Lenovo is lightweight, fast...",
"review_date": "6/17/2026",
"verified_purchase": true,
"positive_feedback": 0,
"negative_feedback": 0,
"badges": ["Verified Purchase"]
}
]
}rating_distribution is the field most people come for, and it is the reason to call this rather than read the headline number. It covers all 222 reviews even though 10 bodies come back, so a 4.5 built from 172 fives and 11 ones is legible as a different product from a flat 4.5. On an item whose ratings are more polarised the gap is wider still: item 467647446 averages 4.2 across 1,150 reviews, and 140 of them are one-star:
The ten bodies are a rating-ordered sample, not the ten most recent. Measured across four items on 2026-07-20 (full survey):
Ratings run high to low on 4 of 4 items, the dates are not in order, and item 504346078 has 1,912 reviews yet returns a set spanning August 2023 to July 2026, which the ten newest could not do. Three consecutive calls on that item returned the same ten review_ids in the same order, so the set is stable rather than sampled fresh per call. Build a "latest reviews" feed on review_date filtering rather than on arrival order, and use rating_distribution for anything about the item as a whole.
Two more things worth coding against, both visible in the committed samples:
titleis oftennull, and on some items it is null on every body. Across the four committed samples it is set on 7, 8, 8 and 0 of the 10 bodies; item15172921575returned no title at all (sample).textis the field you can rely on.textis passed through as Walmart stores it, and Walmart's own copy sometimes carries percent-encoded characters: one body inreviews_high_volume.jsonreads(10 %26 7)where an ampersand belongs. Decode defensively if you are running text analysis, and note that a bare%is also legitimate review text (I am not 100% sure...appears in the same sample).
Runnable: walmart_reviews_api_codes/reviews.py
Watching a product's rating move is the main reason people scrape Walmart reviews, so that use case is in the repo end to end rather than as a snippet. Track the counts rather than the average, because on a large base the average barely moves: 20 fresh one-star reviews on item 504346078 would raise its 1-star count from 113 to 133, a 17.7% jump, while pulling the 4.6 mean down by less than 0.05. rating_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_reviews_api_codes/rating_monitor.py 19075520026 504346078 467647446That is a real first run: nothing to compare against yet, so it seeds the database and says so. Run it again after reviews accrue and each change prints as a diff line (DOWN 4.6 -> 4.5 (-0.1) | +14 review(s) | 1-star +9).
The three items in that run make the point on their own. All three look similar at a glance (4.2 to 4.6), but the 1-star share runs 5.0%, 5.9% and 12.2%. That last product carries 140 one-star reviews out of 1,150, and its headline 4.2 does not say so.
Export the history with one command:
sqlite3 -header -csv walmart_reviews.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/reviews |
4.7s | 3.7 to 6.1s | 6 |
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=6); reproduce it yourself with the scripts here.
MIT. See LICENSE.








