FoodHero Scraper - Grocery Deals, Prices & Stores avatar

FoodHero Scraper - Grocery Deals, Prices & Stores

Pricing

from $1.20 / 1,000 deal or store records

Go to Apify Store
FoodHero Scraper - Grocery Deals, Prices & Stores

FoodHero Scraper - Grocery Deals, Prices & Stores

Scrape FoodHero surplus grocery deals across Canada by area, store or offer ID. Extract products, brands, regular and discounted prices, savings, portions left, best-before dates and CO2 savings, plus store names, chains, addresses, coordinates and types.

Pricing

from $1.20 / 1,000 deal or store records

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

FoodHero Scraper: Surplus Grocery Deals in Canada

FoodHero Scraper turns FoodHero into a clean surplus-grocery data feed. FoodHero is the Canadian app that helps grocery stores sell surplus food before it goes to waste, and this actor returns one row per deal: the product, both prices, the discount, how many portions are left, the best-before date, the CO2 saved, and the store behind it. Search any Canadian area, or paste a store or offer id, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

Why This Scraper?

  • Two ways to find deals. Search any Canadian area by coordinates or place name, or paste a store or offer id directly. Leave the area empty to cover every FoodHero city in Canada at once.
  • Full deal economics on every row. Regular price, deal price, discount percent, portions left, best-before date and CO2 saved, all on one flat record.
  • A pasted store returns something. Every deal it lists right now, or a single store row when it currently has none, so a store you named that FoodHero still lists does not come back empty just because it has no surplus today.
  • Optional store enrichment. In search mode, add each store's phone and website to every row, only when you turn it on, so a plain run stays fast.
  • Built for schedules. Incremental mode returns only new, changed and reappeared deals on recurring runs, and EXPIRED rows are only produced after a run that read every area to the end without a limit or a read error stopping it (see the notes under "Resume and recurring updates").
  • Resume without paying twice. Point Resume at an interrupted run and the deals it already saved are skipped.
  • Pipe results into your apps. Optionally send a summary of each deal into Notion, Linear or Airtable through MCP connectors, as a delivery step that never changes the dataset.

Use Cases

  • Deal-hunting apps and browser extensions: surface today's steepest surplus discounts for a city or a shortlist of favourite stores.
  • Food-waste and sustainability research: track CO2 saved and portions rescued across Canadian grocery chains over time.
  • Price and discount monitoring: watch a handful of stores every morning and get back only what is new or has changed.
  • Retail and chain analytics: compare discount depth, sell-through and department mix across banners and provinces.
  • Local deal roundups and community boards: pull today's best-before-soon bargains near a neighbourhood.

Data You Get

Sample shape: values are illustrative placeholders, not from a live record.

FieldExample
recordType"offer" (also "store" for a pasted target that lists nothing right now)
offerId"64f1a2b3c4d5e6f7a8b9c0d1"
urlFoodHero app link to the offer or store
productName"Frozen Berry Medley"
productBrand / productDescription"Sample Orchards" / "Mixed berries, family pack"
productSize / departmentName"600 G" / "Frozen"
price / discountPrice12.99 / 6.49
discountPercent50
currency"CAD"
itemLeft4
isSoldOut / isLastCallfalse / false
isNew / isFrozentrue / true
bestBefore / bestBeforeAt1798761600 / "2027-01-01T00:00:00+00:00"
co2Volume0.12
productImageUrlphoto of the product
storeId / storeName"64f1a2b3c4d5e6f7a8b9c0aa" / "Sample Grocery Market"
storeType / chainId"Grocery store" / "64f1a2b3c4d5e6f7a8b9c0bb"
address / city / state / postalCode"12 Sample Street, Springfield, ON" / "Springfield" / "ON" / "K1A0B1"
latitude / longitude / distanceKm45.42 / -75.7 / 2.3
phone / websitestore phone and website (search mode: only with store details on; URL mode: always)
productCount / storeOfferCount180 / 42
changeType / changedFields / firstSeenAt / lastSeenAt"UPDATED" / ["discountPrice"] / first and last seen timestamps (incremental mode only)

A pasted store that lists no deal right now comes back as a single "store" row carrying the same store columns, with the deal-only columns empty.

How to Use

  1. Pick a mode: search deals by area, or paste store or offer ids.
  2. For search mode, list one or more areas as place names or lat,lng pairs, set a radius, and optionally filter by store name. Leave the area list empty to cover every FoodHero city in Canada at once.
  3. Turn on Include sold-out deals, or the store-details option to add each store's phone and website in search mode, if you need them, and set Max items (and, optionally, Max pages per area) to size the run.
  4. Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.

Search deals in a couple of cities:

{
"mode": "search",
"locations": ["Toronto", "Vancouver"],
"radiusKm": 15,
"maxItems": 100
}

Every deal at one store, by id (URL mode):

{
"mode": "url",
"urls": ["store/aaaaaaaaaaaaaaaaaaaaaaaa"]
}

National coverage, including sold-out deals:

{
"mode": "search",
"includeSoldOutOffers": true,
"maxItems": 0
}

Recurring monitoring with incremental mode:

{
"mode": "search",
"locations": ["45.5019,-73.5674"],
"incrementalMode": true,
"emitExpired": true,
"stateKey": "montreal-morning",
"maxItems": 0
}

Monitoring runs use coordinates and maxItems: 0 on purpose: a run stopped by Max items never produces EXPIRED rows, and place names carry the lookup risk described below.

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/foodhero-surplus-grocery-scraper").call(
run_input={"mode": "search", "locations": ["Toronto"], "maxItems": 50})
for deal in client.dataset(run["defaultDatasetId"]).iterate_items():
print(deal["productName"], deal["discountPrice"], deal["itemLeft"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/foodhero-surplus-grocery-scraper').call({
mode: 'search', locations: ['Toronto'], maxItems: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.

How Max items and Max pages shape a run

Search mode walks each area in the order given (or the covered Canadian cities, when you leave the area list empty), fully reading one area's deals feed before moving to the next, and stops the whole run once Max items is reached. Max pages per area caps how deep any single area is read (deals are read 50 at a time); leave it at 0 to read as deep as Max items allows. Raise Max items, or set it to 0, to read every deal available across every area.

Max items does not limit EXPIRED rows. With incrementalMode and emitExpired on, EXPIRED rows are added at the end of a run that finished under Max items, on top of the deals it already returned, and each one is billed. A run with Max items 20 that returns 5 live deals can therefore end with 5 rows plus one EXPIRED row for every tracked deal that disappeared since the previous run. Leave emitExpired off if you need Max items to be a hard cap on rows.

Resume and recurring updates

  • Resume (resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor skips the deals it already saved there, so you do not pay twice.
  • Incremental mode (incrementalMode) is for repeating the same search on a schedule. Each deal is classified NEW, UPDATED (with changedFields), UNCHANGED (suppressed and not billed unless emitUnchanged is on), REAPPEARED, or EXPIRED (only with emitExpired). stateKey names or shares the stored state; leave it empty to let the actor derive one from the mode, areas, radius, store-name filter, sold-out setting and pasted ids.
  • When EXPIRED is skipped: a run stopped by Max items or Max pages, a run where a deals page could not be read, a run where a pasted entry was not a valid id, and a resumed run all skip EXPIRED detection and say so in the log.
  • Use coordinates, not place names, for incremental runs with emitExpired. Place names are looked up at the start of every run. If a lookup fails (a name that cannot be found, or the lookup not answering), that area is skipped with only a warning in the log and the run is not treated as partial. When your other areas finish normally, the run counts as complete, so every tracked deal from the skipped area is returned (and billed) as EXPIRED, then as REAPPEARED on the next run that reads that area. lat,lng coordinates are never looked up, so they avoid this. If none of the place names can be looked up, the run falls back to every FoodHero city in Canada.
  • Store details can look like a change. Turning on store details adds one lookup per store. If a store's lookup fails, its rows come back without phone and website and the run still reports success. In incremental mode those rows are then returned (and billed) as UPDATED with only phone and/or website in changedFields, and again as UPDATED on the next run when the lookup succeeds. Switching store details on or off between runs of the same state has the same effect on every row. Keep store details off for incremental monitoring, or ignore UPDATED rows whose only changed fields are phone and website.
  • Resuming into a new incremental state. Resume and Incremental mode can only be combined while that state is still empty. In that run, the deals skipped because the interrupted run already saved them are not added to the new state, so the next incremental run returns (and bills) them once more as NEW. This happens once; from then on they are tracked normally.

Send results into your apps (MCP connectors)

Optionally pipe the collected deals into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the collection: the Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each deal, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify, Settings, Integrations.
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you have not authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where deal pages should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

Input Parameters

ParameterTypeDefaultDescription
modestringsearchsearch (walk the deals feed by area) or url (read only pasted store or offer ids).
locationsarray(none: all of Canada)Search mode only. One entry per area: lat,lng coordinates, or a Canadian place name looked up for you. The form is prefilled with Montreal's coordinates; leave empty to cover every FoodHero city in Canada at once. A place name that cannot be looked up is skipped with a warning in the log.
radiusKminteger25Search mode only. Keep only stores within this distance (km) of each area.
searchStoreNamestring(none)Keep only stores whose name contains this text, matched without case. Applies in both modes.
includeSoldOutOffersbooleanfalseAlso return deals with zero portions left.
enrichStoreDetailsbooleanfalseSearch mode only. Fetch each store's detail page and fill phone and website on its rows (URL-mode rows already carry them). One extra request per store.
urlsarray(none)URL mode only. One target per entry: a bare 24-character id is read as a store, or prefix it (store/<id> or offer/<id>) to be explicit. The form is prefilled with a sample store id.
maxItemsinteger20Stop after this many records (0 = no limit; the run then stops when the deals feed runs out for every area).
maxPagesinteger0Optional. Deals are read 50 at a time; 0 means as deep as the feed goes for each area, bounded by Max items.
resumeFromRunIdstring(none)Continue one interrupted run: paste its run or dataset ID.
incrementalModebooleanfalseReturn only new, changed and reappeared deals on recurring runs of the same selection.
stateKeystring(none)Name or share an incremental-mode state. Leave empty to derive one automatically.
emitUnchangedbooleanfalseAlso return (and bill) deals that have not changed since the last run.
emitExpiredbooleanfalseAlso return (and bill) deals seen before but no longer found. Only produced once a run has fully scanned the tracked selection, and not limited by maxItems.
mcpConnectorsarray(none)Optional: send a summary of each deal to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which deal pages are created.
maxNotifyListingsinteger50Cap on deals exported to each connector per run. Does not affect the dataset.
proxyConfigurationobjectApify ProxyConnection settings. The default works on every plan; the areas you search decide which deals you get back, not the connection's exit point.

Output Example

Sample shape: values are illustrative placeholders, not from a live record.

{
"recordType": "offer",
"offerId": "64f1a2b3c4d5e6f7a8b9c0d1",
"url": "https://foodhero.app.link/offer/64f1a2b3c4d5e6f7a8b9c0d1",
"productName": "Frozen Berry Medley",
"productBrand": "Sample Orchards",
"productDescription": "Mixed berries, family pack",
"productSize": "600 G",
"departmentName": "Frozen",
"price": 12.99,
"discountPrice": 6.49,
"discountPercent": 50,
"currency": "CAD",
"itemLeft": 4,
"isSoldOut": false,
"isLastCall": false,
"isFrozen": true,
"isNew": true,
"co2Volume": 0.12,
"bestBefore": 1798761600,
"bestBeforeAt": "2027-01-01T00:00:00+00:00",
"storeId": "64f1a2b3c4d5e6f7a8b9c0aa",
"storeUrl": "https://foodhero.app.link/store/64f1a2b3c4d5e6f7a8b9c0aa",
"storeName": "Sample Grocery Market",
"storeType": "Grocery store",
"chainId": "64f1a2b3c4d5e6f7a8b9c0bb",
"address": "12 Sample Street, Springfield, ON, K1A0B1",
"city": "Springfield",
"state": "ON",
"postalCode": "K1A0B1",
"country": "CA",
"latitude": 45.42,
"longitude": -75.7,
"distanceKm": 2.3,
"productCount": 180,
"storeOfferCount": 42
}

Plan Requirement

The default connection setting works out of the box on every plan. FoodHero is Canada-only, so it is the areas you search, not where the connection exits, that decides which deals you get back.

FAQ

How much does it cost?

You pay per record returned (one event per deal or store row, including any UNCHANGED or EXPIRED rows you turn on), plus a small start charge per run. The Pricing tab shows the current rates. Use Max items to cap the live deals in a run; note that it does not limit EXPIRED rows.

This actor collects only the deals and store information FoodHero's own app shows to any shopper who opens it. You are responsible for how you use the data: follow FoodHero's terms and the laws that apply to you, and get legal advice before any commercial redistribution.

Can I get only new or changed deals on a schedule?

Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. From the second run on, you get back only NEW, UPDATED and REAPPEARED deals, and unchanged ones are not billed.

What is the difference between Resume and Incremental mode?

Resume finishes ONE interrupted run: paste that run's ID and the deals it already saved are skipped, so you do not pay twice for the same collection. Incremental mode is for running the same search again and again (daily, say) and getting back only what changed since the previous run. Use Resume for a run that stopped partway through; use Incremental mode for recurring monitoring.

Why did my run fail instead of returning an empty dataset?

If the connection is refused before any deal could be read, or none of the pasted entries is a valid store or offer id, the run stops with a clear message so "no deals right now" is never confused with "nothing could be read". A genuinely quiet area, or a valid id that FoodHero no longer lists, still succeeds with zero records.

Why did I get EXPIRED or UPDATED rows for deals that did not change?

Two known cases in incremental mode. If a place name in locations could not be looked up, that area is skipped with only a log warning, and its tracked deals can come back as EXPIRED; use lat,lng coordinates instead. If store details are on and a store's lookup failed, its rows lose phone and website and come back as UPDATED with only those fields in changedFields. Both are described under "Resume and recurring updates" above.

Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

๐Ÿ”— Want more grocery data?

Pair this actor with these related scrapers from the same team:

๐Ÿฅฆ ResQ Club Scraper
Scrape ResQ Club surplus food offers in Finland, Sweden and Estonia: name, price, current...
๐Ÿฅฆ Flashfood Scraper
Scrape Flashfood discounted groceries and stores by location or store. Extract current...
๐Ÿฅฆ Instacart Scraper
Scrape Instacart grocery catalogs with per-store prices. Run keywords across several...
๐Ÿฅฆ E.Leclerc Scraper
Scrape E.Leclerc France (e.leclerc) grocery and retail products. Search by keyword or...
๐Ÿฅฆ Woolworths AU Scraper
Scrape Woolworths Australia (woolworths.com.au) grocery products and reviews. Search by...
๐Ÿฅฆ ALDI AU Scraper
Scrape ALDI Australia (aldi.com.au) products: name, brand, price, was-price, savings...

๐Ÿ‘‰ Browse all abotapi scrapers

๐Ÿ’ฌ Support

  • ๐Ÿž Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
  • ๐Ÿ› ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
  • โญ Enjoying it? A quick review on the actor page helps other users find it.