- Price comparison
- Product matching
- Price data
- Apify
How to Compare Grocery Prices Across 16 Canadian Chains in One Call
Running four retailer scrapers yourself leaves you with four schemas, four location systems and a matching problem. This Actor does the fan-out and the matching, and hands back one row per item per retailer with a score attached to every match.
Savvi5 min read
Start here
A grocery list and a city. Paste this into the Input tab of the Canadian Grocery Price Comparison API, switch to JSON view, and hit Start.
{
"queries": ["eggs", "chicken breast", "celery"],
"location": "Vancouver, BC",
"retailers": ["loblaws", "saveonfoods", "tnt"]
}Same run over HTTP:
curl -X POST \
"https://api.apify.com/v2/acts/sunny_eternity~canada-grocery-price-comparison/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"queries": ["eggs", "chicken breast", "celery"], "location": "Vancouver, BC", "retailers": ["loblaws", "saveonfoods", "tnt"]}'Rows stream in as each retailer finishes. One query against three retailers gives you three rows:
| Retailer | Matched product · price · confidence |
|---|---|
loblaws | No Name Large Eggs 12-pack — $4.49, $0.37/each (high confidence) |
saveon | Western Family Large Eggs 12-pack — $5.29, $0.44/each (high confidence) |
tnt | Large White Eggs 12-pack — $4.99, $0.42/each, on sale (medium confidence) |
The matching is the whole product
Grocery products don’t line up cleanly across chains. Brands differ, sizes differ, and every retailer names things its own way. So the Actor doesn’t claim a match is correct. It shows its work on every row:
match_scoreruns 0 to 1.match_confidencebuckets that intohigh,mediumorlow.match_reasonsandmismatch_reasonsname the signals behind the score, things likebrand_matchandsize_match.
My advice: start at minMatchConfidence: "medium" and look at source_url on a handful of rows before you trust a whole basket. Low confidence rows are useful for coverage and bad for a price claim.
Basket totals per store
Set includeSummaries and each run emits three extra row types on top of the comparison rows:
{
"queries": ["eggs", "milk", "bread", "bananas", "chicken breast", "rice"],
"location": "Vancouver, BC",
"retailers": ["loblaws", "saveonfoods", "tnt"],
"includeSummaries": true,
"minMatchConfidence": "medium"
}basket_summarytotals the basket per retailer.item_best_optionsnames the cheapest source for each item.run_metacarries per-retailer status and any warnings.
Schedule that same input weekly and you have basket totals, per-item winners and sale frequency over time, with no post-processing.
From Python
pip install apify-client, then:
from apify_client import ApifyClient
client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("sunny_eternity/canada-grocery-price-comparison").call(run_input={
"queries": ["eggs", "chicken breast", "celery"],
"location": "Vancouver, BC",
"retailers": ["loblaws", "saveonfoods", "tnt"],
"minMatchConfidence": "medium",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row.get("_type") != "comparison":
continue
print(row["query"], row["retailer"], row["price"], row["match_confidence"])From JavaScript
npm i apify-client, then:
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });
const run = await client.actor('sunny_eternity/canada-grocery-price-comparison').call({
queries: ['eggs', 'chicken breast', 'celery'],
location: 'Vancouver, BC',
retailers: ['loblaws', 'saveonfoods', 'tnt'],
minMatchConfidence: 'medium',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const cheapest = items
.filter((r) => r._type === 'comparison')
.sort((a, b) => a.comparable_unit_price - b.comparable_unit_price);Three things to know before you trust a number
- Unit prices need a shared basis.
comparable_unit_pricenormalizes to whatevercomparable_unit_basisnames (100g,100ml,eachorlb). That’s what puts a 1.89 L Superstore carton and a 2 L Save-On-Foods one on one scale. Checkis_comparable_unit_pricebefore you sort on it. - A partial run still returns. If one retailer fails,
run_meta.retailer_statusgives it a verdict ofsuccess,no_results,failedwith a reason, orskipped. Read that before you conclude a chain was expensive when it actually returned nothing. - Only six retailers run by default.
tnt,loblaws,superstore,nofrills,saveonfoodsandpricesmart. The other ten are opt-in, so name them inretailersif you want Quebec (maxi,provigo) or Atlantic Canada.
Output modes
outputMode changes the shape of what lands in the dataset. Start with comparison unless you know you want another.
| Mode | Best for |
|---|---|
comparison | Flat rows, one matched product per query per retailer. CSV, Excel and ETL. |
comparison_grouped | One record per query with all retailer results nested. Good shape for feeding an LLM or an agent. |
summary | Cheapest retailer, price range, median price and sale count per query. |
category_browse | Full category listings per retailer, with no query matching at all. |
intel | Comparison rows plus summaries, category stats, top deals and a run summary. |
If you want one chain in depth instead of a shallow slice of sixteen, the retailer guides go further: Loblaws and its eleven sibling banners, Save-On-Foods and the Pattison chains, or T&T.
Frequently asked questions
Are the matches exact?
Not always, and the Actor doesn’t pretend otherwise. It compares retailer search results, and grocery products vary by brand, size and naming. Every row carries match_score, match_confidence and the reasons behind them, so you can filter with minMatchConfidence and check the ones you care about against source_url.
Which retailers run by default?
Six national chains: tnt, loblaws, superstore, nofrills, saveonfoods and pricesmart. The other ten are opt-in through the retailers array.
How do I get a basket total per store?
Set includeSummaries to true. Each run then also emits basket_summary rows with the total per retailer, and item_best_options naming the cheapest source for each item.
Does it cover Sobeys, Metro, Walmart or Costco?
Sobeys, Metro and Walmart Canada fall outside the 16 chains. Costco has its own Actor, since its warehouse pricing works differently.
How do unit prices compare across chains with different pack sizes?
comparable_unit_price normalizes to a basis named in comparable_unit_basis (100g, 100ml, each or lb), which is what puts a 1.89 L Superstore carton and a 2 L Save-On-Foods one on the same scale. is_comparable_unit_price marks the rows where that holds.
What happens if one retailer fails mid-run?
The run still returns everything else. run_meta.retailer_status gives each retailer a verdict of success, no_results, failed with a reason, or skipped, so a partial run tells you which one came up short instead of quietly returning less.
Should I use this or a single-retailer Actor?
Use this one to compare across chains. Use a retailer Actor when you want one chain in depth, with full category trees and whole-store scrapes.
Does it track price history?
No. Each run returns current prices. Schedule the same basket and keep each dataset if you want history.
Try it on your own data
The Canadian Grocery Price Comparison API runs on Apify. Paste an input, hit Start, and see what comes back before you write any code against it.
Get the Canadian Grocery Price Comparison APIOther retailers
- Loblaws, No Frills & Superstore API12 Loblaw banners with PC Optimum offers and multi-buy deals.
- Save-On-Foods & PriceSmart APIFour Pattison Food Group chains across BC and the prairies.
- T&T Supermarket APICanada’s largest Asian grocery chain, in BC, AB, ON and QC.
- Costco Scraper APIWarehouse prices, deals and item numbers from costco.ca and costco.com.
They all share the same field names. See them side by side on the grocery data API page, with a sample record for each.
Keep reading
- How to Track Grocery Inflation with a Fixed Basket of StaplesTen staples, priced weekly, turned into an index. Including the two things that quietly corrupt a grocery price series if you don’t watch for them.
- How to Scrape Loblaws, Superstore and No Frills PricesTwelve banners share one input and one output shape. Which banner covers your region, the promo fields only this chain exposes, and how to join across banners.
- How to Scrape Save-On-Foods, PriceSmart and Urban Fare PricesSame group, same platform, different prices. The same milk runs $7.99 at Save-On-Foods and $8.49 at Urban Fare on the same day in the same city.
- How to Scrape T&T Supermarket PricesThe catalogue the mainstream Canadian scrapers skip. Also the three limitations I’d want to know before building anything on it.
- How to Scrape Costco Prices from costco.ca and costco.comPaste the input, get rows back. Then the three things that cost me time, and where the unit prices come from, since Costco publishes none.