# Canadian Grocery Price Comparison Scraper API (`sunny_eternity/canada-grocery-price-comparison`) Actor

Compare grocery prices across Loblaws, Superstore, No Frills, Save-On-Foods, PriceSmart, and T\&T from one input. Get normalized product matches, prices, unit prices, sale flags, store details, product URLs, and CSV/JSON/Excel-ready output for apps, AI agents, and research.

- **URL**: https://apify.com/sunny\_eternity/canada-grocery-price-comparison.md
- **Developed by:** [JChaw](https://apify.com/sunny_eternity) (community)
- **Categories:** Integrations, Automation, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 searches

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## Canadian Grocery Price Comparison API

Compare grocery prices across supported Canadian retailers from one input.

Enter a grocery list, location, and retailers. Get normalized comparison-ready output with matched products, prices, unit prices, sale flags, store details, product URLs, and match confidence.

Supported retailers and banners:

- Loblaws / Real Canadian Superstore / No Frills
- Save-On-Foods / PriceSmart Foods
- T&T Supermarket

Best for:

- fixed basket grocery tracking
- grocery price comparison apps
- AI grocery shopping agents
- CPG competitor price monitoring
- food inflation research
- Google Sheets / Excel grocery tracking workflows

Need data from only one retailer?

- [Loblaws / Superstore / No Frills Grocery Price API](https://apify.com/sunny_eternity/loblaws-grocery-scraper)
- [Save-On-Foods / PriceSmart Grocery Price API](https://apify.com/sunny_eternity/save-on-foods-pricesmart-scraper)
- [T&T Supermarket Grocery Price API](https://apify.com/sunny_eternity/tnt-grocery-scraper)

### What this Actor does

This Actor lets you compare grocery prices across multiple Canadian retailers without running separate APIs manually.

You provide:

- grocery items, such as `eggs`, `milk`, `chicken breast`, or `celery`
- a location, such as `Vancouver, BC`
- retailers, such as `loblaws`, `saveon`, and `tnt`

The Actor returns:

- matched grocery products
- retailer and banner
- store details
- price and sale price
- normalized unit price
- package size
- availability
- product URL
- match confidence
- CSV/JSON/Excel-ready output

Use it when you want one normalized comparison table instead of separate retailer-specific API outputs.

### Quick start

The simplest run only needs grocery items, a location, and retailers:

```json
{
  "items": ["eggs", "chicken breast", "celery"],
  "location": "Vancouver, BC",
  "retailers": ["loblaws", "saveon", "tnt"]
}
````

This returns comparison rows for each item across the selected retailers.

Use `items` for beginner-friendly input. Advanced users can use `queries`, `region`, `postal_code`, and `locationIds` for more control.

Retailer aliases accepted: `saveon`, `save-on`, `t&t`, `rcss`, `no-frills`.

### Example output

Input:

```json
{
  "items": ["eggs"],
  "location": "Vancouver, BC",
  "retailers": ["loblaws", "saveon", "tnt"]
}
```

Example comparison output:

| item | retailer | matched product                   | price | unit price | sale  | confidence |
| ---- | -------- | --------------------------------- | ----: | ---------- | ----- | ---------- |
| eggs | loblaws  | No Name Large Eggs 12-pack        |  4.49 | 0.37/each  | false | high       |
| eggs | saveon   | Western Family Large Eggs 12-pack |  5.29 | 0.44/each  | false | high       |
| eggs | tnt      | Large White Eggs 12-pack          |  4.99 | 0.42/each  | true  | medium     |

The exact returned fields depend on retailer availability and selected output mode.

Each row in `comparison` mode is a full record like:

```json
{
  "_type": "comparison",
  "schema_version": "2026-05-compare-v1",
  "query": "ground beef",
  "retailer": "loblaws",
  "banner": "Loblaws",
  "store": "Robson Street",
  "store_id": "1050",
  "matched_product": "PC Lean Ground Beef",
  "price": 7.99,
  "unit_price": "$1.76/100g",
  "comparable_unit_price": 1.76,
  "sale_price": null,
  "was_price": null,
  "is_on_sale": false,
  "availability": "in_stock",
  "match_confidence": "high",
  "match_score": 0.86,
  "source_url": "https://www.loblaws.ca/...",
  "image_url": "https://...",
  "product_id": "20123456",
  "package_size": "454 g",
  "selling_type": "by_weight",
  "multi_buy_deal": null,
  "pc_optimum_offer": null,
  "category": "search:ground beef",
  "country": "CA",
  "currency": "CAD",
  "region": "BC",
  "scraped_at": "2026-05-15T14:32:11.123Z",
  "run_id": "abc123"
}
```

### Fixed basket tracking

This Actor works well for tracking the same grocery basket over time.

Example fixed basket:

```json
{
  "items": ["eggs", "milk", "bread", "bananas", "chicken breast", "ground beef", "rice", "celery"],
  "location": "Vancouver, BC",
  "retailers": ["loblaws", "saveon", "tnt"],
  "includeSummaries": true
}
```

Run the same basket weekly to track:

- basket total changes
- cheapest retailer by item
- sale items
- unit price changes
- store-by-store differences
- food inflation for your usual groceries

This is useful for personal grocery tracking, household budgeting, food price research, and spreadsheet-based grocery dashboards.

### Common use cases

#### Track a fixed grocery basket

Track the same grocery items weekly and compare basket totals across supported retailers.

#### Build a grocery price comparison app

Use normalized grocery price rows as backend data for Canadian grocery comparison apps.

#### Power an AI grocery shopping agent

Give an AI agent live grocery price context for supported Canadian retailers.

#### Monitor CPG and retail pricing

Compare product prices, sale prices, and unit prices across supported grocery banners.

#### Research food inflation

Collect structured Canadian grocery price data for analysis, charts, and reporting.

### Supported retailers

| Retailer family | Banners |
|---|---|
| Loblaws Cos. | Loblaws, Real Canadian Superstore, No Frills |
| Pattison Food Group | Save-On-Foods, PriceSmart Foods |
| T\&T | T\&T Supermarket |

T\&T's product catalog is unified across stores; the resolved store is used purely to label output records.

### Individual retailer APIs

This Actor is best when you want to compare prices across multiple retailers from one input.

If you only need data from one retailer, use the individual API instead:

| Retailer / banner | Actor | Best for |
|---|---|---|
| Loblaws / Superstore / No Frills | [Loblaws / Superstore / No Frills Grocery Price API](https://apify.com/sunny_eternity/loblaws-grocery-scraper) | Loblaws-owned grocery price and product data |
| Save-On-Foods / PriceSmart Foods | [Save-On-Foods / PriceSmart Grocery Price API](https://apify.com/sunny_eternity/save-on-foods-pricesmart-scraper) | Western Canada grocery prices and product data |
| T\&T Supermarket | [T\&T Supermarket Grocery Price API](https://apify.com/sunny_eternity/tnt-grocery-scraper) | T\&T and Asian grocery product price data |

### Pricing

This Actor uses pay-per-event pricing.

You are charged for:

- **Search**: one grocery item searched on one selected retailer
- **Result**: one normalized comparison row returned

Example:

If you compare 3 grocery items across 3 retailers, that counts as 9 searches.

A small run with 3 items across 3 retailers usually costs around $0.50–$0.75 depending on the number of results returned.

Platform usage is included.

### Output modes

Most users should start with `comparison`.

| Mode | Best for | Description |
|---|---|---|
| `comparison` | Most users | Flat rows, one matched product per item per retailer. Best for CSV, Excel, API, and ETL. |
| `comparison_grouped` | AI / LLM workflows | One record per item with retailer results grouped together. |
| `summary` | quick analysis | Cheapest retailer, price range, median price, and sale count per item. |
| `category_browse` | advanced catalog scraping | Browse full categories by retailer. |
| `intel` | heavy analysis | Comparison rows plus summaries, category data, deals, and run summary. |

Start with `comparison` unless you know you need another mode.

### Input reference

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | string\[] | yes (beginner) | Grocery item search terms. Alias of `queries`. |
| `queries` | string\[] | conditional | Same as `items`. Required for `comparison`, `comparison_grouped`, `summary` modes. |
| `location` | string | no | City/province (e.g. `Vancouver, BC`) or postal code. Beginner-friendly alias for `region` / `postal_code`. |
| `retailers` | string\[] | no | Subset of `tnt`, `loblaws`, `superstore`, `nofrills`, `saveonfoods`, `pricesmart`. Default: all supported. |
| `region` | string | conditional | Metro region; maps to a representative postal code. |
| `postal_code` | string | no | Overrides `region`. |
| `locationIds` | object | no | Per-retailer store ID overrides (e.g. `{ "tnt": "MGFS", "superstore": "1517", "saveonfoods": "1982" }`). |
| `categories` | string\[] | conditional | Unified category slugs (used in `category_browse` / `intel` modes). |
| `maxResultsPerQueryPerRetailer` | integer | no | Default 3. |
| `retailerConcurrency` | integer | no | Default 3. |
| `minMatchConfidence` | string | no | `low` (default) | `medium` | `high`. |
| `strictItemBest` | boolean | no | Default `true`. When `true`, `item_best_options` only chooses rows with comparable unit-price + `medium`/`high` confidence. |
| `includeSummaries` | boolean | no | Default `false`. In `comparison` mode, set `true` to also emit `basket_summary`, `item_best_options`, and `run_meta`. |
| `outputMode` | string | no | See [Output modes](#output-modes). Default `comparison`. |

### Output fields

Each `comparison` record includes:

- `query`, `retailer`, `banner`, `store`, `store_id`
- `matched_product`, `product_id`, `package_size`, `selling_type`
- `price`, `sale_price`, `was_price`, `is_on_sale`
- `unit_price`, `comparable_unit_price`
- `availability`
- `match_confidence` (`high` / `medium` / `low`), `match_score`
- `source_url`, `image_url`
- `multi_buy_deal`, `pc_optimum_offer` (retailer-specific extras, nullable)
- `country`, `currency`, `region`, `scraped_at`, `run_id`

When `includeSummaries` is `true`, the dataset also includes:

- `_type: "basket_summary"`
- `_type: "item_best_options"`
- `_type: "run_meta"`

`run_meta.retailer_status` labels each requested retailer as one of:

- `success` — retailer returned at least one matched row
- `no_results` — retailer executed but returned zero matches
- `failed` — retailer failed (reason included)
- `skipped` — retailer was requested but not executed in the final run path

`run_meta.warnings` collects runtime warnings from location resolution, query search, and category scrape failures.

### Limitations

This Actor compares retailer search results. It does not guarantee exact product equivalence across stores.

Prices and availability may vary by:

- store location
- postal code
- time of scrape
- loyalty program
- product availability
- retailer search behavior

Use `matchConfidence`, product names, package sizes, unit prices, and product URLs to review matches.

This Actor currently supports selected Canadian grocery retailers and banners. It does not cover every grocery retailer in Canada.

### FAQ

#### Should I use this Actor or the individual retailer APIs?

Use this Actor if you want to compare the same grocery items across multiple supported retailers.

Use the individual retailer APIs if you only need data from one retailer:

- [Loblaws / Superstore / No Frills Grocery Price API](https://apify.com/sunny_eternity/loblaws-grocery-scraper)
- [Save-On-Foods / PriceSmart Grocery Price API](https://apify.com/sunny_eternity/save-on-foods-pricesmart-scraper)
- [T\&T Supermarket Grocery Price API](https://apify.com/sunny_eternity/tnt-grocery-scraper)

#### Does this Actor track price history?

Not by itself. Each run returns current comparison output. To track history, run the same basket regularly and store the results in a spreadsheet, database, or BI tool.

#### Can I use this for fixed basket tracking?

Yes. Run the same list of items weekly or monthly and compare basket totals, item prices, unit prices, and sales over time.

#### Can I export the results?

Yes. Apify datasets can be exported as CSV, JSON, Excel, and other formats.

#### Are the matches exact?

Not always. Grocery products vary by brand, package size, store, and retailer naming. Use match confidence and product URLs to review matches.

### Technical notes

This Actor is built on top of retailer-specific grocery APIs and normalizes their outputs into a shared comparison format.

Advanced notes:

- retailer adapters map each API output into a common schema
- query matching uses product names, package sizes, unit prices, and match confidence
- pay-per-event billing charges `Search` and `Result` events
- advanced users can inspect raw output depending on selected output mode

#### How charging is implemented

- `Search` is manually charged via `Actor.charge({ eventName: "retailer-search" })` **before** each item-retailer child actor invocation. If the charge fails (limit reached or `chargedCount < 1`), the child actor is never called, so the user is never billed for work they cannot be charged for.
- `Result` is charged automatically by the Apify SDK: every push to the default dataset via `Actor.pushData(...)` increments the `apify-default-dataset-item` event count and respects the `maxTotalChargeUsd` budget. We do not pass a second event name to `pushData` — doing so would double-charge.
- The synthetic `apify-actor-start` event is never manually charged.

#### Local PPE testing

To exercise the pay-per-event flow locally (without spending real money on Apify), set the SDK's test env vars before running the actor:

```bash
ACTOR_TEST_PAY_PER_EVENT=true \
ACTOR_USE_CHARGING_LOG_DATASET=true \
npm start
```

Verify the following in the local charging log dataset / `OUTPUT`:

- Exactly one `retailer-search` event is charged per valid item-retailer pair.
- One `apify-default-dataset-item` event is charged for each row pushed.
- The synthetic `apify-actor-start` event is never manually charged.
- When the charge limit is reached, no further child actor runs are scheduled.

#### Billing summary

Every run writes a `billing` block to the `OUTPUT` key-value record and to the trailing `run_meta` dataset record (when summaries are enabled):

```json
{
  "billing": {
    "searchEventsAttempted": 9,
    "searchEventsCharged": 9,
    "resultRowsAttempted": 27,
    "resultRowsPushed": 27,
    "chargeLimitReached": false
  }
}
```

The actor also writes a human-readable `SUMMARY` Markdown value with the same information, plus a warning section if the charge limit was reached mid-run.

#### Implementation details

- The actor runs each retailer in parallel (configurable via `retailerConcurrency`); within a retailer, queries are processed sequentially with the per-retailer politeness delay each API already enforces.
- Loblaws location resolution uses a third-party Bullseye API; if `BULLSEYE_API_KEY` and `BULLSEYE_CLIENT_ID` env vars are missing the actor falls back to the banner's default store.
- Retailer adapters live in `src/retailer-dispatch.ts`; adding a new retailer is one adapter object there.

#### Schema files

- Input schema: `INPUT_SCHEMA.json`
- Output schema notes: `OUTPUT_SCHEMA.md`

# Actor input Schema

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

Item names to look up across retailers (e.g. \['ground beef', 'eggs', 'celery']). Required for comparison/summary/intel output modes.

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

Which Canadian grocery retailers to compare. Defaults to all six.

## `region` (type: `string`):

Canadian metropolitan region — maps to a representative postal code for location-aware pricing. Use 'postal\_code' for a specific address.

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

Canadian postal code (e.g. 'V5X 0C4'). Overrides 'region' if provided.

## `locationIds` (type: `object`):

Optional per-retailer store ID overrides (e.g. {"tnt": "MGFS", "superstore": "1517", "saveonfoods": "1982"}). Takes priority over region/postal\_code for that retailer.

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

Unified category slugs to browse (e.g. \['produce', 'dairy-eggs', 'meat']). Used only when outputMode is 'category\_browse' or 'intel'.

## `maxResultsPerQueryPerRetailer` (type: `integer`):

Cap on matched results returned per (query, retailer). Default 3.

## `retailerConcurrency` (type: `integer`):

How many retailers to scrape in parallel. Default 3.

## `minMatchConfidence` (type: `string`):

Drop results below this confidence floor. 'low' (loose), 'medium' (recommended), 'high' (strict, exact-match feel).

## `outputMode` (type: `string`):

comparison = flat rows per match; comparison\_grouped = one record per query; summary = cheapest/spread per query; category\_browse = full per-retailer catalog browse; intel = products + summaries + category leaderboards (heaviest).

## Actor input object example

```json
{
  "retailers": [
    "tnt",
    "loblaws",
    "superstore",
    "nofrills",
    "saveonfoods",
    "pricesmart"
  ],
  "region": "metro_vancouver",
  "maxResultsPerQueryPerRetailer": 3,
  "retailerConcurrency": 3,
  "minMatchConfidence": "low",
  "outputMode": "comparison"
}
```

# Actor output Schema

## `comparisons` (type: `string`):

Price comparison data across Canadian grocery retailers including matched products, prices, unit pricing, confidence scores, and sale indicators

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("sunny_eternity/canada-grocery-price-comparison").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 = {}

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

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

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

```

## CLI example

```bash
echo '{}' |
apify call sunny_eternity/canada-grocery-price-comparison --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=sunny_eternity/canada-grocery-price-comparison",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Canadian Grocery Price Comparison Scraper API",
        "description": "Compare grocery prices across Loblaws, Superstore, No Frills, Save-On-Foods, PriceSmart, and T&T from one input. Get normalized product matches, prices, unit prices, sale flags, store details, product URLs, and CSV/JSON/Excel-ready output for apps, AI agents, and research.",
        "version": "1.0",
        "x-build-id": "wwRDc6LwrDXIGpvIq"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/sunny_eternity~canada-grocery-price-comparison/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-sunny_eternity-canada-grocery-price-comparison",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/sunny_eternity~canada-grocery-price-comparison/runs": {
            "post": {
                "operationId": "runs-sync-sunny_eternity-canada-grocery-price-comparison",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/sunny_eternity~canada-grocery-price-comparison/run-sync": {
            "post": {
                "operationId": "run-sync-sunny_eternity-canada-grocery-price-comparison",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "queries": {
                        "title": "Grocery Queries",
                        "type": "array",
                        "description": "Item names to look up across retailers (e.g. ['ground beef', 'eggs', 'celery']). Required for comparison/summary/intel output modes.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "retailers": {
                        "title": "Retailers",
                        "type": "array",
                        "description": "Which Canadian grocery retailers to compare. Defaults to all six.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "tnt",
                                "loblaws",
                                "superstore",
                                "nofrills",
                                "saveonfoods",
                                "pricesmart"
                            ],
                            "enumTitles": [
                                "T&T Supermarket",
                                "Loblaws",
                                "Real Canadian Superstore",
                                "No Frills",
                                "Save-On-Foods",
                                "PriceSmart Foods"
                            ]
                        },
                        "default": [
                            "tnt",
                            "loblaws",
                            "superstore",
                            "nofrills",
                            "saveonfoods",
                            "pricesmart"
                        ]
                    },
                    "region": {
                        "title": "Region",
                        "enum": [
                            "metro_vancouver",
                            "metro_calgary",
                            "metro_edmonton",
                            "metro_toronto",
                            "metro_ottawa",
                            "metro_montreal",
                            "metro_quebec_city",
                            "metro_halifax"
                        ],
                        "type": "string",
                        "description": "Canadian metropolitan region — maps to a representative postal code for location-aware pricing. Use 'postal_code' for a specific address.",
                        "default": "metro_vancouver"
                    },
                    "postal_code": {
                        "title": "Postal Code (optional override)",
                        "pattern": "^[A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d$",
                        "type": "string",
                        "description": "Canadian postal code (e.g. 'V5X 0C4'). Overrides 'region' if provided."
                    },
                    "locationIds": {
                        "title": "Per-Retailer Store ID Overrides (optional)",
                        "type": "object",
                        "description": "Optional per-retailer store ID overrides (e.g. {\"tnt\": \"MGFS\", \"superstore\": \"1517\", \"saveonfoods\": \"1982\"}). Takes priority over region/postal_code for that retailer.",
                        "additionalProperties": true
                    },
                    "categories": {
                        "title": "Categories (category_browse / intel modes only)",
                        "type": "array",
                        "description": "Unified category slugs to browse (e.g. ['produce', 'dairy-eggs', 'meat']). Used only when outputMode is 'category_browse' or 'intel'.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "produce",
                                "dairy-eggs",
                                "meat",
                                "seafood",
                                "bakery",
                                "frozen",
                                "pantry",
                                "snacks",
                                "beverages",
                                "international",
                                "deli",
                                "prepared-meals",
                                "plant-based",
                                "health-beauty",
                                "baby-care",
                                "pet-care",
                                "household"
                            ],
                            "enumTitles": [
                                "Produce (Fruits & Vegetables)",
                                "Dairy & Eggs",
                                "Meat",
                                "Fish & Seafood",
                                "Bakery",
                                "Frozen Foods",
                                "Pantry & Dry Goods",
                                "Snacks, Chips & Candy",
                                "Beverages",
                                "International Foods",
                                "Deli",
                                "Prepared Meals",
                                "Plant-Based & Non-Dairy",
                                "Health & Beauty",
                                "Baby Care",
                                "Pet Care",
                                "Household & Cleaning"
                            ]
                        }
                    },
                    "maxResultsPerQueryPerRetailer": {
                        "title": "Max Results Per Query Per Retailer",
                        "minimum": 1,
                        "maximum": 50,
                        "type": "integer",
                        "description": "Cap on matched results returned per (query, retailer). Default 3.",
                        "default": 3
                    },
                    "retailerConcurrency": {
                        "title": "Retailer Concurrency",
                        "minimum": 1,
                        "maximum": 6,
                        "type": "integer",
                        "description": "How many retailers to scrape in parallel. Default 3.",
                        "default": 3
                    },
                    "minMatchConfidence": {
                        "title": "Minimum Match Confidence",
                        "enum": [
                            "low",
                            "medium",
                            "high"
                        ],
                        "type": "string",
                        "description": "Drop results below this confidence floor. 'low' (loose), 'medium' (recommended), 'high' (strict, exact-match feel).",
                        "default": "low"
                    },
                    "outputMode": {
                        "title": "Output Mode",
                        "enum": [
                            "comparison",
                            "comparison_grouped",
                            "summary",
                            "category_browse",
                            "intel"
                        ],
                        "type": "string",
                        "description": "comparison = flat rows per match; comparison_grouped = one record per query; summary = cheapest/spread per query; category_browse = full per-retailer catalog browse; intel = products + summaries + category leaderboards (heaviest).",
                        "default": "comparison"
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
