# Tim Hortons, Burger King, Popeyes & Firehouse Subs Scraper (`yugenox/rbi-restaurants-scraper`) Actor

Store locations and store-level menu prices for Tim Hortons, Burger King, Popeyes and Firehouse Subs: address, hours, franchisee, amenities; every item, size and combo with pickup + delivery price, calories, EN/FR names and price changes. Canada, USA + 18 countries. No login.

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

## Pricing

from $1.00 / 1,000 result rows

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

## Tim Hortons, Burger King, Popeyes & Firehouse Subs Scraper

Get **every store** and **every store's own menu prices** for four of North America's biggest restaurant chains in one actor: **Tim Hortons, Burger King, Popeyes and Firehouse Subs**.

- **Complete store lists, no map-tiling or guesswork.** About 22,000 locations across Canada, the USA and 18 more countries: address, coordinates, phone, time zone, franchisee, hours per channel (dining room, drive-thru, delivery, curbside, catering), amenities (drive-thru, Wi-Fi, playground, parking, halal…) and online-ordering status.
- **Store-level menu prices.** One row per item, size and combo per store, with **that store's** price. Prices really differ from store to store. Between one Toronto and one Vancouver Tim Hortons, 229 of 640 shared items had different prices. A Whopper in Chicago ranges from $5.89 to $6.19 depending on the store.
- **Pickup and delivery prices** (Burger King always; Tim Hortons and Firehouse Subs on request), useful for delivery-markup analysis.
- **English + French item names** for Canada, categories, sizes grouped under their product, calories, product photos, and optional full nutrition facts and allergens.
- **Price-change tracking.** Run it on a schedule and get only what changed since your last run: price increases and decreases, new items, removed items.
- Fast and light: pure HTTP, no browser, no login. A store's full menu takes about 2 seconds.

### What you can scrape

| Brand | Store data | Menu prices |
|---|---|---|
| Tim Hortons | Canada (~3,960), USA (~700) | Canada + USA |
| Burger King | USA (~6,500), Canada (~380), UK, Germany, Spain, Portugal, Netherlands, Switzerland, Poland, Czechia, Ireland, Romania, New Zealand, South Africa, Saudi Arabia, Mexico, Brazil | USA + Canada |
| Firehouse Subs | USA (~1,280), Canada (~200) | USA + Canada |
| Popeyes | USA (~3,100), Canada (~380), Spain, New Zealand, Poland, France, Italy, Czechia, Romania, South Korea, Switzerland | not available (store data only) |

Menu prices are available for stores that offer online ordering (about 93% of Tim Hortons stores and 98% of Burger King US stores). Other stores still get a full store row.

### Input

| Field | What it does |
|---|---|
| **Brands** | Any of Tim Hortons, Burger King, Popeyes, Firehouse Subs. |
| **Countries** | Canada and USA by default. |
| **What to output** | Stores, Menu items & prices, Price changes since last run. |
| **Cities** | Store address city, e.g. `Toronto`, `North York`, `Montréal`, `New York`. Add a province/state to disambiguate: `London, ON`. |
| **Provinces / states** | `ON`, `QC`, `NY`, `TX`… or full names. |
| **Postal / ZIP code prefixes** | `M5V`, `H2`, `100`… |
| **Near a point + radius** | `43.6532, -79.3832` and a radius in km. Returns the nearest stores first, with `distanceKm`. |
| **Store IDs** | Specific store numbers. Use `burgerking:12871` when scraping several brands. Listed stores are always included, whatever Max stores says. |
| **Delivery prices** | Adds delivery prices for Tim Hortons and Firehouse Subs (Burger King always has them). |
| **Nutrition & allergens** | Full nutrition facts plus allergens split into *contains / may contain / shared equipment*. |
| **Only items containing** | Keep only items whose name contains a keyword (`iced capp`, `whopper`). Good for tracking a basket across hundreds of stores. |
| **Max stores / Max rows** | Limits. Max stores defaults to 10; set it to `0` to scrape every matching store. |

Leave all location filters empty to get every store of the selected brands and countries.

#### Example inputs

All Tim Hortons stores in Canada (store rows only, well under a minute):

```json
{ "brands": ["timhortons"], "countries": ["CA"], "outputs": ["stores"], "maxStores": 0 }
```

Menu prices at the 20 Burger King and Tim Hortons stores nearest downtown Montréal, with delivery prices:

```json
{ "brands": ["burgerking", "timhortons"], "countries": ["CA"], "nearLocation": "45.5019, -73.5674", "radiusKm": 5, "maxStores": 20, "includeDeliveryPrices": true }
```

Weekly price tracker for Iced Capps across Ontario (schedule it; the first run saves a baseline):

```json
{ "brands": ["timhortons"], "countries": ["CA"], "provinces": ["ON"], "maxStores": 0, "outputs": ["priceChanges"], "itemKeywords": ["iced capp"], "priceHistoryName": "ontario-iced-capp" }
```

### Output

Every row has a `type`: `store`, `menuItem` or `priceChange`. The dataset has ready-made **Menu prices**, **Stores** and **Price changes** table views, and you can export to JSON, CSV, Excel or via API.

**Store row**

```json
{
  "type": "store",
  "brand": "Tim Hortons",
  "country": "CA",
  "storeId": "100141",
  "storeName": "2700, Rue Ontario Est Montreal, Quebec H2K 1W9 - Canada",
  "address": "2700, Rue Ontario Est",
  "city": "Montreal",
  "province": "QC",
  "postalCode": "H2K 1W9",
  "latitude": 45.534464,
  "longitude": -73.551343,
  "phone": "5145261707",
  "email": "TimHortons100141@timzone.com",
  "timezone": "America/Toronto",
  "franchiseGroupName": "9516-0487 QUEBEC INC.",
  "status": "Open",
  "hasMobileOrdering": true,
  "hasDriveThru": false,
  "hasDelivery": true,
  "hasWifi": true,
  "diningRoomHours": { "mon": "00:00-23:59", "tue": "00:00-23:59", "…": "…" },
  "driveThruHours": null,
  "deliveryHours": { "mon": "00:00-23:59", "…": "…" },
  "menuStatus": "scraped",
  "menuItemCount": 801,
  "mapsUrl": "https://www.google.com/maps/search/?api=1&query=45.534464,-73.551343",
  "scrapedAt": "2026-09-24T06:00:42.348Z"
}
```

**Menu item row**

```json
{
  "type": "menuItem",
  "brand": "Burger King",
  "country": "CA",
  "storeId": "12829",
  "storeCity": "Montreal",
  "storeProvince": "QC",
  "itemId": "75197f10-1174-408b-bcb1-7cd3af5e6b40",
  "itemType": "combo",
  "name": "Maple Bacon BBQ Whopper® Medium Combo",
  "nameFr": "Trio Maple Bacon BBQ Whopper® format moyen",
  "category": "Limited Time Only",
  "parentName": "Maple Bacon BBQ Whopper®",
  "variant": "Medium Combo - Side and Drink Included",
  "price": 14.99,
  "pickupPrice": 14.99,
  "deliveryPrice": 16.99,
  "currency": "CAD",
  "isAvailable": true,
  "calories": 1490,
  "caloriesMin": 1170,
  "caloriesMax": 2300,
  "imageUrl": "https://…/image-f131008cc31a498884f172d8b85c188f16e6c70b-1500x1500-png",
  "scrapedAt": "2026-09-24T06:00:42.349Z"
}
```

**Price change row** (only with *Price changes since last run*)

```json
{
  "type": "priceChange",
  "brand": "Tim Hortons",
  "storeId": "102601",
  "storeCity": "Toronto",
  "name": "Medium Patronus Sparkling Quencher",
  "changeType": "priceChanged",
  "previousPrice": 3.99,
  "price": 4.19,
  "priceChange": 0.2,
  "currency": "CAD",
  "previousScrapedAt": "2026-09-17T06:00:00.000Z",
  "scrapedAt": "2026-09-24T06:00:00.000Z"
}
```

`changeType` is `priceChanged`, `newItem` or `removedItem`.

#### Field notes

- `price` is the pickup price, or the delivery price when an item is delivery-only. Prices are in the store's currency (`CAD`, `USD`).
- `itemType`: `item` (a single product or size), `combo` (a meal) or `picker` (a product with several sizes, priced at its default size). Sizes point to their product in `parentName`. Burger King rows also carry the chosen option in `variant` (e.g. "Medium Combo - Side and Drink Included").
- **Firehouse Subs combos** are priced as the combo upgrade (side + drink). The sandwich they are built on is in `mainItemName` / `mainItemPrice`.
- `productHierarchy` (Tim Hortons) is the brand's own product classification, e.g. "Cold Beverages > Ready To Drink > …".
- `menuStatus` on store rows: `scraped`, `no-online-ordering` (the store doesn't sell online, so it has no online menu), `not-available`, `not-supported` (Popeyes and international stores) or `error`.
- Hours are local store time. A day with `null` means no hours for that channel that day.
- `cmsUpdatedAt` is when the brand last updated the store's listing.
- `franchiseGroupName` is the franchisee company and `email` the store's own mailbox. If a franchisee is listed under a person's own name, or a store lists a personal mailbox, the field is left empty: the actor only outputs business identities.

### Use cases

- **Menu price intelligence and inflation tracking.** Follow the same basket across hundreds of stores and weeks.
- **Delivery-markup analysis.** Compare pickup vs in-app delivery price per item and store.
- **Franchise and site-selection research.** Get every location with franchisee company, drive-thru, hours and coordinates.
- **Competitive benchmarking.** Put Tim Hortons, Burger King, Popeyes and Firehouse Subs locations and prices side by side.
- **Nutrition and allergen datasets** for food apps.

### FAQ

**Why do some stores have no menu?** Only stores that sell through the brand's app or website have an online menu. Other stores (some airports, kiosks, a few franchise locations) get a store row with `menuStatus: "no-online-ordering"`.

**Why are there several rows for one drink?** Each size is its own row (Small / Medium / Large), and so is each combo size. Use `parentName` to group them.

**How many rows is a menu?** Roughly 400–800 per store. Use *Only items containing* or *Max rows* to keep runs small.

**How often do prices change?** Promotions rotate every few weeks and prices move store by store. Schedule a daily or weekly run with *Price changes since last run* to get just the differences.

**Do I need an account, a login or cookies?** No login needed. The actor reads the same public store locator and online-ordering menu data the brands' own websites and apps show to anyone, without an account, cookies or an API key. Just pick brands and locations.

**Is it legal to scrape Tim Hortons, Burger King, Popeyes and Firehouse Subs?** This Actor only collects publicly available business data: store locations, hours, amenities, franchisee companies, store phone numbers and mailboxes, and the menu prices each store publishes for online ordering. It doesn't collect personal data: a franchisee listed under a person's own name or a personal email address is left out. Collecting publicly available data is generally legal, but you're responsible for how you use it. You must follow the brands' terms and the privacy and anti-spam laws that apply to you (for example PIPEDA and CASL in Canada, CAN-SPAM in the US) — especially before contacting stores. If you're unsure, check with a lawyer. More on this: [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/)

**Does it access any private data?** No. It never logs in, never touches customer accounts or orders, and never gets around a site's bot protection.

**Does it work for Popeyes menus?** Not yet. Popeyes' menu service is behind bot protection that this actor respects rather than bypasses, so Popeyes is store data only.

**What if my timeout is short?** The run stops a little before its timeout, keeps everything it saved and ends successfully. If the timeout was too short to get anything, the status message says what to set (120 s is plenty for a few stores with menus). A short summary of every run is saved as `RUN_REPORT` in the run's key-value store.

**What does it cost?** You pay per row saved (a store, a menu item or a price change) plus a small fee when a run starts. A store menu is roughly 400–800 rows, so use *Only items containing*, *Max stores* or *Max rows* to keep runs small.

# Actor input Schema

## `brands` (type: `array`):

Which restaurant chains to scrape. Store data is available for all four brands. Store-level menu prices are available for Tim Hortons (Canada + USA), Burger King (USA + Canada) and Firehouse Subs (USA + Canada). Popeyes is store data only.

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

Countries to include. Canada and the USA have store data + menu prices. The other countries are store data only (Burger King and Popeyes international locations).

## `outputs` (type: `array`):

Row types to save. Stores: address, phone, hours per channel, amenities, franchisee. Menu items & prices: every priced item, size and combo at each store, with that store's price. Price changes: what changed since your previous run with price changes turned on (the first run saves a baseline).

## `cities` (type: `array`):

Only stores in these cities, as written in the store address ("Toronto", "North York", "Montréal", "New York"). Accents and case don't matter. Add a province or state to disambiguate: "London, ON". Leave all location filters empty to get every store of the selected brands and countries.

## `provinces` (type: `array`):

Only stores in these provinces or states, as 2-letter codes ("ON", "QC", "NY", "TX") or full names.

## `postalCodes` (type: `array`):

Only stores whose postal or ZIP code starts with one of these ("M5V", "H2", "100").

## `nearLocation` (type: `string`):

Only stores within the radius below of this point, nearest first. Example: "43.6532, -79.3832" (downtown Toronto).

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

Search radius around the point above.

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

Specific store numbers (the storeId field, e.g. "102601"). Prefix with the brand when you scrape several brands: "burgerking:12871", "timhortons:102601". These stores are added on top of the location filters and are always included (Max stores does not cut them).

## `includeDeliveryPrices` (type: `boolean`):

Also fetch the in-app delivery price of every item. Burger King always includes pickup and delivery prices. Costs one extra request per store.

## `includeNutrition` (type: `boolean`):

Add full nutrition facts (fat, sodium, sugar, protein…) and allergens (contains / may contain / shared equipment) to menu items.

## `itemKeywords` (type: `array`):

Only output menu items whose name (English or French) contains one of these words, e.g. "iced capp", "whopper", "coffee". Handy for tracking a few prices across many stores. Price history still covers the whole menu.

## `includeUnavailable` (type: `boolean`):

By default, items the store currently marks as unavailable (sold out, ended promotions) are skipped.

## `includeClosedStores` (type: `boolean`):

Also output stores marked closed, temporarily closed or coming soon (store rows only; they have no menu).

## `priceHistoryName` (type: `string`):

Price changes are compared with the last run that used the same name. Use different names to keep separate trackers (e.g. "weekly-ontario").

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

Stop after this many stores (0 = every matching store). A store menu is roughly 400–800 rows, so the default is 10; for store lists only, set 0 to get every store. With several cities or brands, stores are taken from each in turn. When menus are requested, stores with online ordering come first.

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

Stop after saving this many rows in total (0 = no limit). A store's menu is roughly 400–800 rows.

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

How many stores to scrape at once.

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

Apify Proxy is required. The default datacenter proxy works; the actor switches to residential proxies automatically if it gets blocked.

## Actor input object example

```json
{
  "brands": [
    "timhortons"
  ],
  "countries": [
    "CA"
  ],
  "outputs": [
    "stores",
    "menuItems"
  ],
  "cities": [
    "Toronto"
  ],
  "radiusKm": 10,
  "includeDeliveryPrices": false,
  "includeNutrition": false,
  "includeUnavailable": false,
  "includeClosedStores": false,
  "priceHistoryName": "default",
  "maxStores": 3,
  "maxItems": 0,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All scraped rows.

## `run` (type: `string`):

Status and statistics for this run.

# 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 = {
    "brands": [
        "timhortons"
    ],
    "countries": [
        "CA"
    ],
    "outputs": [
        "stores",
        "menuItems"
    ],
    "cities": [
        "Toronto"
    ],
    "maxStores": 3,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("yugenox/rbi-restaurants-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 = {
    "brands": ["timhortons"],
    "countries": ["CA"],
    "outputs": [
        "stores",
        "menuItems",
    ],
    "cities": ["Toronto"],
    "maxStores": 3,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("yugenox/rbi-restaurants-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 '{
  "brands": [
    "timhortons"
  ],
  "countries": [
    "CA"
  ],
  "outputs": [
    "stores",
    "menuItems"
  ],
  "cities": [
    "Toronto"
  ],
  "maxStores": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call yugenox/rbi-restaurants-scraper --silent --output-dataset

```

## MCP server setup

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