Instacart Scraper - Grocery Prices, Products & Stores avatar

Instacart Scraper - Grocery Prices, Products & Stores

Pricing

from $1.50 / 1,000 product or store results

Go to Apify Store
Instacart Scraper - Grocery Prices, Products & Stores

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

Abot API

Maintained by Community

Actor 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. onSale is 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

FieldExample
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
price8.94, as a number for filtering and arithmetic
priceString"$8.94", as the store renders it, with currency
fullPriceregular price, present only while the item is discounted
onSalefalse
unitPriceprice per unit as a number, where the store publishes it
unitPriceString"$0.21/oz", price per unit as rendered
pricingUnitthe unit the unit price is quoted in
availabletrue
stockLevel"highlyInStock", machine-readable
stockLevelLabel"Many in stock", the store's own wording
dietaryAttributesorganic, 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
imageproduct 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)
sourceUrlthe 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

  1. Pick a mode: search (keywords across one or more stores), store (discover stores near a location) or url (paste Instacart links).
  2. 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.
  3. Turn on any filters, sort order or Fetch product details you need, then set Max products to control run size and cost.
  4. 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 ApifyClient
client = 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.

  1. Authorize a connector under Apify, Settings, Integrations.
  2. Select it in mcpConnectors.
  3. For Notion, also set notionParentPageUrl to the page new item pages are created under. Other connectors ignore it.
  4. maxNotifyListings caps 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

ParameterTypeDefaultDescription
modestringsearchsearch, store or url.
queriesarrayno default (prefill: organic milk)Keywords to search; each keyword is run at every store you list.
retailersarrayno default (prefill: costco, safeway)Store names as they appear in an Instacart address; empty searches every nearby store.
urlsarrayno default (prefill: sample links)Links or bare product ids, url mode.
includeDepartmentsbooleanfalseAdd each store's departments, store mode only.
postalCodestring10001US ZIP the prices are quoted for; alone is enough to run.
latitudenumberno default (auto from postalCode)Optional override, a more precise point than the ZIP centre; set together with longitude.
longitudenumberno default (auto from postalCode)Optional override, same as above; set together with latitude.
sortBystringbestMatchAlso priceAsc, priceDesc, unitPriceAsc, unitPriceDesc.
inStockOnlybooleanfalseKeep only available products.
onSaleOnlybooleanfalseKeep only discounted products.
minPriceintegerno default (optional)Lowest price to keep, USD. Products the store quoted no price for are excluded when this is set.
maxPriceintegerno default (optional)Highest price to keep, USD. Products the store quoted no price for are excluded when this is set.
fetchDetailsbooleanfalseAdd category, description and canonical name by loading the product page; charged per product.
maxItemsinteger20The run's only cap; 0 is unlimited.
resumeFromRunIdstringno default (optional)Continue one interrupted run.
incrementalModebooleanfalseReturn only what changed since last run.
stateKeystringno default (optional)Name a monitoring campaign, or share one across runs.
emitUnchangedbooleanfalseAlso return, and bill, unchanged rows.
emitExpiredbooleanfalseAlso return, and bill, vanished rows; only after a complete scan.
proxyobjectUS residentialApify Proxy is required; keep the default residential US selection.
mcpConnectorsarrayno default (optional)Optional export to your apps.
notionParentPageUrlstringno default (optional)Notion connector only.
maxNotifyListingsinteger50Export 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.

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.