# Franchise & Multi-Location Finder — Chains by Category (`inovaflow/franchise-multi-location-finder`) Actor

Find chains and franchise brands by category and region: location count verified on the brand's own store locator, Wikipedia and franchise directories, HQ address and phone, franchise fee and investment range, emails and socials. No login, dataset-only, MCP-ready.

- **URL**: https://apify.com/inovaflow/franchise-multi-location-finder.md
- **Developed by:** [inovaflow](https://apify.com/inovaflow) (community)
- **Categories:** Lead generation, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 brands

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

## Franchise & Multi-Location Finder — Chains by Category

Type a business category and a region — `pizza` in `Texas`, `gym` in `Ontario`, `dental clinic` in `UK` — and get the
**chains and franchise brands** operating there, one row per brand: a **verified location count** (with its source),
the countries and states covered, **HQ address and phone**, whether it is a **franchise** (with the fee, minimum cash
and total investment range when a franchise directory states them), founding year, emails and social profiles, sample
locations and a who-to-sell-to hint. Or paste a list of brand names to verify.

No login, no API key, no third-party Actors. Dataset-only output, MCP-ready. Pay per brand delivered.

### Who it is for

- **Enterprise and mid-market local sales** — POS, loyalty, ordering, scheduling, payroll, insurance, signage,
  cleaning, packaging: anyone who sells to the *system*, not the store. One row = one HQ to call, with the size of the
  footprint it controls.
- **Franchise suppliers and consultants** — brands that franchise, with fee / investment economics and where they are
  expanding.
- **Multi-location marketing and local SEO agencies** — chains by category and region, ranked by size.
- **Market researchers** — how many pizza chains operate in Texas and how big each one is.

### What it does

1. **Discovers** brands on Google Maps: `<category> in <region>` (several result pages), then the busiest cities in
   the results get their own search so regional chains surface too. Places are folded into brands by website domain
   (or normalised name when a place has no website) and ranked by how often they showed up.
2. **Verifies the location count** on the brand's own site: sitemap URLs under `/locations/`, `/stores/`, … (or the
   `locations.` subdomain many chains use), JSON-LD / embedded store lists on the locator page, and the brand's own
   statement ("more than 5,800 clubs"). Wikipedia's infobox ("Number of locations") and the franchise directories'
   unit counts are read too; the row keeps all of them and names the one it chose.
3. **Enriches** each brand: HQ address and phone (JSON-LD Organization, contact / about pages, footer), LinkedIn,
   franchise page, founded year, employees; **Franchise Direct**, **Franchise Gator** and the **IFA directory** for
   franchise fee, minimum cash, total investment range and unit count; emails and social links from the website.
4. **Delivers** brands with at least `minLocations` locations (and franchise evidence when `franchiseOnly`), charging
   per brand — never for candidates below the minimum, duplicates or brands without a verifiable count.

### Why this Actor

- **Brand rows, not place rows.** Google Maps scrapers give you 300 pizza places; this gives you the 15 pizza *chains*
  behind them, each with one HQ and one footprint number.
- **Counts you can defend.** `locationsCountSource` says whether the number came from the brand's store locator,
  Wikipedia, a franchise directory, the brand's own statement or only the Maps sample — and `locationCounts` shows all
  of them side by side.
- **Franchise economics where they exist.** Fee, minimum cash, investment range and franchising-since from the
  directories that publish them; `null` where they do not.
- **Nothing guessed.** Every field is read from a page the row links to (`sources[]`, `locatorUrl`, `wikipediaUrl`,
  `directoryUrls`).

### Output fields

| Field | Description |
| --- | --- |
| `brand`, `domain`, `website` | Brand name (most frequent normalised name among its places), registrable domain (the row key), website |
| `category`, `categories`, `mapsCategories` | Your category that surfaced the brand; all of yours that did; Google Maps categories seen |
| `locationsCount`, `locationsCountSource`, `locationsCountDetail` | The chosen count, its source (`locator` / `wikipedia` / `directory` / `site-claim` / `maps-sample`) and how it was read |
| `locationCounts` | `{ mapsSample, locator, wikipedia, directory, siteClaim }` — every count that was found |
| `countries`, `states`, `regionsSearched` | Countries (ISO) and states / provinces seen in the Maps sample and HQ; the regions you searched that surfaced the brand |
| `hqAddress`, `hqCity`, `hqState`, `hqCountry`, `hqPhone` | Headquarters from the brand site (JSON-LD / contact page / footer), Wikipedia or a directory; HQ phone from the site |
| `isFranchise`, `franchiseEvidence`, `franchisePageUrl` | `true` with evidence (directory listing, franchising page, Wikipedia), else `null` |
| `franchiseFeeUsd`, `minCashUsd`, `investmentMinUsd`, `investmentMaxUsd`, `franchisingSince` | From Franchise Direct / Franchise Gator / IFA, as stated there |
| `foundedYear`, `employees`, `description` | Wikipedia infobox / JSON-LD / directory; site meta description |
| `emails`, `primaryEmail`, `phonesFromWebsite`, `socials`, `linkedinUrl` | Website contacts (charged only when found) |
| `locatorUrl`, `wikipediaUrl`, `directoryUrls` | The pages the facts came from |
| `avgRating`, `totalReviewsInSample`, `sampleLocations[]` | From the Maps sample: average rating, summed reviews, up to 5 places (name, address, phone, rating, Maps link) |
| `sellTo` | Who to sell to, e.g. "5,463-location chain headquartered at …; sell to the franchisor HQ (VP Franchise Development / Operations, CMO, IT / POS lead) …" |
| `sources[]`, `scrapedAt` | Which sources contributed; ISO timestamp |

Also in the key-value store: `BRANDS.csv` (spreadsheet-ready, first 5,000 rows) and `OUTPUT` (run summary: brands per
category, count sources, candidates found / enriched / dropped with reasons, Maps pages, transport stats).

### How to use

1. Enter one or more **categories** (`pizza`, `gym`, `dental clinic`, `car wash`) and **regions** (`Texas`,
   `Ontario`, `UK`). Or paste **brands** to verify (`Jersey Mike's`, `anytimefitness.com`).
2. Set **minLocations** (default 5) and **maxBrands** (default 50). Turn on **franchiseOnly** for franchisors only.
3. Run. Rows land in the dataset as each brand is verified; export as CSV / JSON / Excel or read them through the API
   and MCP.

Tips: a region-level search returns 60–80 places; the city follow-ups add ~250 more, so 30–40 brands are usually seen
twice or more per category × region. Big national chains appear in every category run; regional chains (10–100
locations) are the ones you will not find elsewhere. For an exact worldwide figure look at `locationCounts.wikipedia`;
for the US locator count look at `locationCounts.locator`.

Time budget: the run stops starting new work at the earlier of its own 50-minute ceiling and the run timeout minus
40 s, delivers the brands verified by then and names the cut in the status message and in `OUTPUT.timeBudget`
(`cutShort`, `candidatesUnverified`). Once `maxBrands` rows are delivered the run ends without waiting for brands
still being enriched (they are not charged). With less than 10 minutes on the clock, discovery reads 2 Maps pages
per query and 3 follow-up cities instead of 4 and 5. The form prefill (`pizza` in Texas, 6 brands) finishes in
about a minute at 1024 MB.

### Cost

Pay per event: **$0.05 per brand** delivered, **$0.01 per brand with contacts** (an email or social profile was
found), $0.005 per Actor start. Platform usage is on top and small: a 15-brand run costs about $0.01–0.02 of compute.

### Input example

```json
{
    "categories": ["pizza", "gym"],
    "regions": ["Texas", "Ontario"],
    "minLocations": 5,
    "maxBrands": 40,
    "franchiseOnly": false,
    "enrichContacts": true,
    "brands": ["Jersey Mike's", "anytimefitness.com"]
}
```

### Output sample

```json
{
    "brand": "Cicis Pizza",
    "domain": "cicis.com",
    "website": "https://www.cicis.com/",
    "category": "pizza",
    "locationsCount": 499,
    "locationsCountSource": "locator",
    "locationsCountDetail": "499 sitemap URLs at depth 3 under \"locations\" (612 matched in total)",
    "locationCounts": { "mapsSample": 6, "locator": 499, "wikipedia": 279, "directory": null, "siteClaim": null },
    "countries": ["US"],
    "states": ["Texas"],
    "hqAddress": "1080 W Bethel Rd, Coppell, TX 75019",
    "hqPhone": "(972) 745-4200",
    "isFranchise": true,
    "franchiseEvidence": "listed on franchisegator; franchise page on the brand site (https://www.cicis.com/franchising)",
    "franchiseFeeUsd": 30000,
    "investmentMinUsd": 610000,
    "investmentMaxUsd": 1050000,
    "foundedYear": 1985,
    "emails": ["franchising@cicis.com"],
    "sellTo": "499-location regional chain headquartered at 1080 W Bethel Rd, Coppell, TX 75019; sell to the franchisor HQ (VP Franchise Development / Operations, CMO, IT / POS lead) — franchisees buy locally, the system buys centrally.",
    "sources": ["google-maps", "brand-site", "brand-site-locator", "wikipedia", "franchisegator", "brand-site-contacts"],
    "scrapedAt": "2026-09-26T15:03:20.000Z"
}
```

### FAQ

**Why do the counts differ between sources?** A brand's US site lists its US locations; Wikipedia gives the worldwide
figure of a given year; a directory reports the units it was told. The row keeps all of them in `locationCounts` and
`locationsCountSource` names the one in `locationsCount` (store locator first, then Wikipedia, directory, the brand's
own statement, and the Maps sample as a lower bound — a clearly larger source wins so a partial sitemap never
undercuts the others).

**A brand I know is missing.** Google Maps ranks independents high on region-level queries; raise `mapsPagesPerQuery`
/ `citiesPerRegion`, add the brand to `brands[]`, or search a narrower region. Brands without any verifiable count
are listed in `OUTPUT.dropped` with the reason.

**Is `isFranchise: null` a "no"?** No — it means no evidence was found. Only `true` is asserted.

**Which regions work?** Anything Google Maps understands: US states, Canadian provinces, UK counties / metros,
countries, cities. Google's country setting follows the region (Texas → US, Ontario → CA, UK → GB).

**Does it need proxies?** Every site is read through the run's own connection first (Google Maps included); the proxy
you configure is the per-host fallback. If Maps starts answering with captchas, use RESIDENTIAL.

**Legal.** Reads public pages only: Google Maps search results, brand websites, Wikipedia and public franchise
directory listings. Use the data in line with the sources' terms and your local law.

# Actor input Schema

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

One category per line, the way you would search Google Maps — e.g. `pizza`, `gym`, `dental clinic`, `car wash`, `urgent care`, `hair salon`. Each category is searched in every region.

## `regions` (type: `array`):

One region per line: a US state, Canadian province, country or metro — e.g. `Texas`, `Ontario`, `UK`, `Greater Manchester, UK`. Google Maps is searched for the region and then for its busiest cities; brands that show up more than once are the chain candidates.

## `minLocations` (type: `integer`):

Only deliver brands with at least this many locations (verified count: store locator, Wikipedia, franchise directory, the brand's own statement — or the Google Maps sample as a lower bound).

## `maxBrands` (type: `integer`):

Stop after this many brands have been delivered. Only delivered brands are charged.

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

Paste brand names or domains to look up and verify without searching by category — e.g. `Jersey Mike's`, `anytimefitness.com`. Each is searched on Google Maps by name, then enriched like any other brand.

## `franchiseOnly` (type: `boolean`):

Deliver only brands with franchise evidence: listed on a franchise directory (Franchise Direct, Franchise Gator, IFA), a franchising page on the brand site, or Wikipedia calling it a franchise.

## `enrichContacts` (type: `boolean`):

Reads the brand homepage plus contact / about pages and pulls out email addresses, phone numbers and Facebook / Instagram / LinkedIn / X / YouTube / TikTok links. Charged only when something was found.

## `useDirectories` (type: `boolean`):

Look each brand up on Franchise Direct, Franchise Gator and the IFA directory for franchise fee, minimum cash, total investment range, unit count and founding year.

## `useWikipedia` (type: `boolean`):

Read the brand's Wikipedia infobox ("Number of locations", founded, headquarters, employees). Accepted only when the article's website matches the brand's domain.

## `mapsPagesPerQuery` (type: `integer`):

Result pages (20 places each) read for every region-level search; city follow-ups read up to 3.

## `citiesPerRegion` (type: `integer`):

After the region-level search, the busiest cities in the results get their own search (`<category> in <city>, <region>`) so regional chains surface too. 0 disables it.

## `language` (type: `string`):

Interface language for the Maps results (`hl`), e.g. `en`, `de`, `fr`.

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

How many brands are enriched in parallel.

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

Every site is read through the run's own connection first (Google Maps included — it accepts the platform's egress more often than proxy ranges); this proxy is used only for hosts that block that path. RESIDENTIAL helps when Google Maps starts answering with captchas.

## Actor input object example

```json
{
  "categories": [
    "pizza"
  ],
  "regions": [
    "Texas"
  ],
  "minLocations": 5,
  "maxBrands": 6,
  "franchiseOnly": false,
  "enrichContacts": true,
  "useDirectories": true,
  "useWikipedia": true,
  "mapsPagesPerQuery": 4,
  "citiesPerRegion": 5,
  "language": "en",
  "maxConcurrency": 6,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `brands` (type: `string`):

One row per brand: verified location count with its source, footprint, HQ, franchise facts, contacts, sample locations and a who-to-sell-to hint.

## `csv` (type: `string`):

Spreadsheet / CRM-ready CSV of the brands (first 5,000 rows).

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

Counts: brands delivered per category, location-count sources, candidates found / enriched / dropped with reasons, Google Maps pages and transport statistics.

# 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 = {
    "categories": [
        "pizza"
    ],
    "regions": [
        "Texas"
    ],
    "minLocations": 5,
    "maxBrands": 6,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("inovaflow/franchise-multi-location-finder").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 = {
    "categories": ["pizza"],
    "regions": ["Texas"],
    "minLocations": 5,
    "maxBrands": 6,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("inovaflow/franchise-multi-location-finder").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 '{
  "categories": [
    "pizza"
  ],
  "regions": [
    "Texas"
  ],
  "minLocations": 5,
  "maxBrands": 6,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call inovaflow/franchise-multi-location-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,inovaflow/franchise-multi-location-finder"
        }
    }
}
```

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/nGptItw6BFKydoaOz/builds/LCz0d58S3LsRBLUYk/openapi.json
