Instacart Scraper - Grocery Prices, Products & Stores
Pricing
from $1.50 / 1,000 product or store results
Instacart Scraper - Grocery Prices, Products & Stores
Scrape Instacart grocery catalogs with per-store prices. Run keywords across several stores at once to compare what each banner charges, discover every store serving a US location, or paste Instacart links. Returns name, brand, size, price, regular price, unit price, stock and full store detail.
Pricing
from $1.50 / 1,000 product or store results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
7
Total users
4
Monthly active users
6 days ago
Last modified
Categories
Share
Instacart Scraper: Grocery Prices, Unit Prices & Stores
Instacart is a local marketplace: the same box of cereal costs one thing at Costco and another at Safeway, and which stores even exist depends on where you are shopping from. This actor scrapes the Instacart grocery catalog with the price each store actually quotes, in three modes: run a keyword across one or more stores to compare what each banner charges, discover every store serving a US postal code, or paste Instacart links directly. Export the results as JSON, CSV or Excel, or read them through the API.
Why This Scraper?
- Prices from several stores in one run. One keyword is run at every store you name, so the output is a price comparison rather than one store's list.
- Every price carries its store. Banner, service type and store id ride on every row; the branch's street address also rides along when stores are named in the Stores field, because a grocery price without its store is not a fact about anything.
- Real unit prices, where published. Price per ounce, pound or count, plus a unit price ordering, so different pack sizes are actually comparable when the store quotes them.
- Sale detection that means something.
onSaleis set from the regular price the store is striking through, not guessed from a badge. - Store discovery built in. Ask which stores serve a postal code before you decide what to scrape, with departments, coordinates and postal addresses.
- Change monitoring that names what moved. Schedule it and get only new and changed rows, with the changed fields named, so a price change is one row rather than a fresh catalog.
Use Cases
- Grocery price comparison shoppers: run one keyword at every nearby store to find the cheapest banner before placing a delivery or pickup order.
- Recipe and meal-planning apps: check ingredient prices and stock across retailers for a shopping list.
- Retail and pricing analysts: track how prices and unit prices move store by store over time.
- Coupon and deal-alert tools: watch a keyword list and get notified only when something goes on or off sale.
- Researchers and market analysts: build a grocery price index across US postal codes.
Data You Get
| Field | Example |
|---|---|
recordId | "100000|items_500000-20000001", unique per store and product |
name | "Store Brand Whole Milk, 1 Gallon, 2-count" |
brand | "store brand" |
size | "each", pack size as the store states it |
price | 8.94, as a number for filtering and arithmetic |
priceString | "$8.94", as the store renders it, with currency |
fullPrice | regular price, present only while the item is discounted |
onSale | false |
unitPrice | price per unit as a number, where the store publishes it |
unitPriceString | "$0.21/oz", price per unit as rendered |
pricingUnit | the unit the unit price is quoted in |
available | true |
stockLevel | "highlyInStock", machine-readable |
stockLevelLabel | "Many in stock", the store's own wording |
dietaryAttributes | organic, gluten free and similar, as published |
retailerName | "Costco" |
retailerSlug | "costco", store name as it appears in an Instacart address |
shopId | "100000", numeric storefront id |
serviceType | "delivery" (also "pickup") |
storeAddress | "500 Example Ave, San Francisco, CA 94105", when stores are named in the Stores field |
productId | "20000001", product id at that store |
image | product image link |
productUrl | "https://www.instacart.com/store/costco/products/20000001" |
searchQuery | "organic whole milk" (search mode and url links that resolve to a search) |
sourceUrl | the public link the row was found through |
A few secondary fields ride along without a table row of their own: retailerId and storeLocationId are the numeric ids behind retailerSlug and storeAddress; tags carries source tags such as storeBrand; boughtCountLabel and fsaHsaEligibleLabel are rolling, store-written labels that are deliberately excluded from change detection because they move on their own, with no price behind the move. Turning on Fetch product details adds category and canonicalName to product rows where the product page publishes them, plus unitCount, description or detailImage when the product page happens to publish those too; a nutrition panel is not promised, since the source ships one for only some products, and store rows never go through this step. Incremental mode adds changeType, changedFields, firstSeenAt and lastSeenAt. Store mode returns store rows instead of product rows, carrying recordId, recordType, shopId, retailerId, retailerSlug, retailerName, retailerType, serviceType, address, latitude, longitude, categories and storefrontUrl, plus departments and departmentCount when Include departments is on.
How to Use
- Pick a mode:
search(keywords across one or more stores),store(discover stores near a location) orurl(paste Instacart links). - Set your US postal code, and list the queries or retailers for search mode, or paste links for url mode. The postal code alone is enough to run.
- Turn on any filters, sort order or Fetch product details you need, then set Max products to control run size and cost.
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.
Compare one keyword across two stores:
{"mode": "search","queries": ["organic whole milk"],"retailers": ["costco", "safeway"],"postalCode": "10001","sortBy": "unitPriceAsc","maxItems": 40}
Find out which stores serve a location, with departments:
{"mode": "store","includeDepartments": true,"postalCode": "94105","maxItems": 50}
Scrape pasted links, with the product category added:
{"mode": "url","urls": ["https://www.instacart.com/store/costco/s?k=greek+yogurt","https://www.instacart.com/store/costco/products/20000001"],"fetchDetails": true,"maxItems": 25}
Monitor sale prices daily and get only the changes:
{"mode": "search","queries": ["coffee"],"retailers": ["safeway"],"onSaleOnly": true,"incrementalMode": true,"stateKey": "coffee-watch","maxItems": 0}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/instacart-grocery-price-scraper").call(run_input={"mode": "search","queries": ["organic whole milk"],"retailers": ["costco", "safeway"],"postalCode": "10001",})for product in client.dataset(run["defaultDatasetId"]).iterate_items():print(product["name"], product["retailerName"], product["priceString"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/instacart-grocery-price-scraper').call({mode: 'search',queries: ['organic whole milk'],retailers: ['costco', 'safeway'],postalCode: '10001',});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
A store name, and your postal code
A store name is the part of a store address after /store/: in instacart.com/store/safeway/storefront the name is safeway.
Your postal code alone is enough. Its coordinates are looked up automatically, from a bundled US ZIP-code table, so there is no separate lookup step and no external geocoding call. Latitude and longitude are optional and only worth setting for a point more precise than a ZIP code's centre, or to shop from somewhere the postal code doesn't describe. When you do set them, they decide WHICH stores come back while the postal code still selects the pricing context those stores quote in, so keep the two in agreement; the actor warns if they land more than 50 miles apart.
One page per keyword, per store
The source serves a single page of results for a keyword at a store and publishes no way to page past it. In practice a narrow keyword ("greek yogurt") returns fewer, more exact matches than a broad one ("yogurt"), and how many that is varies a lot by store and category. When more results exist than the page carries, the run says so in the log and in the run status rather than quietly returning less. Narrow the keyword to reach the rest.
A few other limits are also worth knowing: department pages are not served to this actor by the source, so a pasted /collections/ link is skipped with a message; use search mode or a pasted search link for that store's products instead. Instacart publishes no customer review surface for products, so no review fields are returned. The source serves US storefronts only.
Resume and recurring updates
Two different things. Resume (resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor skips every record already collected there, so you don't pay twice. Incremental mode (incrementalMode) is for running the same search again and again, for example daily, and getting only what changed. The first run returns everything as NEW. Later runs return only NEW and UPDATED (with changedFields naming what moved) by default, plus REAPPEARED only in the case below. Turn on emitUnchanged or emitExpired to also get, and be billed for, rows that did not change or that disappeared.
REAPPEARED and EXPIRED are tied together and neither happens by default. A row is only ever marked EXPIRED by a run that had emitExpired on and fully scanned the tracked search end to end: no cap hit, no Resume, no keyword with more results than the source serves in one page, and no target refused or errored along the way. Only a row that was previously tombstoned EXPIRED by such a run can later come back as REAPPEARED; a row that was simply never marked EXPIRED in the first place cannot reappear. Resume and Incremental mode cannot be turned on together, since Resume deliberately skips records it already has, which would make Incremental mode report those records as missing.
Send results into your apps (MCP connectors)
Results can be piped into the apps you already use, over Model Context Protocol, without touching the dataset output.
- Authorize a connector under Apify, Settings, Integrations.
- Select it in
mcpConnectors. - For Notion, also set
notionParentPageUrlto the page new item pages are created under. Other connectors ignore it. maxNotifyListingscaps how many items are written per connector per run. It does not affect the dataset.
Supported: Notion, Linear, Airtable, Apify. Leave mcpConnectors empty to skip. An export that fails never fails the run.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search, store or url. |
queries | array | no default (prefill: organic milk) | Keywords to search; each keyword is run at every store you list. |
retailers | array | no default (prefill: costco, safeway) | Store names as they appear in an Instacart address; empty searches every nearby store. |
urls | array | no default (prefill: sample links) | Links or bare product ids, url mode. |
includeDepartments | boolean | false | Add each store's departments, store mode only. |
postalCode | string | 10001 | US ZIP the prices are quoted for; alone is enough to run. |
latitude | number | no default (auto from postalCode) | Optional override, a more precise point than the ZIP centre; set together with longitude. |
longitude | number | no default (auto from postalCode) | Optional override, same as above; set together with latitude. |
sortBy | string | bestMatch | Also priceAsc, priceDesc, unitPriceAsc, unitPriceDesc. |
inStockOnly | boolean | false | Keep only available products. |
onSaleOnly | boolean | false | Keep only discounted products. |
minPrice | integer | no default (optional) | Lowest price to keep, USD. Products the store quoted no price for are excluded when this is set. |
maxPrice | integer | no default (optional) | Highest price to keep, USD. Products the store quoted no price for are excluded when this is set. |
fetchDetails | boolean | false | Add category, description and canonical name by loading the product page; charged per product. |
maxItems | integer | 20 | The run's only cap; 0 is unlimited. |
resumeFromRunId | string | no default (optional) | Continue one interrupted run. |
incrementalMode | boolean | false | Return only what changed since last run. |
stateKey | string | no default (optional) | Name a monitoring campaign, or share one across runs. |
emitUnchanged | boolean | false | Also return, and bill, unchanged rows. |
emitExpired | boolean | false | Also return, and bill, vanished rows; only after a complete scan. |
proxy | object | US residential | Apify Proxy is required; keep the default residential US selection. |
mcpConnectors | array | no default (optional) | Optional export to your apps. |
notionParentPageUrl | string | no default (optional) | Notion connector only. |
maxNotifyListings | integer | 50 | Export cap per connector, per run. |
Output Example
{"recordId": "100000|items_500000-20000001","itemId": "items_500000-20000001","productId": "20000001","name": "Store Brand Whole Milk, 1 Gallon, 2-count","brand": "store brand","size": "each","priceString": "$8.94","price": 8.94,"onSale": false,"available": true,"stockLevel": "highlyInStock","stockLevelLabel": "Many in stock","retailerName": "Costco","retailerSlug": "costco","retailerId": "900000","shopId": "100000","storeLocationId": "500000","serviceType": "delivery","storeAddress": "500 Example Ave, San Francisco, CA 94105","image": "https://d2lnr5mha7bycj.cloudfront.net/product-image/file/large_00000000.jpg","productUrl": "https://www.instacart.com/store/costco/products/20000001","sourceUrl": "https://www.instacart.com/store/costco/s?k=organic+whole+milk","category": "Plain Milk","canonicalName": "Store Brand Homogenized Milk, 2 gal","unitCount": "2","searchQuery": "organic whole milk","scrapedAt": "2026-09-29T06:01:44+00:00"}
Plan Requirement
Apify Proxy is required, and the default US residential selection is the supported setting. This source checks the connection before it will serve a storefront: switching to the standard pool is a run-time option, but it is measured to serve only a small fraction of connections, so results may be empty or a run may stop early. The actor tells you once, up front, when a run is not using the default.
What you are charged for (the Pricing tab shows the current rates):
- Product or store result, charged for each record in the dataset.
- Product detail, charged once per product, only when Fetch product details is on, for the extra request that adds the category, description and canonical name. Off by default; the fetch and the charge are both gated on this one toggle, whether the product came from a search or from a pasted link.
- Actor start, charged once per run, per GB of memory.
A record suppressed by incremental mode is never charged, even when its detail page was already fetched to work out that nothing changed.
FAQ
How much does it cost?
You pay per record returned, with the optional product-detail lookup billed only when you switch it on. The Pricing tab shows the current rates. Use Max products to cap the cost of any run.
Is it legal to scrape Instacart?
This actor collects only publicly available grocery listing data. You are responsible for how you use it: follow Instacart's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.
Can I get only price changes on a schedule?
Yes. Schedule the actor from the Schedules tab with the same search and turn on Incremental mode. Each run then returns only new and updated rows, and unchanged ones are not billed unless you turn emitUnchanged on.
Why did I get fewer results than I expected?
The source serves one page of results per keyword per store and publishes no way to page past it. A broad keyword like "yogurt" is capped at that one page just as a narrow one is, so it can return proportionally fewer of the store's actual matches. Narrow the keyword (for example "greek yogurt" rather than "yogurt") to reach more of what a store carries, and check the run log: it names any keyword or store combination where more results existed than could be read.
Why did my run fail instead of returning an empty dataset?
If the source refuses every request, or every keyword and store target errors out before returning anything, the run fails with a clear message, since no real search was ever completed. A search that genuinely matches nothing, or a store that genuinely has nothing in stock, is reported as a quiet, successful, empty result instead, so "no products matched" is never confused with "nothing could be read".
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
π Want more grocery data?
Pair this actor with these related scrapers from the same team:
| π₯¦ FoodHero Scraper Scrape FoodHero surplus grocery deals across Canada by area, store or offer ID. Extract... | π₯¦ Woolworths AU Scraper Scrape Woolworths Australia (woolworths.com.au) grocery products and reviews. Search by... |
| π₯¦ E.Leclerc Scraper Scrape E.Leclerc France (e.leclerc) grocery and retail products. Search by keyword or... | π₯¦ Auchan France Scraper Scrape Auchan.fr products: grocery, drinks, household and general merchandise. Real price... |
| π₯¦ REWE.de Scraper Scrape REWE Germany (rewe.de) grocery products: current price, was-price and discount... | π₯¦ Mercadona Scraper Scrape Mercadona products by delivery-area postal code. Search by keyword or category, or... |
π Browse all abotapi scrapers
π¬ Support & custom scrapers
- π 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 abotapi@proton.me or message Telegram @abotapi.
- β Enjoying it? A quick review on the actor page helps other users find it.