Lowe's Scraper - Products, Prices, Stock & Reviews
Pricing
from $1.70 / 1,000 product records
Lowe's Scraper - Products, Prices, Stock & Reviews
Scrape Lowe's (lowes.com) home improvement, tools, appliances and building supplies by keyword search. Extract product names, brands, prices, descriptions, specifications, stock availability, ratings and customer reviews. Ideal for price monitoring, competitor analysis and retail market research.
Pricing
from $1.70 / 1,000 product records
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Security
Limited permissionsShare
Lowe's Product Scraper
Scrape Lowes.com product listings: keyword search, full product details, prices, stock, ratings and customer reviews. Lowe's is the second-largest US home-improvement retailer (tools, decor, building supplies, hardware), and this actor turns its catalog into clean structured rows for price monitoring, assortment analytics and competitor research.
Why This Scraper?
- Two ways in. Point it at keyword searches, or paste storefront links - search pages, category pages and product pages all walk with the same grammar.
- Deep product records. Every listing row carries title, brand, price (with was-price), the per-unit price when the storefront publishes one, stock quantity, rating, review count, badges, promotions and the spec table; detail enrichment adds the description, model number, barcode, availability, variants and the full image gallery.
- Customer reviews included. Rating summary, star distribution and up to 20 review texts per product, with verified-purchaser, recommendation and helpful-vote fields.
- Scheduled-run friendly. Incremental mode classifies every product NEW / UPDATED / UNCHANGED / EXPIRED across runs, so a daily monitor pushes only what moved; a product whose listing data did not change is neither read again nor charged. A price-and-availability scope, compact change rows and a grace period before EXPIRED keep a price monitor cheap.
- Resume that never double-charges. Paste the ID of an interrupted run and the products it already collected are skipped (and not billed again); a run the platform restarts continues by itself.
- Built for the wall. The storefront's edge refuses most scripted readers, so the actor reads the storefront's own listing and product data over residential US exits and moves to a fresh exit automatically whenever one is refused.
Use Cases
- Who: pricing teams tracking Lowe's promos on competitive assortments - price and was-price land per row (plus a per-unit price where the storefront lists one), updated per scheduled run.
- Who: sellers and brands auditing their shelf - badges, promotions, stock quantities and review velocity per product.
- Who: analysts building home-improvement datasets - walk any keyword search with the same grammar and export straight to CSV/Excel or a warehouse.
Data You Get
Sample shape: values are illustrative placeholders, not from a live record.
| Field | Example |
|---|---|
title / brand / brandId | 20-volt 1/2-in Keyless Brushless Cordless Drill / CRAFTSMAN / 722 |
price / wasPrice | 129.0 / null (a marked-down product shows its reference price) |
priceUnavailableReason | null (set only when price is null: no_standalone_offer or not_published, see Limitations) |
savingsAmount / savingsPercent / saleEndsAt | 50.0 / 27.0 / 2026-10-31T23:59:59.000Z (marked-down products only) |
priceInfo | The storefront's price block: display type, selling and was price, savings, sale window and every price row |
pricePerUnit | null (filled only when the storefront lists a unit price; the drill is each-priced) |
stockQuantity / inStock | 423 / true |
pickupEligible / parcelEligible / freeShipping | true / true / true |
inventory | Availability per delivery method (parcel, pickup, truck, same day): status, units, lead time |
promotions | Product, finance and shipping promotions attached to the product, with ids, names and dates |
modelId / itemNumber / lin / productId | CMCD714D1 / 6812916 / 1000742101 / 5016412045 |
barcode | 198706013438 |
rating / reviewsCount / badges | 4.9 / 96 / ["LOWES EXCLUSIVE"] |
specs / measurements | [{key: Cordless, value: Yes}, ...] / depth, width, height, weights with metric conversions |
description / marketingBullets / availability | Full product description text / highlights / In Stock |
images / videos / guides | Full image gallery, product videos, manuals and compliance documents |
variants | Every option of the variant selector (finish, size...) with the product it leads to |
categories / merchandisingHierarchy / breadcrumbs | Category ids, the business-area to assortment hierarchy, the shopper's category trail |
groups / collection | Collections and bundles the product belongs to, with links |
status / lowesExclusive / energyStar | ACTIVE / false / false |
storeNumber / storeInfo | 1539 (the store nearest the connection) / the store the prices were read for |
reviewsAverage / reviewsRecommended / ratingDistribution | 4.9 / 70 / [{stars: 5, count: 89}, ...] |
reviews | [{rating, title, text, author, verifiedPurchaser, ...}] |
changeType / changedFields | NEW / UPDATED / [price] (incremental mode) |
priceChanges / variantChanges | Old and new value of what moved (compact UPDATED and EXPIRED rows) |
How to Use
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("abotapi/lowes-com-scraper").call(run_input={"mode": "search","searchTerms": ["cordless drill"],"maxItems": 50,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["title"], item["price"])
JavaScript:
import { Actor } from 'apify';const run = await Actor.call('abotapi/lowes-com-scraper', {mode: 'urls',urls: ['https://www.lowes.com/search?searchTerm=drill'],maxItems: 50,});const { default: dataset } = await Actor.openDataset(run.defaultDatasetId);await dataset.forEach((item) => console.log(item.title, item.price));
Search by keyword:
{"mode": "search","searchTerms": ["cordless drill", "ceiling fan"],"maxItems": 50}
Default links: leaving the Storefront links field unchanged scrapes one example of a keyword search page and a product page. Category pages (the /pl/<name>/<id> links in the storefront's menus) are supported too: paste one into Storefront links.
Walk a search page, a category page and one product (reviews capped):
{"mode": "urls","urls": ["https://www.lowes.com/search?searchTerm=drill","https://www.lowes.com/pl/christmas-decorations/christmas-trees/4294417432","https://www.lowes.com/pd/CRAFTSMAN-20-volt-Max-1-2-in-Keyless-Brushless-Cordless-Drill-1-Batteries-Included-Soft-Bag-Included/5016412045"],"maxItems": 100,"maxReviews": 5}
Scheduled price and availability monitor (slim change rows, two-scan grace before EXPIRED):
{"mode": "search","searchTerms": ["cordless drill"],"maxItems": 0,"fetchDetails": false,"fetchReviews": false,"incrementalMode": true,"changeScope": "priceAndStock","compactUpdates": true,"emitExpired": true,"expireAfterMissedScans": 2}
Continue an interrupted run (nothing already collected is returned or billed again):
{"mode": "search","searchTerms": ["cordless drill"],"maxItems": 0,"resumeFromRunId": "ID_OF_THE_INTERRUPTED_RUN"}
Resume and scheduled monitoring
- Resume from a previous run continues ONE interrupted run. Paste its run ID or dataset ID: the products it already collected are skipped and not billed again. A run that the platform restarts (for example after a server move) continues by itself with no ID. If the ID cannot be read the run stops with a message instead of collecting everything again. Resume never produces EXPIRED rows, because a resumed walk does not prove the whole scope was seen.
- Incremental mode remembers the previous run of the same scope. The first run returns every product as NEW; later runs return NEW, UPDATED and REAPPEARED products only. Unchanged products are decided from the listing data alone, so they are not read again and not charged. The state is kept per scope (search terms or links, and the detail and review settings); caps, proxy and the incremental controls never start a new baseline. A State key names or deliberately shares a campaign.
- Resume together with Incremental mode finishes an interrupted monitoring run: the products it already returned are not returned again and are tracked as seen. A monitoring run that ends early (time limit, abort, server move) remembers the rows it already returned, so the next run does not return them again either.
- What counts as a change: price, was-price, savings and sale end, unit price, title, brand, model, in-stock, pickup and parcel availability, free shipping, packaging, badges, highlights, specifications, categories, collections and promotions. Never counted (still returned in the row): scrape time, the product's rank, the store nearest the connection, per-delivery-method availability detail, the category trail, and the counters stock units, review count and average rating, which tick between two reads of an unchanged product (turn on Count stock unit changes to compare the stock counter).
- Changes to watch
priceAndStocknarrows the comparison to price, was-price, savings, sale end, unit price, availability, free shipping and per-option price and stock, so a title, description or specification edit never produces a billable UPDATED row. - Compact UPDATED rows carry ids, link, title, current price and availability plus
priceChangesandvariantChanges(old and new). They carry no product-page detail or reviews, so no detail or review charge applies and the product page is not read. NEW rows stay full. - EXPIRED rows appear only after a run that read the whole scope to its end: never when Max items capped it, a page could not be read, the time or spending limit stopped it, or Resume was used. A product dropping out of a result list is not necessarily unavailable: set Expire after missed scans to 2 or more to hold it until it has been missing from that many complete scans in a row. EXPIRED rows count against Max items.
- Emit unchanged products returns the unchanged rows with the product-page details the last run collected for them. Each returned row is billed like any other row.
Tips
- The storefront re-ranks results between runs - incremental mode treats a moving
positionas noise and never re-bills it. - Search rows carry the storefront's network price (
storeNumberis null); product-page rows name the store nearest the connection instoreNumber, which incremental mode never counts as a change. - Flooring and other area-priced products show the storefront's per-square-foot price in
price, exactly as the listing displays it. - A product can show no price at all (roughly one product in four on a broad search). These are items the storefront does not offer on their own: the listing card says "See Product Page for Pickup and Delivery Options" and the product record marks the item unavailable, typically because it is a component of a kit or collection or is not sold individually. The product page, the product's data record and selecting another store or ZIP code were all checked on several stores across the country and none of them carries a price for such an item, so
pricestays null rather than borrowing a kit's price.priceUnavailableReasonsays why:no_standalone_offer(the storefront flags the item as not offered on its own) ornot_published(no price and no such flag). It is null wheneverpriceis set. The field is not part of the incremental comparison. - Every run stores a short summary under the key
OUTPUTof its key-value store: rows returned by change type, products scanned, whether every scope was read to its end, and whether the time or spending limit stopped it. - Max items counts the rows a run RETURNS, not the products it scans: with Incremental mode a run keeps walking the listing past unchanged products until it has returned that many changed rows (or reached the end), and Max items 0 means no limit.
breadcrumbs/categoryPathcome from the product page itself. When only the product's data record can be read, the trail stays empty, or keeps the one the previous incremental run stored for that product.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | select | urls | search walks keyword searches; urls walks pasted storefront links. |
searchTerms | string list | drill | Keyword searches to walk (search mode). Sending search terms while leaving the example links untouched runs the search terms only. |
urls | URL list | 2 example links | Search pages, category pages (/pl/<name>/<id>) and product pages (links mode). |
maxItems | integer | 20 | Maximum records per run (up to 100000). 0 means no limit. |
fetchDetails | checkbox | true | Read each product's detail page (description, model, barcode, gallery, breadcrumbs). |
fetchReviews | checkbox | true | Collect rating summary and review texts per product. |
maxReviews | integer | 20 | Review texts collected per product, 0 to 100. 0 skips texts. |
resumeFromRunId | string | (empty) | Run ID or dataset ID of an interrupted run to continue; its collected products are skipped and not billed again. |
incrementalMode | checkbox | false | Classify rows NEW/UPDATED/UNCHANGED/EXPIRED across scheduled runs. |
stateKey | string | (empty) | Names the incremental baseline; empty derives it from the scope. |
emitUnchanged | checkbox | false | Also return products that did not change, with their stored details. |
emitExpired | checkbox | false | Return EXPIRED rows for products that left a fully scanned scope. |
changeScope | select | all | priceAndStock compares only price, sale and availability. |
compactUpdates | checkbox | false | Slim UPDATED and EXPIRED rows with old and new values. |
expireAfterMissedScans | integer | 1 | Complete scans a product must be missing before it is EXPIRED (1 to 30). |
watchStockQuantity | checkbox | false | Also compare the stock-units counter. |
ignoreFieldsForChanges | list | (empty) | Field names (or dotted paths such as variants.image) that never count as a change. |
zipCode | string | (empty) | Not applied in this version: prices and stock come from the storefront's default store regardless of the connection country (see storeInfo). Leave empty. |
mcpConnectors | string list | (hidden) | Internal: connector ids that receive records as they push. |
notionParentPageUrl | string | (hidden) | Internal: parent page for the Notion connector. |
maxNotifyListings | integer | 50 (hidden) | Internal: cap on records sent to connectors per run. |
proxyConfiguration | proxy | Residential US | The storefront refuses scripted readers on datacenter exits; residential is the supported lane. When the selected country's exits keep being refused, the run continues on the same proxy group in other countries (see Plan Requirement). |
All fields are also accepted through the API exactly as documented, including the hidden internal ones.
Output Example
Sample shape: values are illustrative placeholders, not from a live record.
{"recordId": "lowes:5016412045","productId": "5016412045","title": "20-volt 1/2-in Keyless Brushless Cordless Drill","brand": "CRAFTSMAN","price": 129.0,"wasPrice": null,"currency": "USD","stockQuantity": 423,"rating": 4.9,"reviewsCount": 96,"badges": ["LOWES EXCLUSIVE"],"availability": "In Stock","url": "https://www.lowes.com/pd/CRAFTSMAN-20-volt-Max-1-2-in-Keyless-Brushless-Cordless-Drill/5016412045"}
Product rows with the detail enrichment on, from a live search walk.
Plan Requirement
This actor requires an Apify paid plan: the storefront's edge refuses datacenter exits, so every run reads pages through residential proxies, which are not available on the Apify Free plan.
Runs start on the proxy country you select (US by default). The storefront's refusals come in waves per country, so when the selected country's exits keep being refused, the run continues on residential exits of the same proxy group in other countries; the storefront answers those with the same US catalog, prices and stock, and the run log says "Served over the backup connection" when that happened. Proxy usage is billed by your Apify plan exactly as for the selected country.
FAQ
Is it legal to scrape Lowes.com?
The actor collects publicly available product data from the storefront's own pages, at a polite pace, without logging into any account. Review your use case and the site's terms before running at scale.
Why does a run fail with a connection problem?
The storefront denies individual proxy exits intermittently, in waves. The actor moves to a fresh exit automatically, falls back to residential exits in other countries, and waits through short deny windows; a run that still reports a connection problem hit a fully closed window - retry later, the same input usually succeeds on the next run.
How much does a run cost?
You pay the actor's per-event pricing, plus your Apify plan's proxy usage. The charged events, exactly as the code bills them:
- Product record - once per product row pushed to the dataset.
- Detail enrichment - once per pushed row whose product page was read (
fetchDetailson). - Customer reviews - once per pushed row that delivered at least one review text (not per review).
Every row the run returns bills the product record, whatever its kind: a NEW, UPDATED, compact UPDATED, UNCHANGED (only when you ask for them) or EXPIRED row. Rows that are not returned bill nothing: unchanged products in incremental mode are decided from the listing alone, so they cost no product-page read and no charge, and products skipped by Resume are not billed again. A compact UPDATED row carries no product-page detail or reviews, so it bills the record only. A run that reaches your spending limit stops before the next row is stored and says so; nothing is charged for a row that was not stored. Card-only rows (details off) and rows without reviews bill no enrichment event. Product details load on most but not all connections: when neither the product page nor the product's data record can be read after several fresh connections, a search-result row still ships with its listing fields only and no detail enrichment event is charged; a pasted product link has no listing row to fall back to, so it ships nothing and charges nothing. See the actor's Pricing tab for the registered event prices.
Can I track price changes daily?
Yes - schedule the run with incrementalMode on and the same inputs. Only NEW and UPDATED rows push, each with changedFields naming what moved. For a pure price monitor set changeScope to priceAndStock and compactUpdates on: a title or description edit then never produces a billable row, and each UPDATED row shows the old and new price.
What happens if a monitoring run is interrupted?
The rows a run returned before it ended are remembered (a run that reaches its time limit finishes cleanly with them, and a run that is aborted or moved by the platform saves them as it ends), so the next run does not return them again, and Resume skips the products an interrupted run already collected. If a run is killed outright without notice (for example out of memory), the last few rows it returned can come back once more on the next run; pasting that run's ID into Resume closes even that gap.
Send results into your apps (MCP connectors)
Connect the actor to Notion, Google Sheets and other MCP-capable apps from the Apify Console: records this run pushed are delivered as they land, capped per run. Connector settings ship hidden in the input and need no attention unless you use connectors.
๐ More scrapers you might like
Pair this actor with these related scrapers from the same team:
| ๐ Kohl's Scraper Scrape Kohl's (kohls.com) products by keyword search or category browsing. Extract... | ๐งฉ CVS Scraper Scrape CVS.com pharmacy, beauty, health and personal care products by keyword search... |
| ๐ Bunnings Scraper Scrape bunnings.com.au products with full specifications, price, brand, stock, image... | ๐ The Warehouse NZ Scraper Scrape The Warehouse New Zealand search results, category pages, and direct product URLs... |
| ๐งฉ Joom Scraper Scrape Joom products by keyword or URL. Extract product names, prices, discounts... | ๐งฉ JB Hi-Fi AU Scraper Scrape JB Hi-Fi Australia (jbhifi.com.au) products from the public storefront. Extract... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.