# Flashfood Deals Scraper — Grocery Markdowns, Canada + US (`yugenox/flashfood-deals-scraper`) Actor

Scrape Flashfood's discounted near-expiry groceries by postal code, ZIP, city, store or the whole network: sale vs regular price, % off, best-before date, quantity left, department, barcode (GTIN) and store details. Monitor mode tracks new, repriced and sold-out items between runs.

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

## Pricing

from $1.50 / 1,000 discounted items

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

## Flashfood Deals Scraper — Grocery Markdowns in Canada + the US

Scrape the discounted, close-to-best-before groceries listed on **Flashfood** at No Frills, Loblaws, Real Canadian Superstore, Maxi, Provigo, Zehrs, Atlantic Superstore, Giant Eagle, Meijer, Tops, DashMart and hundreds of independents. Search by **postal code, ZIP, city or coordinates**, pull **specific stores**, or sweep the **whole network** (about 2,100 stores and 30,000+ live deals).

Each row is one discounted item: **sale price vs regular price, % off, best-before date, days left, quantity left, department (English + French), barcode (GTIN) when available**, photos, and the store it is at, with address, coordinates, distance, opening hours and pickup instructions.

**Monitor mode** compares each run with the previous one and outputs only what changed: new markdowns, price and quantity changes (with units sold since the last run), and items that sold out or expired.

No account or login is needed.

### What you can do with it

- **Deal-finder apps and newsletters**: "50%+ off meat within 5 km of M5V", refreshed every hour.
- **Shrink and markdown analytics for grocers and CPG brands**: what gets marked down, where, how deep, how close to expiry, and how fast it sells.
- **Price research**: pair the `loblawUpc` / `gtin` fields with regular shelf-price data to compare markdown prices with regular prices.
- **Food-rescue and community groups**: find surplus fresh food near a neighbourhood.
- **Store directories**: every Flashfood partner store with coordinates and hours (stores-only mode).

### Input examples

**Deals near a postal code** (the default form):

```json
{ "locations": ["M5V 2T6"], "radiusKm": 20, "maxItems": 200 }
```

**Several cities, only meat and seafood, 40%+ off, best before within 3 days:**

```json
{
  "locations": ["Toronto, ON", "Montréal, QC", "Pittsburgh, PA"],
  "radiusKm": 30,
  "departments": ["Meat", "Seafood"],
  "minDiscountPercent": 40,
  "expiringWithinDays": 3
}
```

**Specific stores** (use `storeId` from any earlier run):

```json
{ "storeIds": ["5ccc4fa913ed23170c14db02"], "includeStores": true }
```

**The whole network, Canada only:**

```json
{ "fullNetwork": true, "countries": ["CA"] }
```

**Hourly change feed** (schedule this):

```json
{ "locations": ["M5V 2T6"], "radiusKm": 10, "monitorMode": true, "stateKey": "toronto-core" }
```

#### Locations

Any of these work:

- a Canadian postal code: `M5V 2T6`, or just the first three characters `M5V`
- a US ZIP code: `48226`
- a place: `Toronto, ON`, `Pittsburgh, PA`, `Montréal`, `Detroit, Michigan, USA`
- coordinates: `43.6532,-79.3832`

Every store within `radiusKm` is scraped, nearest first. There is no cap of 100 stores: large areas are split automatically until every store has been found.

#### Filters

| Input | What it keeps |
|---|---|
| `banners` | Chains, e.g. `No Frills`, `Maxi`, `Giant Eagle`, `Meijer` (ignores case, spaces and accents) |
| `countries` | `CA` and/or `US` |
| `keywords` | Item names containing a word, e.g. `salmon`, `poulet`, `bread` |
| `departments` | Departments, in English or French, e.g. `Meat`, `Boulangerie` |
| `minDiscountPercent` | At least this % below the regular price |
| `maxPrice` | Sale price at or below this amount |
| `expiringWithinDays` | Best before within N days (0 = today) |
| `maxItems` / `maxStores` | Caps for the whole run |

### Output

One row per item (`kind: "item"`). This is a real row:

```json
{
  "kind": "item",
  "itemId": "6ab30bbf0be6e43a5fab9833",
  "name": "PURI FANC GEMS MSE TUNA",
  "price": 1.25,
  "originalPrice": 2.5,
  "discountPercent": 50,
  "savings": 1.25,
  "currency": "CAD",
  "quantityAvailable": 1,
  "bestBefore": "2027-06-01",
  "daysToExpiry": 251,
  "department": "Grocery",
  "departmentFr": "Épicerie",
  "legacyDepartment": "Grocery",
  "storageTreatment": "NONE",
  "isSnapEligible": false,
  "listedAt": "2026-09-22T23:14:07.233Z",
  "imageUrl": "https://img.flashfood.com/products/lcl/media/en/5580/00005580055068_0_a1c1_000600_front_a05_@2.jpeg",
  "imageGallery": ["https://img.flashfood.com/products/lcl/media/en/5580/00005580055068_0_a1c1_000600_front_a05_@2.jpeg"],
  "gtin": "00005580055068",
  "loblawUpc": "05580055068",
  "storeId": "5f2cdb299108010bf40533e2",
  "storeName": "Jotis No Frills Vancouver",
  "banner": "Nofrills",
  "storeAddress": "310 West Broadway",
  "city": "Vancouver",
  "stateCode": "BC",
  "countryCode": "CA",
  "storeLatitude": 49.2628406,
  "storeLongitude": -123.1115665,
  "distanceKm": 1.57,
  "timezone": "America/Vancouver",
  "storeHours": { "mon": "08:00-21:00", "tue": "08:00-21:00", "wed": "08:00-21:00", "thu": "08:00-21:00", "fri": "08:00-21:00", "sat": "08:00-21:00", "sun": "08:00-21:00" },
  "isEBTEnabled": false,
  "pickupInstructions": "Please see a cashier for order pickup and confirmation.",
  "queryLocation": "Vancouver, BC",
  "scrapedAt": "2026-09-24T05:40:47.467Z"
}
```

#### Fields

**Item**: `itemId`, `name`, `price`, `originalPrice`, `discountPercent`, `savings`, `currency` (CAD or USD), `quantityAvailable`, `bestBefore` (YYYY-MM-DD), `daysToExpiry` (counted from today's date at the store), `department`, `departmentFr`, `legacyDepartment`, `storageTreatment` (`FRESH_TO_FROZEN` means "freeze on arrival"), `isSnapEligible` (US), `listedAt`, `imageUrl`, `imageGallery`, `gtin`, `loblawUpc`

**Store**: `storeId`, `storeName`, `banner`, `storeAddress`, `city`, `stateCode`, `countryCode`, `storeLatitude`, `storeLongitude`, `distanceKm` (from your location), `timezone`, `storeHours`, `isEBTEnabled`, `pickupInstructions`

**Run**: `queryLocation`, `scrapedAt`

**Monitor mode adds**: `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `SOLD_OUT`, `EXPIRED`), `changedFields`, `previousPrice`, `previousQuantity`, `quantityDelta`, `unitsSoldSinceLastRun`, `firstSeenAt`, `hoursSinceLastRun`

**Store rows** (`includeStores` or `storesOnly`, `kind: "store"`): address, coordinates, hours, pickup and checkout instructions, logo, website, and `itemCount`, the number of deals the store has listed right now.

A run summary (counts per location, change totals in monitor mode) is saved as the `RUN_REPORT` record in the run's key-value store.

#### Barcodes

`gtin` is filled for items listed from a retailer's product catalogue: most packaged goods at Loblaw banners, DashMart, Ren's Pets and some US chains. That is about 44% of all items across the network and around 60% in big Canadian cities. Items a store photographed itself, such as produce boxes and in-store bakery goods, have no barcode, so `gtin` is `null` for them.

`loblawUpc` is the same code in the format Loblaw's own product data uses (11 digits, no check digit). Use it to join Flashfood markdowns onto Loblaw shelf prices. It is only filled for Loblaw-banner packaged items.

### Monitor mode

Turn on `monitorMode` and schedule the actor, for example hourly. Each run is compared with the previous run that used the same `stateKey`:

- **NEW**: the item was not listed last time.
- **UPDATED**: the price, regular price, quantity or best-before date changed. `changedFields` lists which ones, and `unitsSoldSinceLastRun` shows how many units went since the last run.
- **SOLD_OUT**: the item was listed last time and is gone now, before its best-before date.
- **EXPIRED**: the item is gone and its best-before date has passed.

Unchanged items are left out, and not billed, unless you turn on `emitUnchanged`. Only stores that were fully scraped in a run can report sold-out items, so a run cut short by `maxItems` never reports false sell-outs.

### Coverage

Flashfood is **Canada-first**. Canada has about 1,200 stores (Ontario, Quebec, Alberta, BC, the Maritimes and the Prairies), mostly Loblaw banners plus independents, DashMart and Ren's Pets. The US has about 900 stores, concentrated in Michigan, Ohio, Pennsylvania, New York, Indiana and Virginia (Giant Eagle, Meijer, Tops, Kroger, SpartanNash and regional grocers). A full network sweep on 2026-09-24 returned 33,082 live items: 18,336 in Canada and 14,746 in the US.

Some places have no Flashfood stores, including Boston and Washington DC. A location with no stores in range finishes normally with 0 items and a status message saying so, and no item is charged (only the small per-run start fee).

### Speed

- A 20 km radius in a big city: roughly 5–15 seconds for 1,000–2,500 items.
- The whole network: about 3 minutes for about 33,000 items.

### FAQ

**How fresh is the data?** It is live. Every run reads what is listed on Flashfood at that moment, and markdowns change throughout the day.

**Do I need a Flashfood account?** No. No login needed: no Flashfood account, cookie, phone or API key of your own. Datacenter proxies are enough (the free automatic Apify Proxy group works).

**Is it legal to scrape Flashfood?** This actor collects only publicly available information that the Flashfood app shows to anyone browsing deals without signing in: the discounted items each store lists (name, price, regular price, best-before date, quantity, photo) and the stores' business details (address, hours, pickup instructions). It does not collect information about individual people: it never touches user accounts, orders, carts or payments. Scraping public data is generally lawful, but how you use the results is up to you: follow the laws that apply to you (for example PIPEDA, CCPA or GDPR) and Flashfood's terms, and check with a lawyer if you are unsure. Apify's guide is a good starting point: [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/)

**Does it access any private data?** No. It reads only the public store and deal listings. Everything behind a Flashfood login (your account, orders, payment methods) is never requested.

**Why is `gtin` empty for some items?** Those items were photographed in the store rather than listed from a product catalogue, so there is no barcode to report. See [Barcodes](#barcodes).

**What does `daysToExpiry: 0` mean?** The best-before date is today, at the store's local time.

**Can I get a store's full list of deals?** Yes. Put its `storeId` in `storeIds` and leave the filters empty.

**What does a run cost?** You pay per item row, plus the small per-run start fee shown on the Pricing tab. Store rows that come with items (`includeStores`) are free; in stores-only mode each store row is billed as a `store` event, because the store list is then the whole result. A run that finds nothing, or that stops because of an input problem, is charged the start fee only.

**What if something goes wrong?** The run still finishes as *Succeeded*, keeps everything it collected, and explains what happened in its status message. If your run's timeout is too short for Flashfood to answer, the message says so (60 s is plenty for most runs).

**What if Flashfood updates its app?** The scraper adapts on its own: it picks up a new app access key, slows down if Flashfood starts rate-limiting, and adjusts if Flashfood changes how many stores it returns per request. The status message says when that happened.

# Actor input Schema

## `locations` (type: `array`):

Where to look. Each entry can be a Canadian postal code (M5V 2T6 or just M5V), a US ZIP code (48226), a place ("Toronto, ON", "Pittsburgh, PA", "Montréal") or coordinates ("43.65,-79.38"). Every Flashfood store within the radius below is scraped, nearest first. A store found from several locations is only scraped once.

## `radiusKm` (type: `integer`):

Search radius around each location, in kilometres (up to 400). 5–10 km is a neighbourhood, 20 km a city, 50 km a metro area.

## `storeIds` (type: `array`):

Specific Flashfood stores to scrape, by their 24-character id (the storeId field of any earlier run, e.g. 5ccc4fa913ed23170c14db02). Can be combined with locations.

## `fullNetwork` (type: `boolean`):

Scrape every Flashfood store in Canada and the US (about 2,100 stores). Use "Countries" to keep one country. Locations and the radius are not needed.

## `countries` (type: `array`):

Only stores in these countries. Leave empty for both.

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

Only these chains, e.g. No Frills, Loblaws, Real Canadian Superstore, Maxi, Provigo, Zehrs, Meijer, Giant Eagle, Kroger, Tops, DashMart. Matching ignores case, spaces and accents ("No Frills" = "Nofrills"). Leave empty for all.

## `keywords` (type: `array`):

Only items whose name contains one of these words (matched at the start of a word, case- and accent-insensitive), e.g. salmon, chicken, poulet, bread. Leave empty for everything.

## `departments` (type: `array`):

Only these departments, English or French, e.g. Meat, Seafood, Produce, Bakery, Deli, Dairy, Boucherie. Partial names match.

## `minDiscountPercent` (type: `integer`):

Only items at least this much below their regular price.

## `maxPrice` (type: `string`):

Only items at or below this sale price, in the store's currency, e.g. 5 or 4.99.

## `expiringWithinDays` (type: `integer`):

Only items whose best-before date is at most this many days away (0 = today).

## `maxItems` (type: `integer`):

Maximum number of item rows for the whole run. Leave empty for no limit (a 20 km radius in a big Canadian city is typically 1,000–3,000 items; the whole network is 30,000+).

## `maxStores` (type: `integer`):

Stop after this many stores (nearest first). Leave empty for no limit.

## `includeStores` (type: `boolean`):

Also output one row per store (address, coordinates, opening hours, pickup instructions, number of deals listed). Store rows are free.

## `storesOnly` (type: `boolean`):

Output only the store rows — a Flashfood store directory for the area or the whole network. No item rows. Each store row is billed as one `store` event (store rows next to items stay free).

## `monitorMode` (type: `boolean`):

Compare with the previous run of the same monitor key and output only what changed: NEW items, UPDATED ones (price, quantity or best-before changed, with quantity sold since last run), and SOLD_OUT / EXPIRED items that disappeared. Schedule it (e.g. hourly) to track markdowns and sell-through.

## `stateKey` (type: `string`):

Name of the saved state to compare against. Runs with the same key compare with each other. Leave empty to derive one from your locations and filters.

## `emitUnchanged` (type: `boolean`):

In monitor mode, also output items that did not change (marked UNCHANGED).

## `emitSoldOut` (type: `boolean`):

In monitor mode, output items that disappeared since the last run (SOLD_OUT, or EXPIRED when their best-before date has passed).

## `maxConcurrency` (type: `integer`):

Parallel requests. The default is fast and gentle.

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

Apify Proxy is required. Datacenter proxies work; the actor switches to residential by itself if they stop working.

## `apiKeyOverride` (type: `string`):

Advanced / support only.

## `apiBaseUrlOverride` (type: `string`):

Advanced / support only.

## Actor input object example

```json
{
  "locations": [
    "M5V 2T6"
  ],
  "radiusKm": 20,
  "fullNetwork": false,
  "maxItems": 200,
  "includeStores": false,
  "storesOnly": false,
  "monitorMode": false,
  "emitUnchanged": false,
  "emitSoldOut": true,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items (and store rows when enabled).

## `summary` (type: `string`):

Status message, counts per location, requests, and the monitor-mode change summary.

# 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 = {
    "locations": [
        "M5V 2T6"
    ],
    "maxItems": 200,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("yugenox/flashfood-deals-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 = {
    "locations": ["M5V 2T6"],
    "maxItems": 200,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("yugenox/flashfood-deals-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 '{
  "locations": [
    "M5V 2T6"
  ],
  "maxItems": 200,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call yugenox/flashfood-deals-scraper --silent --output-dataset

```

## MCP server setup

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