# Voilà Grocery Scraper — Sobeys, IGA & Safeway Prices (`yugenox/voila-grocery-scraper`) Actor

Sobeys, IGA & Safeway grocery prices from Voilà (voila.ca) by postal code or province: regular & sale price, unit price, Scene+ and multi-buy deals, stock, dietary labels, nutrition and ingredients. Search, categories or the whole catalogue, in English or French.

- **URL**: https://apify.com/yugenox/voila-grocery-scraper.md
- **Developed by:** [Yugenox Corp](https://apify.com/yugenox) (community)
- **Categories:** E-commerce, Automation, Integrations
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 product 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

Grocery prices from **Voilà** (voila.ca), the online grocery store of **Sobeys, IGA and Safeway**, by postal code or by province. It covers the regular price, sale price, unit price, Scene+ offers, multi-buy deals, stock, dietary labels and, optionally, nutrition facts and ingredients.

Search by keyword, browse categories, or take a region's **entire catalogue**. You can run it in **English or French**.

- **Coverage:** 10 regional catalogues: Ontario, Quebec, British Columbia, Manitoba, Saskatchewan, Nova Scotia, New Brunswick, PEI, Newfoundland and Labrador, and Alberta. Each has its own product range and prices.
- **Setup:** none. You don't need an account, an API key or a browser.
- **Proxy:** it works best when your Apify account can use residential proxies. Without them it switches to datacenter IPs, which voila.ca often refuses, so runs are slower and may come back incomplete.
- **Speed:** about 300 products per request. The entire Ontario catalogue (about 19,000 products) takes about 3 minutes.

### What you get

**The price you pay and the price it was.** `price` is what the item costs right now. `regular_price`, `sale_price` and `was_price` separate the shelf price from the deal, and `is_on_sale` flags it.

**Deals, split out.** Each promotion lands in its own field:

- `deal_text`: the sale or percentage-off deal.
- `multi_buy_deal`: "Buy 2 for $7".
- `member_offer`: Scene+ member-only savings.
- `scene_plus_offer` and `scene_plus_points`: Scene+ points offers, with the number of points.

A points-only offer is not counted as a sale.

**Unit prices you can compare.** `unit_price` is shown as printed, for example `$0.22/100ml`. `comparable_unit_price` normalizes it to $/100 g, $/100 ml or $/each. Items sold by weight are flagged with `selling_type: by_weight`.

**Region-vs-region pricing.** One run can price the same products in several provinces. For example, Natrel 2% 2 L cost $5.99 in Ontario, $5.69 in Quebec, $5.89 in BC and $4.49 in PEI on the same day. `product_id` is shared across regions, so the rows join cleanly.

**What's on the label.** `dietary_attributes` carries labels such as Organic, Vegan, Gluten Free, Kosher and Lactose Free, and `freshness_guarantee` gives the guaranteed shelf life. Turn on `include_details` to add:

- `description`, `ingredients` and `storage`.
- `nutrition` (every nutrient with its % daily value) and `nutrition_serving`.
- `average_weight_kg` for meat and produce sold by weight.

**Paid placements removed.** Search and category pages include a shelf of sponsored products. These are dropped by default. If you choose to keep them, they are flagged with `is_sponsored`.

**Whole-catalogue crawls.** `all_products` walks every department of a region. Products that appear in several categories are deduplicated.

### Run it

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

```json
{
  "search_terms": ["eggs", "milk", "chicken breast"],
  "postal_code": "M5V 2T6"
}
```

Rows stream into Output as they arrive. You can download them from Storage → Dataset as JSON, CSV or Excel.

From there, you can change the run in a few ways:

- **Compare provinces:** add `"regions": ["ON", "QC", "BC", "NS"]`. Every row names its region.
- **Browse a category:** use `"categories": ["Dairy & Eggs > Milk", "Frozen Pizza"]`, or paste a voila.ca category URL.
- **Take the whole catalogue:** set `"all_products": true`.
- **Only deals:** set `"on_sale_only": true`.
- **French:** set `"language": "fr"` to get names such as "Natrel Lait 2 % finement filtré 2 L". Category names work in English or French.

### Input

| Field | What it does |
|---|---|
| `search_terms` | Keywords to search in every location (up to 100 characters each). |
| `postal_code` / `postal_codes` | Price the catalogue that serves these postal codes. `deliverable` tells you whether Voilà delivers there. |
| `regions` | Province catalogues: `ON`, `QC`, `BC`, `MB`, `SK`, `NS`, `NB`, `PE`, `NL`, `AB`. |
| `categories` | A category name, a path (`Dairy & Eggs > Milk`) or a voila.ca category URL. |
| `all_products` | Every product in each location's catalogue. |
| `include_details` | Adds description, ingredients, storage, nutrition and average weight. This costs one extra request per product. |
| `on_sale_only` | Keeps only products on sale or with a public deal. |
| `max_items_per_search` | Cap for each search term or category, per location. The default is 500; `0` means no limit. |
| `max_items` | Cap for the whole run. `0` means no limit. |
| `language` | `en` or `fr`. |
| `include_sponsored` | Keep paid placements, flagged with `is_sponsored`. |
| `max_concurrency` | How many locations are scraped in parallel. |

If you give no postal code and no region, the run uses Toronto (M5V 2T6).

### What a row looks like

```json
{
  "store": "Voilà",
  "product_id": "267314EA",
  "name": "Earth's Own Unsweetened Almond Milk Alternative Original 1.89 L",
  "brand": "Earth's Own",
  "package_size": "1.89L",
  "normalized_package_size": { "size": 1890, "unit": "ml" },
  "price": 4.19,
  "regular_price": 4.69,
  "sale_price": 4.19,
  "was_price": 4.69,
  "is_on_sale": true,
  "unit_price": "$0.22/100ml",
  "comparable_unit_price": 0.22,
  "comparable_unit": "100ml",
  "deal_text": "SALE",
  "scene_plus_offer": null,
  "stock_status": "in_stock",
  "dietary_attributes": ["Gluten Free"],
  "category_path": "Dairy & Eggs > Milk > Non-Dairy Milk",
  "image_url": "https://voila.ca/images-v3/2d92d19c-0354-49c0-8a91-5260ed0bf531/f79de8a5-1620-4c69-9386-c9cfbff666f8/800x800.jpg",
  "product_url": "https://voila.ca/products/earth-s-own-unsweetened-almond-milk-alternative-original-1-89-l/267314EA",
  "location": "M5V 2T6",
  "location_name": "Ontario",
  "region_id": "4b2788bd-7207-4a8e-b5ef-0ad94345846b",
  "deliverable": true,
  "ingredients": "Almond base (filtered water, almonds), Sea salt, Gellan gum, Sunflower lecithin, …",
  "nutrition": { "Calories": "35", "Fat": "3 g (4%)", "Sodium": "150 mg (7%)", "Calcium": "300 mg (23%)" },
  "nutrition_serving": "per 1 cup (250 mL)"
}
```

Every row has 50+ fields. The dataset schema in the Output tab documents each one.

### Use cases

- **Price monitoring:** track Sobeys, IGA and Safeway shelf and sale prices daily for a set of products or a whole category.
- **Competitive pricing:** pair it with a Loblaws-family scraper to cover Canada's two largest grocers with the same column names.
- **Regional price gaps:** see what the same product costs in Toronto, Montréal, Vancouver and Halifax.
- **Deal feeds and flyer apps:** use `on_sale_only` with `categories: ["FLYER & DEALS"]` to get the current promotions, with multi-buy and Scene+ detail.
- **Nutrition and assortment research:** `all_products` plus `include_details` gives a full catalogue with ingredients and nutrition facts.

### FAQ

**Why do some provinces show `deliverable: false`?** Voilà delivers to homes in parts of Ontario and Quebec. Other provinces still have a full regional catalogue with their own prices, which this actor reads. The Alberta catalogue is currently empty on the site, so an Alberta run returns no products.

**Is there a barcode (UPC)?** Voilà doesn't publish barcodes. To match products across stores, use `brand`, `name` and `normalized_package_size`. Within Voilà, `product_id` is the same in every region.

**Why is a product missing from one region?** Each region stocks a different range. For example, Farm Boy and Longo's products are Ontario-only.

**Can I use my own proxy?** Yes. A Canadian residential proxy is the default and works best. If the store refuses another proxy, the run switches to Canadian residential automatically.

**Do I need residential proxies?** They're strongly recommended. If your Apify account can't use them, the run switches to Apify datacenter IPs. voila.ca refuses many of those, so the run keeps trying fresh IPs for up to about 5 minutes before it gives up. You may get fewer products, or none. The status message tells you when this happened. To fix it, enable residential proxies on your Apify account (Apify Console → Proxy), or paste your own Canadian residential proxies in the Proxy input.

**How are long runs handled?** Products are saved as they are found. If a run approaches its timeout, it stops a minute early and ends successfully with everything collected so far.

**Is it legal to scrape Voilà?** This Actor only collects publicly available data: product listings, prices, deals, stock and product details such as ingredients and nutrition facts that anyone can see on voila.ca without an account. Collecting publicly available data is generally legal, but you're responsible for how you use it. You must follow privacy laws such as GDPR, PIPEDA and CCPA, as well as Voilà's terms. If you're unsure, check with a lawyer. More on this: [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/)

**Does it access any private data?** No. Everything comes from pages Voilà shows to any visitor without logging in. It never uses a login, never touches private or restricted accounts, and never reaches password-protected areas.

# Actor input Schema

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

Products to look up, e.g. "eggs", "chicken breast", "compliments". Each term is searched in every location. French terms work too ("lait", "fromage"). Terms are cut to 100 characters (the store's limit).

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

Canadian postal code. Prices and the product range come from the Voilà catalogue that serves it. Leave blank (and no regions) to use Toronto M5V 2T6.

## `regions` (type: `array`):

Scrape the catalogue of whole provinces — each is its own catalogue with its own prices, so pick several to compare. Home delivery exists in Ontario and Quebec; the other provinces are price catalogues for their region.

## `postal_codes` (type: `array`):

Extra postal codes, each scraped as its own location in the same run.

## `categories` (type: `array`):

Browse categories instead of (or as well as) searching. Use a name ("Dairy & Eggs", "Frozen Pizza"), a path ("Dairy & Eggs > Milk") or a voila.ca category URL (https://voila.ca/categories/farm-boy/WEB1146400).

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

Scrape every product in each location's catalogue (Ontario: about 19,000 products in roughly 3 minutes). Search terms and categories are optional when this is on.

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

Also fetch each product's page: description, ingredients, storage, nutrition facts, serving size, full category ids and average weight for items sold by weight. One extra request per product.

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

Keep only products with a sale price or a public deal (sale, % off, multi-buy). Scene+ points-only offers don't count.

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

Upper limit for each search term and each category, per location. 0 = no limit.

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

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

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

Product names, promotions and descriptions in English or French.

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

Search and category pages carry a shelf of paid product placements. Off by default; turn on to keep them, flagged with is\_sponsored.

## `region_ids` (type: `array`):

Voilà's internal region ids (every row reports its region\_id), to pin an exact catalogue.

## `max_concurrency` (type: `integer`):

How many postal codes / provinces are scraped at the same time.

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

Canadian residential proxy is recommended and is the default. Other proxies switch to it automatically if the store refuses them.

## `_noResidential` (type: `boolean`):

Internal (canaries): behave exactly like an account without the RESIDENTIAL proxy group.

## Actor input object example

```json
{
  "search_terms": [
    "eggs"
  ],
  "postal_code": "M5V 2T6",
  "all_products": false,
  "include_details": false,
  "on_sale_only": false,
  "max_items_per_search": 50,
  "max_items": 0,
  "language": "en",
  "include_sponsored": false,
  "max_concurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CA"
  },
  "_noResidential": false
}
```

# Actor output Schema

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

Every scraped product row, as JSON.

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

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

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

The same rows narrowed to product, price, was-price, deal, unit price, stock and region.

# 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 = {
    "search_terms": [
        "eggs"
    ],
    "postal_code": "M5V 2T6",
    "max_items_per_search": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "CA"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("yugenox/voila-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 = {
    "search_terms": ["eggs"],
    "postal_code": "M5V 2T6",
    "max_items_per_search": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "CA",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("yugenox/voila-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 '{
  "search_terms": [
    "eggs"
  ],
  "postal_code": "M5V 2T6",
  "max_items_per_search": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "CA"
  }
}' |
apify call yugenox/voila-grocery-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yugenox/voila-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/SHCNmzmQlyQ3K9m5o/builds/Oiw8dpCNzthPAEpNj/openapi.json
