# Instacart Scraper - Grocery Prices, Products & Stores (`abotapi/instacart-grocery-price-scraper`) Actor

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.

- **URL**: https://apify.com/abotapi/instacart-grocery-price-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 product or store results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Instacart Grocery Catalog & Price Scraper

Scrape the Instacart grocery catalog with the price each store actually quotes.

Instacart is a local marketplace: the same box of cereal costs one thing at
Costco and another at Safeway, and which stores exist at all depends on where
you are shopping from. This actor is built around that fact.

### 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, store id and the
  branch's street address ride on each row, because a grocery price without its
  store is not a fact about anything.
- **Real unit prices.** Price per ounce, pound or count where the store
  publishes it, plus a unit price ordering, so different pack sizes are actually
  comparable.
- **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 an address before you
  decide what to scrape, with departments, coordinates and postal addresses.
- **Change monitoring.** Schedule it and get only what moved, with the changed
  fields named, so a price change is one row rather than a fresh catalog.
- **Honest limits.** Where the source caps what it serves, the run says so in
  the log and in the run status instead of quietly returning less.

### Data You Get

| Field | Description |
|---|---|
| `recordId` | Stable identity for the row, unique per store and product |
| `name` | Product name as the store lists it |
| `brand` | Brand name |
| `size` | Pack size as the store states it |
| `price` | Current price as a number, for filtering and arithmetic |
| `priceString` | Current price as the store renders it, with currency |
| `fullPrice` | Regular price, present only while the item is discounted |
| `onSale` | Whether the store is currently discounting the item |
| `unitPrice` | Price per unit as a number, where published |
| `unitPriceString` | Price per unit as rendered, for example `$0.21/oz` |
| `pricingUnit` | The unit the unit price is quoted in |
| `available` | Whether the store currently reports the item in stock |
| `stockLevel` | Machine-readable stock level |
| `stockLevelLabel` | The store's own wording, for example `Many in stock` |
| `dietaryAttributes` | Organic, gluten free and similar, as published |
| `tags` | Source tags such as `storeBrand` |
| `retailerName` | The store the price was quoted at |
| `retailerSlug` | Store name as it appears in an Instacart address |
| `shopId` | Numeric storefront id |
| `serviceType` | `delivery` or `pickup` |
| `storeAddress` | Street address of the branch quoting the price |
| `productId` | Product id at that store |
| `image` | Product image link |
| `productUrl` | Public product page link |
| `searchQuery` | The keyword that surfaced this row |

Turning on product details adds `category`, `canonicalName`, `description` and
`unitCount`. Incremental mode adds `changeType`, `changedFields`, `firstSeenAt`
and `lastSeenAt`. Store mode returns store rows instead, carrying
`retailerName`, `retailerType`, `serviceType`, `address`, `latitude`,
`longitude`, `categories`, `storefrontUrl` and optionally `departments`.

### How to Use

Compare one product across two stores in Manhattan -- a postal code is all the
location this needs:

```json
{
  "mode": "search",
  "queries": ["organic whole milk"],
  "retailers": ["costco", "safeway"],
  "postalCode": "10001",
  "sortBy": "unitPriceAsc",
  "maxItems": 40
}
```

Find out which stores serve an address first:

```json
{
  "mode": "store",
  "includeDepartments": true,
  "postalCode": "94105",
  "maxItems": 50
}
```

Scrape pasted links, with the product category added:

```json
{
  "mode": "url",
  "urls": [
    "https://www.instacart.com/store/costco/s?k=greek+yogurt",
    "https://www.instacart.com/store/costco/products/19208138"
  ],
  "fetchDetails": true,
  "maxItems": 25
}
```

Monitor sale prices daily and get only the changes:

```json
{
  "mode": "search",
  "queries": ["coffee"],
  "retailers": ["safeway"],
  "onSaleOnly": true,
  "incrementalMode": true,
  "stateKey": "coffee-watch",
  "maxItems": 0
}
```

**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 -- no separate lookup step, no external geocoding call, just a
bundled US ZIP-code table. `latitude`/`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.

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search`, `store` or `url` |
| `queries` | array | `["organic whole milk"]` | Keywords, search mode |
| `retailers` | array | `["costco","safeway"]` | Store names; empty searches every nearby store |
| `urls` | array | see prefill | Links or bare product ids, url mode |
| `includeDepartments` | boolean | `false` | Add each store's departments, store mode |
| `postalCode` | string | `10001` | US ZIP the prices are quoted for; alone is enough to run |
| `latitude` | number | auto from postalCode | Optional override, a more precise point than the ZIP centre |
| `longitude` | number | auto from postalCode | Set together with latitude; optional, same as above |
| `sortBy` | string | `bestMatch` | Also `priceAsc`, `priceDesc`, `unitPriceAsc`, `unitPriceDesc` |
| `inStockOnly` | boolean | `false` | Keep only available products |
| `onSaleOnly` | boolean | `false` | Keep only discounted products |
| `minPrice` | integer | unset | Lowest price to keep, USD |
| `maxPrice` | integer | unset | Highest price to keep, USD |
| `fetchDetails` | boolean | `false` | Add category and description; charged per product |
| `maxItems` | integer | `20` | The run's only cap; `0` is unlimited |
| `resumeFromRunId` | string | unset | Continue one interrupted run |
| `incrementalMode` | boolean | `false` | Return only what changed since last run |
| `stateKey` | string | unset | Name a monitoring campaign |
| `emitUnchanged` | boolean | `false` | Also return, and bill, unchanged rows |
| `emitExpired` | boolean | `false` | Also return, and bill, vanished rows |
| `proxy` | object | US residential | Required; keep the default selection |
| `mcpConnectors` | array | unset | Optional export to your apps |
| `notionParentPageUrl` | string | unset | Notion connector only |
| `maxNotifyListings` | integer | `50` | Export cap per connector, per run |

### Output Example

```json
{
  "recordId": "12|items_74-19208138",
  "itemId": "items_74-19208138",
  "productId": "19208138",
  "name": "Kirkland Signature Whole Milk, 1 Gallon, 2-count",
  "brand": "kirkland signature",
  "size": "each",
  "priceString": "$8.94",
  "price": 8.94,
  "onSale": false,
  "available": true,
  "stockLevel": "highlyInStock",
  "stockLevelLabel": "Many in stock",
  "tags": ["storeBrand"],
  "retailerName": "Costco",
  "retailerSlug": "costco",
  "retailerId": "5",
  "shopId": "12",
  "storeLocationId": "74",
  "serviceType": "delivery",
  "storeAddress": "123 Main St, San Francisco, CA 94103",
  "image": "https://d2lnr5mha7bycj.cloudfront.net/product-image/file/large_24511800.jpg",
  "productUrl": "https://www.instacart.com/store/costco/products/19208138",
  "category": "Plain Milk",
  "canonicalName": "Kirkland Signature Homogenized Milk, 2 gal",
  "unitCount": "2",
  "searchQuery": "organic whole milk",
  "scrapedAt": "2026-09-13T06:01:44+00:00"
}
```

### 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, API & 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.

#### Limits, stated plainly

- **One page of results per keyword, per store.** The source serves a single page
  for a keyword and publishes no way to page past it, in practice roughly 30 to
  180 products depending on the store. When more exist, the run says so. Narrow
  the keyword to reach the rest: `greek yogurt` rather than `yogurt`.
- **Department browsing is not available.** Department pages are served by a
  route the source does not open to this actor, so a pasted `/collections/` link
  is skipped with a message rather than silently returning nothing. Store mode
  can still list a store's department names and links.
- **No reviews or ratings.** Instacart publishes no customer review surface for
  products, so no review fields are returned. That is an absence of data, not an
  omission.
- **Nutrition is not promised.** The source ships a nutrition panel for only some
  products, so the detail lookup returns category, description and canonical name
  rather than a field that would be empty on most rows.
- **US only.** The source serves US storefronts.

#### Recurring monitoring, and what counts as a change

The first run returns everything as `NEW`. Later runs return only `NEW`,
`UPDATED` and `REAPPEARED`, with `changedFields` naming exactly what moved. A
price change is one row saying `price` changed.

`EXPIRED` rows are produced only after a run that fully scanned the tracked
search, so never when the cap stopped it, when Resume was used, or when a keyword
had more results than the source serves at once. A partial run cannot tell "gone"
from "not reached".

The keyword that surfaced a row is not treated as a change, so widening your
keyword list does not re-bill your whole catalog as updated.

### Plan Requirement

Apify Proxy is required, and the US residential selection is the supported
setting. This source checks the connection before it will serve a storefront:
measured on 2026-09-13, 1 of 6 standard connections was served against 2 of 2 US
residential ones. You can override it, and the run tells you once what that
means.

What you are charged for (the Store page 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.
- **Actor start**, charged once per run, per GB of memory.

A record suppressed by incremental mode is never charged, including when its
detail page was already fetched to work out that nothing changed.

# Actor input Schema

## `mode` (type: `string`):

Choose 'search' to run keywords against one or more stores and get each store's own prices, 'store' to list every store serving your location, or 'url' to scrape pasted Instacart links (store pages, search links, and product links are all accepted).

## `queries` (type: `array`):

One or more keywords to search, for example 'organic milk' or 'greek yogurt'. Each keyword is searched separately at each store. A narrower keyword returns fewer matches, so start broad and narrow down.

## `retailers` (type: `array`):

Store names as they appear in an Instacart address, for example 'costco', 'safeway', 'kroger', 'publix'. Copy one from a store page address: instacart.com/store/STORE-NAME/storefront. Leave empty to search EVERY store serving your location, which can be a few hundred stores, so set Max products before you do that.

## `urls` (type: `array`):

Mix freely: search links such as https://www.instacart.com/store/costco/s?k=milk , product links such as https://www.instacart.com/store/costco/products/19208138 , store pages such as https://www.instacart.com/store/safeway/storefront , or bare product ids such as 19208138. Department links (/collections/...) are refused by the source for this actor and are skipped with a message.

## `includeDepartments` (type: `boolean`):

Add each store's own department list (produce, dairy, frozen and so on) with the department link, to every store record. Costs one extra request per store.

## `postalCode` (type: `string`):

The US ZIP code to shop from, for example 10001. This alone is enough to run a search. Defaults to 10001 (Manhattan).

## `latitude` (type: `number`):

Only needed for a point more precise than the postal code's own centre, or to shop from a different area than the postal code you gave. Leave empty to use the postal code's coordinates automatically. Must be set together with Longitude.

## `longitude` (type: `number`):

Only needed for a point more precise than the postal code's own centre, or to shop from a different area than the postal code you gave. Leave empty to use the postal code's coordinates automatically. Must be set together with Latitude.

## `sortBy` (type: `string`):

Ordering. This is the source's own ordering, sent with the search request, so the results come back already ranked. Across several stores the combined list is re-ordered the same way.

## `inStockOnly` (type: `boolean`):

Keep only products the store currently reports as available.

## `onSaleOnly` (type: `boolean`):

Keep only products the store is currently discounting, that is, products showing a regular price above the current price.

## `minPrice` (type: `integer`):

Optional. Keep only products priced at or above this amount. Products the store quoted no price for are excluded when this is set.

## `maxPrice` (type: `integer`):

Optional. Keep only products priced at or below this amount. Products the store quoted no price for are excluded when this is set.

## `fetchDetails` (type: `boolean`):

Add the source's own product category, the written description and the canonical product name to each product, by loading its product page. Costs one extra request per product and is charged per product. Nutrition panels are NOT promised: the source publishes them for only some products.

## `maxItems` (type: `integer`):

Maximum number of records to return across the whole run. This is the run's only cap. Use 0 for unlimited, but note that leaving Stores empty searches every store serving your location.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large crawl without returning or charging for records already collected there. Use this after an interrupted run. For recurring monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for recurring price monitoring. The first run returns everything as NEW. Later runs return only NEW, UPDATED and REAPPEARED records, so a daily run shows you the price moves and nothing else. Turn on 'Emit unchanged' or 'Emit expired' only when you also want those rows returned (and billed). State is kept separately for each keyword, store, location and detail setup; use State key to name or deliberately share a campaign.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. Leave empty to let the actor derive a key from the keywords, stores, location and settings, so two different searches never mix state.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products whose price and details have not moved since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced after a run that fully scanned the tracked search, so never when Max products capped it, when Resume was used, or when a keyword had more results than the source serves in one request. This returns, and bills, extra synthetic rows.

## `proxy` (type: `object`):

Keep the default US residential selection for reliable results. The source serves US storefronts only, so the country is pinned to the United States.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page per item; other connectors get a best effort write or digest. Leave empty to skip; this never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "organic milk"
  ],
  "retailers": [
    "costco",
    "safeway"
  ],
  "urls": [
    "https://www.instacart.com/store/costco/s?k=greek+yogurt",
    "https://www.instacart.com/store/costco/products/19208138"
  ],
  "includeDepartments": false,
  "postalCode": "10001",
  "sortBy": "bestMatch",
  "inStockOnly": false,
  "onSaleOnly": false,
  "fetchDetails": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `stores` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "queries": [
        "organic milk"
    ],
    "retailers": [
        "costco",
        "safeway"
    ],
    "urls": [
        "https://www.instacart.com/store/costco/s?k=greek+yogurt",
        "https://www.instacart.com/store/costco/products/19208138"
    ],
    "includeDepartments": false,
    "postalCode": "10001",
    "inStockOnly": false,
    "onSaleOnly": false,
    "fetchDetails": false,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/instacart-grocery-price-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "queries": ["organic milk"],
    "retailers": [
        "costco",
        "safeway",
    ],
    "urls": [
        "https://www.instacart.com/store/costco/s?k=greek+yogurt",
        "https://www.instacart.com/store/costco/products/19208138",
    ],
    "includeDepartments": False,
    "postalCode": "10001",
    "inStockOnly": False,
    "onSaleOnly": False,
    "fetchDetails": False,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/instacart-grocery-price-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "queries": [
    "organic milk"
  ],
  "retailers": [
    "costco",
    "safeway"
  ],
  "urls": [
    "https://www.instacart.com/store/costco/s?k=greek+yogurt",
    "https://www.instacart.com/store/costco/products/19208138"
  ],
  "includeDepartments": false,
  "postalCode": "10001",
  "inStockOnly": false,
  "onSaleOnly": false,
  "fetchDetails": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call abotapi/instacart-grocery-price-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/instacart-grocery-price-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xDXlanYQLAqIh5pcK/builds/UNdgdzEM7RfxZ6fmS/openapi.json
