# Local Business Lists with Contact Routes (`olliolli/open-business-lists`) Actor

Local businesses by everyday category and town from open map data (Overture Maps, 18 countries), with the public contact route from each business's own website and its source page.

- **URL**: https://apify.com/olliolli/open-business-lists.md
- **Developed by:** [Tom J](https://apify.com/olliolli) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 contact profiles

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

Build **local business lists by category and town** from open map data (Overture Maps, 18 countries) and add each business's **public contact route** from its own website, with the source page. Type everyday words like "kapper", "boulangerie", "plumber" or "Friseur" and a town. No Google scraping, no platform scraping. From **$2 per 1,000 businesses** + $12 per 1,000 contact profiles.

### What you can do with it

- **Build a lead list for any local category**: hairdressers in Ghent, bakeries in Lille, accountants in Eindhoven.
- **Get a source-backed contact route** (email, phone or form) from each business's own website.
- **Include businesses without a website**, with name, address and category from open data.
- **Leave out chains** if you only want independent businesses.

### How to build a local business list in 3 steps

1. **Enter categories** (1–10, in Dutch, French, English or German) and a **town**, and pick the **country**.
2. **Set limits**: how many websites to scan and how many businesses without a website to list.
3. **Click Start.**

### Input

| Field | Meaning | Default |
|---|---|---|
| `categories` | Everyday words, e.g. `kapper`, `coiffeur`, `Friseur`, `fiduciaire`; Overture codes like `hair_salon` also work | required |
| `location` | Municipality or city, e.g. `Gent`, `Pelt`, `Lille` | required |
| `country` | BE NL LU FR DE AT CH GB IE DK SE NO FI ES PT IT PL CZ | `BE` |
| `maxWebsites` | Business websites scanned for contact routes | 100 (max 500) |
| `includeWithoutWebsite` | Add rows for businesses without a website | on |
| `maxListedRows` | Upper limit for those rows | 100 (max 3,000) |
| `excludeChains` | Leave out chain branches | off |
| `contactPurpose` | `any`, `sales`, `general`, `support` | `any` |
| `emailScope` | `role_person` or `all` | `role_person` |
| `channelScope` | `core` or `all` | `core` |
| `conversionRoutes` | Proven booking and quote routes | off |
| `maxPages`, `maxRunSeconds`, `maxConcurrency` | Scan limits | 5, 600, 5 |

### Output

One row per business. With a website: the contact profile (chosen email or route, up to 25 observed emails, each with its source page). Without a website: name, address, category and any phone or social link from the listing, **marked as not verified**. Chain branches are marked (`listing.chain`, with the reason) unless you leave them out.

### How much does a business list cost?

| Event | Price |
|---|---|
| Business listed (no own website) | $0.002 |
| Website checked | $0.002 |
| Contact profile | $0.012 |
| Booking or quote route (with `conversionRoutes`) | $0.005 |
| Actor start | $0.00005 |

**Example:** 800 websites checked, 456 with a contact profile, plus 100 businesses without a website = $1.60 + $5.47 + $0.20 = **$7.27**.

#### What you are not charged for

Rows beyond `maxListedRows`, chains you excluded, and anything a resumed run already charged. Rows are stored before they are charged.

### Measured coverage

In four test municipalities (Pelt, Hasselt, Gent-centre, Eindhoven) the open data listed 2,200–12,000 places each, 73–84% with their own website, and 5–15× more business websites than OpenStreetMap alone. On 200 sampled businesses with a website, **57% returned a usable contact profile and 43% an email address**. Email selection precision of the underlying scanner: **97.4–97.9%** in a 300-site blind benchmark. Results on samples, not a guarantee.

### Integrate with any app

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("olliolli/open-business-lists").call(
    run_input={"categories": ["kapper"], "location": "Gent", "country": "BE"}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row)
```

Send the list to **Google Sheets, a CRM, Make, Zapier or n8n** through Apify's integrations.

### Privacy

Personal email addresses (e.g. of sole traders) are labelled `personal_data: true`; follow the GDPR rules for B2B outreach in your country.

### Good to know

- Open map data is not a complete business register; listings can be outdated. Local shops and services are well covered, trades and offices less so.
- Large cities: at most 3,000 places are read, highest data confidence first.
- An empty result does not mean the business has no email or phone.
- Sources: Overture Maps Places (CDLA Permissive 2.0 / Apache 2.0 / CC0 per source); municipality boundaries from Overture Divisions (© OpenStreetMap contributors, ODbL).

### FAQ

**How is this different from a Google Maps scraper?** It uses open map data instead of Google, and adds a source-backed contact route from each business's own website.

**Which countries are supported?** BE, NL, LU, FR, DE, AT, CH, GB, IE, DK, SE, NO, FI, ES, PT, IT, PL, CZ.

**Can I type categories in Dutch or French?** Yes, and in English or German.

**Why does a business show no website?** The open data has none for it. You still get its name, address and category.

### Need help?

Open an issue on the **Issues** tab.

# Actor input Schema

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

Everyday words in Dutch, French, English or German, e.g. "kapper", "boulangerie", "plumber", "Friseur", "fiduciaire". Accents and umlauts are optional. Overture category codes such as "hair_salon" also work.

## `location` (type: `string`):

Name of the municipality or city, e.g. "Gent", "Pelt", "Lille".

## `country` (type: `string`):

Country of the place (ISO code), for example BE, NL, FR, LU or DE.

## `maxWebsites` (type: `integer`):

Businesses with their own website are scanned for contact routes, up to this number (highest data confidence first).

## `includeWithoutWebsite` (type: `boolean`):

Adds rows with the open-data listing for businesses without their own website (name, address, and any phone or social link from the source, marked as not verified). Each such row triggers the small business-listed event; see maxListedRows.

## `maxListedRows` (type: `integer`):

At most this many businesses without their own website are listed (each triggers business-listed). 0 lists none.

## `excludeChains` (type: `boolean`):

Off (default): branches of chains are kept and marked in the results (listing.chain = true, with the reason), because their website may give head-office contacts instead of the branch. On: they are left out and not scanned; OUTPUT only counts them. Detection uses the brand field of the open data, branch-page website addresses (store locator or branch pages) and one website shared by 3 or more places in the area. Small chains without these signals are not detected.

## `maxPages` (type: `integer`):

Includes the homepage. Robots.txt and bounded redirects are separate requests. Lower limits can miss contacts.

## `maxRunSeconds` (type: `integer`):

Stops new work at this time budget. Unprocessed URLs are listed in OUTPUT. The platform timeout must leave additional time to save the summary.

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

Bounded parallel scans. Higher values use more Actor memory/network capacity and may increase target-site load; 5 is the default.

## `contactPurpose` (type: `string`):

Sales prefers sales, then general, never support; general addresses with nearby customer-service phrases are excluded. This is a conservative heuristic, not a guarantee of commercial suitability. General/support only accept their own mailbox category.

## `emailScope` (type: `string`):

role_person returns role and named company-domain addresses. all also includes freemail and department addresses; these categories are less validated.

## `channelScope` (type: `string`):

Extra social, booking and WhatsApp links are experimental and are not included by default.

## `conversionRoutes` (type: `boolean`):

Also look for online booking/appointment and quote-request routes (proven, with evidence) and online ordering and price routes (candidates, with evidence). Opens the page behind each candidate button once (robots.txt respected) and adds a few seconds per website. Proven routes: in a blind test on 660 new SME websites (BE/NL/FR, October 2026), 48 of 50 booking and quote routes were correct. Candidates are not covered by that figure.

## Actor input object example

```json
{
  "categories": [
    "kapper"
  ],
  "location": "Gent",
  "country": "BE",
  "maxWebsites": 10,
  "includeWithoutWebsite": true,
  "maxListedRows": 20,
  "excludeChains": false,
  "maxPages": 5,
  "maxRunSeconds": 240,
  "maxConcurrency": 5,
  "contactPurpose": "any",
  "emailScope": "role_person",
  "channelScope": "core",
  "conversionRoutes": false
}
```

# Actor output Schema

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

One row per business: open-data listing and, for businesses with their own website, the contact route with its source page.

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

Business search counts, attribution, stop reason and unprocessed websites (OUTPUT).

# 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": [
        "kapper"
    ],
    "location": "Gent",
    "maxWebsites": 10,
    "maxListedRows": 20,
    "maxRunSeconds": 240
};

// Run the Actor and wait for it to finish
const run = await client.actor("olliolli/open-business-lists").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": ["kapper"],
    "location": "Gent",
    "maxWebsites": 10,
    "maxListedRows": 20,
    "maxRunSeconds": 240,
}

# Run the Actor and wait for it to finish
run = client.actor("olliolli/open-business-lists").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": [
    "kapper"
  ],
  "location": "Gent",
  "maxWebsites": 10,
  "maxListedRows": 20,
  "maxRunSeconds": 240
}' |
apify call olliolli/open-business-lists --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,olliolli/open-business-lists"
        }
    }
}
```

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/FvyDx4C406KKYyxiZ/builds/zYIwGYeJTdPa9Lfqe/openapi.json
