# Loblaws, Superstore, No Frills, Maxi & 8 More — Grocery Prices (`arpitgandhi1934/loblaws-grocery-scraper`) Actor

Canadian grocery prices from all 12 Loblaw banners and 1,107 stores, priced at the individual store by postal code. 67 fields per product: UPC barcodes, stock status, PC Optimum member prices, promo expiry dates, nutrition and ingredients. Many chains and stores in one run. No API key, no browser.

- **URL**: https://apify.com/arpitgandhi1934/loblaws-grocery-scraper.md
- **Developed by:** [Arpit Gandhi](https://apify.com/arpitgandhi1934) (community)
- **Categories:** E-commerce, Automation, Integrations
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.69 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Canadian grocery prices from **all 12 Loblaw banners and 1,107 stores**, priced at the individual store by postal code — with **UPC barcodes, live stock status, PC Optimum member prices, promo expiry dates, ratings, nutrition and ingredients** on the row.

**67 fields per product.** Not a name-and-price feed: everything the shelf tag, the barcode and the label carry, from one input.

- **Chains** — Loblaws, Real Canadian Superstore, No Frills, Zehrs, Fortinos, Valu-mart, Your Independent Grocer, Independent City Market, Provigo, Maxi, Wholesale Club, Atlantic Superstore
- **Coverage** — 1,107 stores across every province plus Yukon and the Northwest Territories, in English and French
- **Setup** — none. No API key, no account, no proxy, no browser, no geocoder

### What you get that a price feed doesn't

**Scan-ready identifiers.** `upc` and `gtin` on the row, so prices join straight onto an existing product database or a barcode scan. No fuzzy name matching.

**Stock, not just price.** `stock_status` and `is_shoppable` tell you whether the item is actually on the shelf at that store today. A price for something out of stock is a bad datapoint.

**Member vs shelf price.** `member_price` and `member_price_ends` capture PC Optimum member-only pricing separately from `price` and `was_price` — three different numbers the same product can show.

**Promotions with expiry dates.** `sale_ends`, `deal_ends`, `pc_optimum_offer_ends` and numeric `pc_optimum_points`, alongside `multi_buy_deal` and `purchase_limit`. You know when a price dies, not only that it exists.

**Many chains and many stores in one run.** `banners` takes a list and `stores_per_banner` prices the nearest N of each — so "Superstore vs No Frills vs Maxi, three stores each" is a single run, not nine.

**The full label.** Turn on `include_details` for `nutrition`, `ingredients`, `category_path`, `average_weight_kg` and `gtin`.

**Paid placements removed by default.** The storefront injects sponsored products into every search page, often unrelated — a search for milk returns kombucha and baby cereal. Those are dropped unless you ask for them, and flagged with `is_sponsored` when you do.

**Real unit-price maths.** `comparable_unit_price` normalizes to $/100 g or $/100 ml and stays `null` when a package size won't parse, so a wrong number never enters your comparison. `comparison_prices` keeps every unit the store quoted ($/kg *and* $/lb).

### Run it

Paste into the Input tab (JSON view) and hit Start:

```json
{
  "banners": ["superstore"],
  "search_terms": ["eggs", "milk", "chicken breast"],
  "postal_code": "V5X 0C4"
}
```

Rows stream into Output as they arrive. Download from Storage → Dataset as JSON, CSV or Excel.

Three things to change from there:

- **Compare chains** — `"banners": ["superstore", "nofrills", "maxi"]`, one run, every row naming its store
- **Compare stores** — `"stores_per_banner": 3` prices the three nearest locations of each banner
- **Take the whole store** — `"all_products": true` instead of search terms

### What a row looks like

```json
{
  "store": "Real Canadian Superstore",
  "banner": "superstore",
  "name": "Bananas, Bunch",
  "product_id": "20175355001_KG",
  "upc": "4011",
  "price": 1.98,
  "price_unit": "ea",
  "was_price": null,
  "is_on_sale": false,
  "member_price": null,
  "unit_price": "$1.72/kg, $0.78/lb",
  "comparable_unit_price": 0.17,
  "comparable_unit": "100g",
  "selling_type": "by_weight",
  "stock_status": "in_stock",
  "is_shoppable": true,
  "rating": 3.67,
  "review_count": 3,
  "image_url": "https://digital.loblaws.ca/PCX/20175355001_KG/en/1/...png",
  "product_url": "https://www.realcanadiansuperstore.ca/en/bananas-bunch/p/20175355001_KG",
  "location_id": "1517",
  "location_name": "Real Canadian Superstore Marine Drive",
  "location_address": "350 SE Marine Dr, Vancouver, BC, V5X 2S5",
  "location_province": "British Columbia",
  "location_coordinates": { "lat": 49.20906, "lng": -123.10 },
  "distance_km": 1.4
}
```

### Fields

**Product** — `name`, `brand`, `description`, `product_id`, `article_number`, `upc`, `upcs`, `package_size`, `parsed_package_size`, `normalized_package_size`, `variant_label`, `image_url`, `product_url`, `seller_name`

**Price** — `price`, `price_unit`, `was_price`, `is_on_sale`, `sale_ends`, `member_price`, `member_price_ends`, `unit_price`, `comparable_unit_price`, `comparable_unit`, `parsed_unit_price`, `comparison_prices`, `selling_type`, `deposit`, `eco_fee`, `tax_info`

**Promotions** — `deal_type`, `deal_text`, `deal_ends`, `multi_buy_deal`, `purchase_limit`, `pc_optimum_offer`, `pc_optimum_points`, `pc_optimum_offer_ends`, `badges`

**Availability & reviews** — `stock_status`, `is_shoppable`, `rating`, `rating_count`, `review_count`, `is_sponsored`

**Store** — `store`, `banner`, `location`, `location_id`, `location_name`, `location_address`, `location_city`, `location_province`, `location_postal_code`, `location_coordinates`, `location_phone`, `distance_km`

**Run** — `search_term`

**With `include_details`** — `gtin`, `category`, `category_path`, `ingredients`, `nutrition`, `average_weight_kg`, `variant_group_id`, `variant_count`, `variants`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `banners` | string\[] | — | One or more of the 12 banners below. Several in one run compares chains. |
| `search_terms` | string\[] | — | Keyword queries, e.g. `["eggs", "no name"]`. Use French for Maxi and Provigo. |
| `postal_code` | string | — | Any Canadian postal code; picks the nearest store of each banner. |
| `all_products` | boolean | `false` | Walk the store's entire catalogue instead of searching. |
| `include_details` | boolean | `false` | Add nutrition, ingredients, category path, GTIN, average weight. One extra request per product. |
| `on_sale_only` | boolean | `false` | Keep only products on sale or carrying a deal. |
| `group_variants` | boolean | `false` | Collapse sizes of one product into a single row with a `variants` array. |
| `stores_per_banner` | integer | `1` | Price the nearest N stores (1–10) of each banner. |
| `locationId` | string | — | One exact store, overriding `postal_code`. |
| `language` | string | `auto` | `en` / `fr`. Auto = French for Maxi and Provigo. |
| `max_items_per_search` | integer | `500` | Cap per search term, per store. |
| `max_items` | integer | `0` | Stop the whole run after N products. `0` = no limit. |
| `include_sponsored` | boolean | `false` | Keep the storefront's paid placements. |

One of `search_terms` or `all_products` is required.

### Examples

Compare one basket across three chains:

```json
{ "banners": ["superstore", "nofrills", "maxi"], "search_terms": ["milk", "eggs", "bread"], "postal_code": "H2X 1Y6" }
```

Everything on sale at the nearest No Frills:

```json
{ "banners": ["nofrills"], "all_products": true, "on_sale_only": true, "postal_code": "M6H 1V4" }
```

Barcodes and nutrition for a category of products:

```json
{ "banners": ["loblaw"], "search_terms": ["greek yogurt"], "include_details": true, "postal_code": "M5V 2T6" }
```

Price the same products at the five nearest stores:

```json
{ "banners": ["superstore"], "search_terms": ["chicken breast"], "stores_per_banner": 5, "postal_code": "T2P 1J9" }
```

One exact store:

```json
{ "banners": ["superstore"], "locationId": "1517", "search_terms": ["bananas"] }
```

### Supported stores

| Banner | Store | Stores | Provinces |
|---|---|---:|---|
| `nofrills` | No Frills | 350 | AB, BC, MB, NB, NL, NS, ON, PE, SK |
| `maxi` | Maxi | 198 | NB, QC |
| `independent` | Your Independent Grocer | 181 | AB, BC, NB, NL, NS, NT, ON, PE, QC, SK, YT |
| `superstore` | Real Canadian Superstore | 119 | AB, BC, MB, ON, SK, YT |
| `wholesaleclub` | Wholesale Club | 56 | AB, BC, MB, NB, NL, NS, ON, QC, SK |
| `rass` | Atlantic Superstore | 52 | NB, NS, PE |
| `loblaw` | Loblaws | 48 | AB, BC, ON |
| `zehrs` | Zehrs | 42 | ON |
| `fortinos` | Fortinos | 24 | ON |
| `provigo` | Provigo | 20 | QC |
| `valumart` | Valu-mart | 14 | ON |
| `independentcitymarket` | Independent City Market | 3 | ON |

Counts read live from Loblaw's own store directory. Each banner prices independently — the same SKU can differ by a dollar or more between them. `rass` is Atlantic Superstore's internal code. Aliases accepted: `loblaws`, `rcss`, `no frills`, `valu-mart`, `yig`, `atlantic`, `icm`.

### From the API

Grab a token at [console.apify.com/settings/integrations](https://console.apify.com/settings/integrations):

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/arpitgandhi1934~loblaws-grocery-scraper/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"banners": ["superstore"], "search_terms": ["eggs", "milk"], "postal_code": "V5X 0C4"}'
```

`run-sync-get-dataset-items` suits small runs and can time out on large ones. For scheduled or whole-store scrapes, POST to `/runs` and read the returned `defaultDatasetId`. The official JavaScript and Python clients work too, as do Zapier, Make, Google Sheets and Airbyte.

### What people build with it

- **Price comparison apps** keyed to a real store, not a national average
- **Food inflation research** — rerun a fixed basket weekly and diff `price` by `product_id` and store
- **CPG competitor monitoring** across all 12 banners, including member-only pricing rivals can't see in a flyer
- **Deal and flyer sites** from `is_on_sale`, `multi_buy_deal`, `pc_optimum_offer` and their expiry dates
- **Barcode and inventory apps** joining on `upc` / `gtin` with `stock_status`
- **AI shopping agents** that need grounded prices with a store, a timestamp and a stock flag

### FAQ

**How do I scrape Real Canadian Superstore prices?** Set `"banners": ["superstore"]` with `search_terms` and a `postal_code`. The Run it example above works as-is.

**Can I get prices for a specific postal code?** Yes. Any Canadian postal code resolves to the nearest store of each banner, and every row reports that store's name, address, postal code, coordinates and distance.

**Does this work in Quebec?** Yes — `maxi` and `provigo`, which default to French. `independent` and `wholesaleclub` also have Quebec locations.

**Can I compare the same product across chains?** Yes, in one run: pass several `banners`. `product_id` is shared across the Loblaw catalogue, so rows join directly.

**Is there an official Loblaws API?** No. Loblaw publishes no public pricing API, which is why this reads the same JSON endpoints its own storefronts and app use.

**How do I get barcodes?** `upc` is on every row. `gtin` comes with `include_details`.

**How do I track prices over time?** Schedule it daily or weekly and keep each run's dataset, then compare `price` per `product_id` per store. The Actor returns current prices and stores no history itself.

**How many products can one run return?** A search term returns up to `max_items_per_search` (default 500). `all_products` walks the whole catalogue — a small Valu-mart is ~2,000 products in under a minute; a large No Frills is ~21,000 and takes several minutes. Around 25,000 products are reachable per store. Rows stream as they go, so partial results are available while the run continues.

**Do I need a proxy?** No. It runs from Apify's datacenter IPs without one. `proxyConfiguration` is there for very large or very frequent scrapes.

**Is scraping Loblaws legal?** Scraping publicly available product and price information is generally legal. Complying with the retailer's terms and the law in your jurisdiction is your responsibility, so keep request volume reasonable.

### Notes

- Grocery size variants are separate products in Loblaw's catalogue; `group_variants` groups them by brand and name with no extra requests. True `variants` arrays are clothing (Joe Fresh size/colour).
- Result ordering from the upstream search is not stable between identical calls, so whole-store runs walk the catalogue in repeated passes until a pass adds almost nothing. That is why a full store takes minutes rather than seconds.
- A postal code outside a banner's footprint returns a clear error naming the provinces that banner serves, so a Quebec query never quietly returns an Ontario store.

# Actor input Schema

## `banners` (type: `array`):

One or more Loblaw banners. Pick several to compare the same products across chains in one run — a banner with no store near your postal code is skipped with a note.

## `search_terms` (type: `array`):

Products to look up, e.g. "eggs", "chicken breast", "no name". Use French terms for Maxi and Provigo for the best matches ("lait", "oeufs").

## `postal_code` (type: `string`):

Canadian postal code. Prices come from the nearest store of each banner. Leave blank to use a default store.

## `all_products` (type: `boolean`):

Scrape every product the store carries (tens of thousands of items, takes a while). Search terms are optional when this is on.

## `include_details` (type: `boolean`):

Also fetch each product's detail: full category path, nutrition facts, ingredients, GTIN barcode, average weight and size variants. One extra request per product — slower, much richer.

## `on_sale_only` (type: `boolean`):

Keep only products that are on sale or have a multi-buy deal right now.

## `group_variants` (type: `boolean`):

Collapse the sizes of one product (e.g. olive oil in 500 ml, 1 L and 3 L, or a shirt in S/M/L) into one row with a variants list of each size, price and stock. No extra requests.

## `max_items_per_search` (type: `integer`):

Upper limit for each search term, per store.

## `max_items` (type: `integer`):

Stop the whole run after this many products. 0 = no limit.

## `stores_per_banner` (type: `integer`):

Price the nearest N stores of each banner (1–10) — useful for comparing stores in one city.

## `locationId` (type: `string`):

Price one exact store instead of the nearest one (overrides postal code). Works with a single banner. Example: 1517 is Real Canadian Superstore Marine Drive, Vancouver; every row reports its store ID.

## `language` (type: `string`):

Product names and descriptions. Auto = French for Maxi and Provigo, English for the rest.

## `include_sponsored` (type: `boolean`):

The storefront mixes paid product ads into search results (often unrelated to your term). Off by default; turn on to keep them, flagged with is\_sponsored.

## `proxyConfiguration` (type: `object`):

Not needed for normal runs. Enable if you run very large or very frequent scrapes.

## Actor input object example

```json
{
  "banners": [
    "superstore"
  ],
  "search_terms": [
    "eggs",
    "milk",
    "chicken breast"
  ],
  "postal_code": "V5X 0C4",
  "all_products": false,
  "include_details": false,
  "on_sale_only": false,
  "group_variants": false,
  "max_items_per_search": 500,
  "max_items": 0,
  "stores_per_banner": 1,
  "language": "auto",
  "include_sponsored": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `products` (type: `string`):

Every scraped product row, as JSON. One object per product per store, with price, was-price, member price, unit price, promotions, stock status, UPC and the store it was priced at.

## `productsCsv` (type: `string`):

The same rows as CSV, for a spreadsheet or a price-tracking sheet.

## `productsTable` (type: `string`):

The same rows narrowed to the key columns — product, brand, store, price, was-price, deal, unit price and stock.

# 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 = {
    "banners": [
        "superstore"
    ],
    "search_terms": [
        "eggs",
        "milk",
        "chicken breast"
    ],
    "postal_code": "V5X 0C4",
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("arpitgandhi1934/loblaws-grocery-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 = {
    "banners": ["superstore"],
    "search_terms": [
        "eggs",
        "milk",
        "chicken breast",
    ],
    "postal_code": "V5X 0C4",
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("arpitgandhi1934/loblaws-grocery-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 '{
  "banners": [
    "superstore"
  ],
  "search_terms": [
    "eggs",
    "milk",
    "chicken breast"
  ],
  "postal_code": "V5X 0C4",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call arpitgandhi1934/loblaws-grocery-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arpitgandhi1934/loblaws-grocery-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/E2ialIGI4TFphpjME/builds/NFts5Z3155uYhTQrf/openapi.json
